9
votes

Comment expliquer la différence de performance dans ces 2 boucles simples?

pour ceux qui sont intéressés par la façon dont je fais le repère, look ici , je simples remplacer / ajouter quelques méthodes près de la méthode de construction dans" boucle 1K ".

Désolé, j'ai oublié de dire mon environnement de test. .NET 4.5 x64 (ne choisissez pas 32bit préféré). dans x86 Les deux méthodes prennent 5 fois autant que le temps.

LOOP2 prend autant de choses Time comme boucle . Je pensais que x ++ / x + = y ne doit pas ralentir lorsque x devient plus grand (puisqu'il faut 1 ou 2 instructions de CPU) < / p>

est-il dû à la localité de référence? Cependant, je pensais que dans Loop2 Il n'y a pas beaucoup de variables, ils doivent tous être proches les uns des autres ... xxx

mise à jour < / Strong>: quand, si jamais, est une boucle déroulante toujours utile? < / a> est utile


9 commentaires

Vous devez montrer comment vous avez mis en place la mesure de la performance.


et combien coûte testsize


Pourrait être un problème d'optimisation. Dans "boucle", l'optimiseur peut supposer en toute sécurité que la variable P est de type int plutôt que longtemps. Par conséquent, l'incrément de la boucle interne peut être effectué plus rapidement.


J'ai des résultats similaires (mais cela ne prend que 2 fois aussi longtemps que je l'essaie). Je l'ai essayé avec testsize = 10000000, version de sortie. A exécuté plusieurs essais dans une boucle, en utilisant chronomètre pour chronométrage.


Je viens de vérifier avec cette méthode Stackoverflow.com/a/1048708/421143 - J'ai des résultats similaires pour les deux méthodes.


@outColdman TRY X64 Build.


@Axelkemper L'environnement de test est X64. int et long ne devrait faire aucune différence.


Je suis surpris qu'ils ne reçoivent pas tous les deux convertis vers Retour TestSize> 0? 10000 * Testsize: 0; . N'est-il pas censé être une construction optimisée?


Je ne sais pas ce qui se passe, mais la localité de référence n'a rien à voir avec cela: vous n'accédez à aucune mémoire.


7 Réponses :


1
votes

La boucle doit être plus rapide que la boucle2, la seule explication qui me vient à mon esprit est que le compilateur optimisant commence et réduit le long p = 0; pour (int j = 0; j <1000; j ++) { p ++; } à quelque chose comme long p = 1000; , la vérification du code d'assembleur généré apporterait une clarté.


2 commentaires

Cela fournirait une vitesse d'environ 1000 fois.


J'ai changé p ++ dans p + = 10 , le problème persiste. Je ne pense pas que la compilée soit suffisamment intelligente pour optimiser + = 10 ... et j'ai décompilé en C # en utilisant ILSPY, n'a pas trouvé d'optimisation.



2
votes

Je ne vois aucune différence de performance appréciable. Utilisation de ce script LINQPAD (et comprenant ces deux méthodes de la tienne):

void Main()
{
    // Warmup the vm
    Loop(10);
    Loop2(10);

    var stopwatch = Stopwatch.StartNew();
    Loop(10 * 1000 * 1000);
    stopwatch.Stop();
    stopwatch.Elapsed.Dump();

    stopwatch = Stopwatch.StartNew();
    Loop2(10 * 1000 * 1000);
    stopwatch.Stop();
    stopwatch.Elapsed.Dump();
}


5 commentaires

Essayez X64 Build, j'ai oublié de mentionner en X86, ils sont similaires. Et Linqpad est par défaut X86 (je l'ai essayé)


@Colinfang, je viens de l'essayer à l'aide d'une application de console spécifiant explicitement x64. Il a effectué exactement la même chose. Avez-vous essayé mon code comme une application de console de votre propre?


J'ai utilisé le vôtre. Voulez-vous visiter l'écran?


@Kirkwoll 22 secondes? Est-ce une version de version ou une construction de débogage que vous testez? L'avez-vous dirigé à l'extérieur du débogueur? J'ai couru votre code sur mon ordinateur et cela n'a pris que 2,5. Je doute que mon ordinateur soit 8 fois plus rapide que le vôtre, alors je soupçonne que vous n'ayez pas timing une version de sortie.


Vous avez raison, je n'utilisais pas une version de sortie. Maintenant, je suis et je reçois le 6.1917275 et 6.1968686 respectivement.



0
votes

J'ai géré mon propre test et je ne vois aucune différence significative. Essayez-le:

using System;
using System.Diagnostics;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch sw = new Stopwatch();
            while (true)
            {
                sw.Start();
                Loop(5000000);
                sw.Stop();
                Console.WriteLine("Loop: {0}ms", sw.ElapsedMilliseconds);
                sw.Reset();

                sw.Start();
                Loop2(5000000);
                sw.Stop();
                Console.WriteLine("Loop2: {0}ms", sw.ElapsedMilliseconds);
                sw.Reset();

                Console.ReadLine();
            }
        }

        static long Loop(long testSize)
        {
            long ret = 0;
            for (long i = 0; i < testSize; i++)
            {
                long p = 0;
                for (int j = 0; j < 1000; j++)
                {
                    p++;
                }
                ret += p;
            }
            return ret;
        }

        static long Loop2(long testSize)
        {
            long ret = 0;
            for (long i = 0; i < testSize; i++)
            {
                for (int j = 0; j < 1000; j++)
                {
                    ret++;
                }
            }
            return ret;
        }
    }

}


1 commentaires

Essayez X64 Build, j'ai oublié de mentionner en X86, ils sont similaires.



1
votes

En regardant l'IL lui-même, Loop2 devrait être plus rapide (et il est plus rapide sur mon ordinateur)

boucle IL xxx

loop2 il < Pré> xxx


3 commentaires

L'analyse seule n'est pas l'histoire entière. Beaucoup d'IL différents peuvent être jetés dans le même résultat efficace.


Il s'agit comme une surprise (négative) que l'optimiseur n'est pas capable de accélérer plus de choses.


@Axelkemper La plupart des optimisations sont fabriquées par le compilateur JIT, plutôt que par le compilateur C #.



1
votes

Je peux confirmer ce résultat sur mon système.

Les résultats de mon test sont les suivants: xxx

Ceci est une version de construction en dehors du débogueur. xxx


0 commentaires

6
votes

On a dit avant plusieurs fois que le X86 JIT fait un meilleur travail que le X64 JIT en matière d'optimisation et on dirait que c'est ce qui se passe dans ce cas. Bien que les boucles se produisent essentiellement la même chose, le code de montage X64 que la création de jiteur est fondamentalement différent, et je pense qu'il représente la différence de vitesse que vous voyez.

Le code de montage entre les deux méthodes diffères dans la boucle interne critique, qui s'appelle 1000 * N fois. C'est ce que je pense de comptabiliser la différence de vitesse.

boucle 1: xxx

boucle 2: xxx

Vous remarquerez que le JIT est déroulant la boucle interne, mais le code réel de la boucle diffère grandement en ce qui concerne le nombre d'instructions faites. La boucle 1 est optimisée pour créer une seule instruction ADD de 40, où la boucle 2 fait 4 ajouter des relevés de 10.

mon (sauvage) estime que le jiteur peut mieux optimiser la variable p car il est défini dans la portée intérieure de la première boucle. Comme il peut détecter que p n'est jamais utilisé en dehors de cette boucle et est vraiment temporaire, il peut appliquer différentes optimisations. Dans la deuxième boucle, vous agissez sur une variable définie et utilisée en dehors de la portée des deux boucles, et les règles d'optimisation utilisées dans le JIT X64 ne le reconnaissent pas comme le même code qui pourrait avoir les mêmes optimisations.


2 commentaires

J'avais pensé que X64 Jit est plus agressif que x86. Par exemple. Quand il s'agit d'éliminer la chèque de portée, la queue récursive ... x64 sont définitivement mieux


Je suis sûr que c'est la bonne réponse (ou à proximité). J'ai trouvé que si vous passez dans l'incrément utilisé dans la boucle interne sous forme de paramètre sur les méthodes, les deux méthodes fonctionnent à presque la même vitesse. Cela prend en charge la notion qu'il s'agit d'une optimisation de la boucle interne et que cela s'appuie sur le compilateur sachant que l'incrément est à la compilation.



0
votes

La boucle extérieure est la même dans les deux cas, mais c'est ce qui bloque le compilateur pour optimiser le code dans le second cas.

Le problème est que la variable RET n'est pas déclarée suffisamment proche de la boucle interne, ce qui n'est pas dans le corps de la boucle extérieure. La variable RET est en dehors de la boucle extérieure, ce qui signifie qu'il est hors de portée de l'optimiseur de compilateur qui ne peut pas optimiser le code à 2 boucles.

Cependant, la variable P est déclarée juste avant la boucle interne, c'est pourquoi elle est bien optimisée.


0 commentaires