IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Linq Discussion :

Enumération réévalué à chaque appel


Sujet :

Linq

  1. #1
    maa
    maa est déconnecté
    Membre actif
    Avatar de maa
    Inscrit en
    Octobre 2005
    Messages
    672
    Détails du profil
    Informations personnelles :
    Âge : 40

    Informations forums :
    Inscription : Octobre 2005
    Messages : 672
    Points : 288
    Points
    288
    Par défaut Enumération réévalué à chaque appel
    Bonjour,


    Je m'étonne du comportement de ce code :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    List<string> tests = new List<string> { "a", "b", "c","d" };
    var newTests = tests.Select(y =>
                    { 
                        return y; 
                    })
    Le delagate inclus dans la méthode Select est réévalué à chaque appel sur newTests.

    Si je veux empêcher cela, il faut que newTests soit une collection (il suffit d'ajouter .ToList()).

    Pourquoi un tel comportement pour les énumérations ?

    Merci d'avance pour vos éclaircissements.

    mathmax
    ****************************************

    - I don’t write plumbing code anymore
    - I use PostSharp
    - And you?


    ****************************************

  2. #2
    Membre confirmé
    Profil pro
    Inscrit en
    Janvier 2007
    Messages
    547
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2007
    Messages : 547
    Points : 627
    Points
    627
    Par défaut
    Salut maa,

    En fait ton select n'est evalué qu'au dernier moment, quand tu "forecheras" ton IEnumerable. A noter aussi (on ne s'en rend pas compte avec ce var (pas bien les var !)), que newTests est un IEnumerable<string>, pas une collection, et n'a pas d'existence reelle, il n'existe que quand tu l'appelles et de maniere sequentielle (les valeurs existeront les unes apres les autres mais jamais en tant que collection).

    Quand tu fais un .ToList(), l'enumeration acquiert une existence concrete puisqu'en derriere le Fx a fait un return new List<T>(IEnumerable<T>).

    En conclusion, si tu dois evaluer plusieurs fois, une enumeration "linquée", mieux vaux la fixer le plus tot possible pour eviter, comme tu l'as vu, la reevaluation constante de ces valeurs.

    Un snippet pour etre plus clair :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
        class Program
        {
            static int i = 0;
            static void Main()
            {
                List<int> test = new List<int>(Enumerable.Range(0, 100));
     
                IEnumerable<int> enu1 = test.Select(
                    x =>
                    {
                        i++;
                        return x * 2;
                    }
                    );
     
                for (int j = 0; j < 3; j++)
                {
                    foreach (int item in enu1)
                    {
                        Console.WriteLine(item);
                    }
                }
     
                Trace.WriteLine(i);
     
                i = 0;
     
                IEnumerable<int> enu2 = test.Select(
                    x =>
                    {
                        i++;
                        return x * 2;
                    }
                    ).ToList() /*Je transforme en List<int> !*/;
     
                for (int j = 0; j < 3; j++)
                {
                    foreach (int item in enu2)
                    {
                        Console.WriteLine(item);
                    }
                }
                Trace.WriteLine(i);
     
                Console.Read();
     
            }
        }
    Je créé une liste d'int de 1 à 100, et je fais select dessus pour me retourner le double du current. Dans le premier cas, le delegate sera appelé 300 fois, il est bien reevalué à chaque fois que j'utilise le IEnumerable. Dans le second cas, il n'est evalué que 100 fois et plus tot que dans le premier cas, en fait l'evaluation se produit au moment de la creation de la liste, mais plus pour pour utiliser l'IEnumerable (qui n'est plus un IEnumerable generé par Linq mais par ma collection).

    Enfin à noter un danger, si je fais ca :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
        class Program
        {
            static int i = 0;
            static void Main()
            {
                List<int> test = new List<int>(Enumerable.Range(0, 100));
     
                IEnumerable<int> enu1 = test.Select(
                    x =>
                    {
                        i++;
                        return x * 2;
                    }
                    );
     
                test.Clear();
     
                for (int j = 0; j < 3; j++)
                {
                    foreach (int item in enu1)
                    {
                        Console.WriteLine(item);
                    }
                }
     
                Console.Read();
     
            }
        }
    donc si j'efface le sous-jacent de mon IEnumerable, newTests n'enumere sur plus rien du tout, sa source ayant été effacé, cela tend à prouver que les IEnumerable sont "lazy evalué", n'ont aucune existence concrete, et sont totalement dependante de leur source.

  3. #3
    maa
    maa est déconnecté
    Membre actif
    Avatar de maa
    Inscrit en
    Octobre 2005
    Messages
    672
    Détails du profil
    Informations personnelles :
    Âge : 40

    Informations forums :
    Inscription : Octobre 2005
    Messages : 672
    Points : 288
    Points
    288
    Par défaut
    Merci pour tes explications.

    En fait c'est un peu plus subtile que ça il me semble. Quand la méthode d'extension possède un yield return alors elle est évalué à chaque appel. Si par contre elle possède un simple return alors elle est évaluée une fois au début. Pourquoi ?...

    On peut s'en rendre compte en écrivant les méthodes Select2 et Select3 suivante :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    public static IEnumerable<TResult> Select2<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
    {
    	foreach (var item in en)
    	    yield return selector(item);
    }
     
    public static IEnumerable<TResult> Select3<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
    {
    	return  en.Select(selector);
    }
    ****************************************

    - I don’t write plumbing code anymore
    - I use PostSharp
    - And you?


    ****************************************

  4. #4
    Membre confirmé
    Profil pro
    Inscrit en
    Janvier 2007
    Messages
    547
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2007
    Messages : 547
    Points : 627
    Points
    627
    Par défaut
    Salut Maa,

    en fait, c'est la meme chose, dans Select2, tu recrées ton propre Select qui se suffit à lui meme, le yield return servant à marquer l'evaluation sequentielle du schmilblik grace au delegué fourni. Dans la seconde, en fait le code est deporté dans la methode d'extension standard Select (this IEnumerable ... ), tu ne fais que retourner la reference vers l'iterateur et tu ne peux pas voir l'evaluation (La classe genéré pour iterer le select est marqué avec l'attribut [DebuggerHidden] ).

    Essaie de refaire le test avec ca :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static IEnumerable<TResult> Select2<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                foreach (var item in en)
                    yield return selector(item);
            }
     
            public static IEnumerable<TResult> Select3<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                return en.Select(selector);
            }
     
            public static IEnumerable<TResult> Select4<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                return en.Select2(selector);
            }
    en utilisant Select4 en lui et place de Select2 dans ton code, tu verras que Select4 est appelé une fois, et ensuite l'iteration est assuré par Select2 (le meme principe s'applique pour Select3 et le Select standard).

  5. #5
    maa
    maa est déconnecté
    Membre actif
    Avatar de maa
    Inscrit en
    Octobre 2005
    Messages
    672
    Détails du profil
    Informations personnelles :
    Âge : 40

    Informations forums :
    Inscription : Octobre 2005
    Messages : 672
    Points : 288
    Points
    288
    Par défaut
    Tout à fait d'accord, mais en plaçant un point d'arrêt dans Select2 et Select3, tu te rend compte que la première fonction est appelée à chaque appel sur une variable définit comme ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    var e = list.Select2(y=>y);
    Alors qu'avec la méthode Select3 (j'aurais pu mettre n'importe quoi dedans, j'ai choisi de rediriger vers le Select standard pour la simplicité de l'exemple), celle-ci est évaluée une fois au moment de l'initialisation de "e" et jamais rappelée sur les appels à "e".
    Ceci est dû au fait que j'utilise un return à la place du yield return.
    ****************************************

    - I don’t write plumbing code anymore
    - I use PostSharp
    - And you?


    ****************************************

  6. #6
    Membre confirmé
    Profil pro
    Inscrit en
    Janvier 2007
    Messages
    547
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2007
    Messages : 547
    Points : 627
    Points
    627
    Par défaut
    Je n'ai pas trop compris ta reponse (c'est quoi 'e' ? =p), mais en gros pour essayer d'etre plus clair, ici la difference yield return et return n'est pas importante, le yield c'est une facilité syntaxique pour dire "Créer moi un interateur". Le comportement produit, in fine, sera identique, c'est à dire, "créé moi un iterateur" (dans Select2, l'iterateur va etre produit à la compilation, dans Select3, il sera produit dans System.Linq.Enumerable.<SelectIterator>d__d<TSource, TResult>) et parcours le (if (MoveNext()) Current; etc ...).

    Donc la seule difference entre yield-returner un iterateur, ou returner un iteratuer, c'est qui va créer l'iterateur, et le nombre de saut de fonction avant de retrouver la fonction. Pour le reste, ca donne lieu au meme nombre d'evaluation. A noter aussi, que quand bien meme tu return (sans yield) un iteratuer un fin de chaine, il y aura toujours un yield return (ou une implementation d'IEnumerable<T>).

    En esperant avoir été plus clair.

  7. #7
    maa
    maa est déconnecté
    Membre actif
    Avatar de maa
    Inscrit en
    Octobre 2005
    Messages
    672
    Détails du profil
    Informations personnelles :
    Âge : 40

    Informations forums :
    Inscription : Octobre 2005
    Messages : 672
    Points : 288
    Points
    288
    Par défaut
    En fait le "e" désignait le nom de la variable dans le code de mon précédent message.

    Tu as raison, l'évaluation du filtre est faite à chaque appel dans les 2 cas, par contre ce code montre que Select3 n'est pas réévalué à chaque fois... ce qui a une conséquence sur les performances.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
     
        class Program
        {
            static void Main(string[] args)
            {
                List<string> s = new List<string> { "aaa", "aab", "ccc" };
                Func<string, string> mySelector = new Func<string,string>(y=>y);
     
                string result;
                var test1 = s.Select2(mySelector);
                var test2 = s.Select3(mySelector);
     
                Stopwatch sw = new Stopwatch();
                sw.Reset();
                sw.Start();
                result = test1.First();
                sw.Stop();
     
                Console.WriteLine(sw.ElapsedMilliseconds.ToString());
     
                sw.Reset();
                sw.Start();
                result = test2.First();
                sw.Stop();
     
                Console.WriteLine(sw.ElapsedMilliseconds.ToString());
            }
        }
     
        public static class Extensions
        {
            public static IEnumerable<TResult> Select2<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                var list = en.ToList();
                Thread.Sleep(1000);
                foreach (var item in list)
                    yield return selector(item);
            }
     
            public static IEnumerable<TResult> Select3<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                var list = en.ToList();
                Thread.Sleep(1000);
                return list.Select(selector);    
            }
        }
    Il semble que Select3 soit appelée une fois lors de l'initialisation de ma variable puis chaque appel sur ma variable redirige directement vert Select sans repasser par Select3. Pourquoi... ?
    ****************************************

    - I don’t write plumbing code anymore
    - I use PostSharp
    - And you?


    ****************************************

  8. #8
    Membre confirmé
    Profil pro
    Inscrit en
    Janvier 2007
    Messages
    547
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Janvier 2007
    Messages : 547
    Points : 627
    Points
    627
    Par défaut
    En fait, le point que tu souleves quant aux questions de perfs, c'est plus du à des facteurs 'externes' : regarde ton snip reecrit pour tester 20 fois chaque foreach, tu verras que les temps ont tendance à converger (au bout de 5 iterations, je tends vers les 10 ticks, je ne sais meme pas si le stopwatch est encore valable dans ces intervalles). La difference entre les deux sur une iteration, se joue surtout sur des initialisateurs de type (des ctor static) qui doivent etre appelé, apparement les iterateurs de Enumerable (namespace Linq) ont beaucoup moins de dependance (cf Reflector), ce qui peut (du moins pourrait, ne soyons pas affirmatif) expliquer les differences sur les premieres iterations. J'ai testé avec ce code :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
        class Program
        {
            static void Main(string[] args)
            {
                List<string> s = new List<string> { "aaa", "aab", "ccc" };
                Func<string, string> mySelector = new Func<string, string>(y => y);
     
                System.Linq.Enumerable.Select(s, mySelector);
                string result;
                var test1 = s.Select2(mySelector);
                var test2 = s.Select3(mySelector);
     
                Stopwatch sw = new Stopwatch();
     
                for (int i = 0; i < 20; i++)
                {
                    sw.Reset();
                    sw.Start();
     
                    foreach (string item in test1)
                    {
                        result = item;
                    }
                    sw.Stop();
     
                    Console.WriteLine("Select2 : iteration {0}  {1} ticks", i, sw.ElapsedTicks);
                }
     
                for (int i = 0; i < 20; i++)
                {
     
                    sw.Reset();
                    sw.Start();
                    foreach (string item in test2)
                    {
                        result = item;
                    }
     
                    sw.Stop();
                    Console.WriteLine("Select3 : iteration {0}  {1} ticks", i, sw.ElapsedTicks);
                }
     
            }
        }
     
        public static class Extensions
        {
            public static IEnumerable<TResult> Select2<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
     
                foreach (var item in en)
                    yield return selector(item);
            }
     
            public static IEnumerable<TResult> Select3<T, TResult>(this IEnumerable<T> en, Func<T, TResult> selector)
            {
                return en.Select(selector);
            }
        }
    Pour le fait qu'on ne passe qu'une fois par select3, c'est normal, Select3 te renvoie une reference vers un iterateur qui est contenu dans un assembly standard (System.Core) et cet iterateur tu ne peux pas le parcourir par un step in au debuggage, donc tu ne vois que l'appel à l'evaluateur qui lui est declaré dans ton code. Dans Select2, tu vois le debugger passer dans le foreach au fur et à mesure, c'est aussi normal, la classe qui sera créé à la compilation par le foreach meme si tu ne peux pas la parcourir en direct, le debugger à des infos de debuggages pour faire le rapprochement entre l'iteration dans la classe generé, qui ressemble à ca :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    while (iter.MoveNext())
    {
         ... = iter.Current;
    }
    et ce que tu as ecris, à savoir :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    foreach(string item in enu)
    {
        ... = item;
    }
    .

    Mais il n'y pas vraiment de difference entre les deux methodes (mis à part ce que j'ai dit au dessus), nul part il n'y a de foreach (le foreach n'existe pas en IL, c'est le meme principe que pour using(){} pour le pattern dispose, => reflector encore une fois, options "optimisations : aucune"), mais juste dans un cas des methodes parcourable avec un debugger et d'autres non.

    Pour rapprocher ca de l'actualité, tu referas le test une fois qu'on pourra parcourir les sources du fx avec VS2008 (yeah !), et tu verras que ca donne exactement la meme chose.

Discussions similaires

  1. Variable qui change de valeur à chaque appel de fonction
    Par bpascal123 dans le forum Débuter
    Réponses: 5
    Dernier message: 12/03/2010, 11h47
  2. Nouvelle instance de service créée à chaque appel
    Par blepeign dans le forum Services Web
    Réponses: 1
    Dernier message: 03/03/2010, 23h17
  3. Réponses: 22
    Dernier message: 17/11/2009, 18h16
  4. Réponses: 11
    Dernier message: 21/01/2009, 15h55
  5. Perl.exe crash à chaque appel
    Par Fabien Celaia dans le forum Langage
    Réponses: 4
    Dernier message: 07/06/2006, 08h56

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo