43 votes

Pourquoi l'ajout de variables locales ralentit-il le code .NET ?

Pourquoi le fait de commenter les deux premières lignes de cette boucle for et de décommenter la troisième entraîne-t-il une accélération de 42 % ?

int count = 0;
for (uint i = 0; i < 1000000000; ++i) {
    var isMultipleOf16 = i % 16 == 0;
    count += isMultipleOf16 ? 1 : 0;
    //count += i % 16 == 0 ? 1 : 0;
}

Derrière le timing se cache un code d'assemblage très différent : 13 contre 7 instructions dans la boucle. La plate-forme est Windows 7 exécutant .NET 4.0 x64. L'optimisation du code est activée et l'application de test a été exécutée en dehors de VS2010. [ Mise à jour : Projet de reproduction utile pour vérifier les paramètres du projet].

L'élimination du booléen intermédiaire est une optimisation fondamentale, l'une des plus simples de mon expérience des années 1980. Livre du Dragon . Comment se fait-il que l'optimisation n'ait pas été appliquée lors de la génération du CIL ou du JIT du code machine x64 ?

Existe-t-il un interrupteur "Vraiment compilateur, je voudrais que vous optimisiez ce code, s'il vous plaît" ? Bien que je comprenne le sentiment selon lequel l'optimisation prématurée est assimilable à la l'amour de l'argent Je comprends la frustration qu'il y a à essayer de profiler un algorithme complexe dont les routines sont parsemées de problèmes de ce type. Vous travaillez sur les points chauds, mais vous n'avez aucune idée de la région chaude plus large qui pourrait être grandement améliorée en modifiant à la main ce que nous prenons normalement pour acquis du compilateur. J'espère vraiment que j'ai raté quelque chose ici.

Mise à jour : Des différences de vitesse se produisent également pour x86, mais elles dépendent de l'ordre dans lequel les méthodes sont compilées en juste-à-temps. Voir Pourquoi la commande JAT affecte-t-elle les performances ?

Code d'assemblage (comme demandé) :

    var isMultipleOf16 = i % 16 == 0;
00000037  mov         eax,edx 
00000039  and         eax,0Fh 
0000003c  xor         ecx,ecx 
0000003e  test        eax,eax 
00000040  sete        cl 
    count += isMultipleOf16 ? 1 : 0;
00000043  movzx       eax,cl 
00000046  test        eax,eax 
00000048  jne         0000000000000050 
0000004a  xor         eax,eax 
0000004c  jmp         0000000000000055 
0000004e  xchg        ax,ax 
00000050  mov         eax,1 
00000055  lea         r8d,[rbx+rax] 

    count += i % 16 == 0 ? 1 : 0;
00000037  mov         eax,ecx 
00000039  and         eax,0Fh 
0000003c  je          0000000000000042 
0000003e  xor         eax,eax 
00000040  jmp         0000000000000047 
00000042  mov         eax,1 
00000047  lea         edx,[rbx+rax]

9voto

Maciej Points 3666

La question devrait être "Pourquoi est-ce que je vois une telle différence sur ma machine ?". Je ne peux pas reproduire une différence de vitesse aussi importante et je soupçonne qu'il y a quelque chose de spécifique à votre environnement. Il est cependant très difficile de dire ce que cela peut être. Il peut s'agir d'options (de compilation) que vous avez définies il y a quelque temps et que vous avez oubliées.

J'ai créé une application console, reconstruite en mode Release (x86) et exécutée en dehors de VS. Les résultats sont pratiquement identiques, 1,77 secondes pour les deux méthodes. Voici le code exact :

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    int count = 0;

    for (uint i = 0; i < 1000000000; ++i)
    {
        // 1st method
        var isMultipleOf16 = i % 16 == 0;
        count += isMultipleOf16 ? 1 : 0;

        // 2nd method
        //count += i % 16 == 0 ? 1 : 0;
    }

    sw.Stop();
    Console.WriteLine(string.Format("Ellapsed {0}, count {1}", sw.Elapsed, count));
    Console.ReadKey();
}

S'il vous plaît, quiconque a 5 minutes pour copier le code, le reconstruire, l'exécuter en dehors de VS et poster les résultats dans les commentaires de cette réponse. J'aimerais éviter de dire "ça marche sur ma machine".

EDIT

Pour être sûr, j'ai créé un 64 bits L'application Winforms et les résultats sont similaires à ceux de la question - l'application Winforms n'a pas été utilisée. la première méthode est plus lente (1,57 sec) que le second (1,05 sec). La différence que j'observe est de 33% - ce qui est quand même beaucoup. Il semble qu'il y ait un bug dans le compilateur JIT 64 bits de .NET4.

4voto

Will Hartung Points 57465

Je ne peux pas parler du compilateur .NET, ni de ses optimisations, ni même du moment où il effectue ses optimisations.

Mais dans ce cas précis, si le compilateur a intégré cette variable booléenne dans l'instruction réelle et que vous essayez de déboguer ce code, le code optimisé ne correspondra pas au code écrit. Vous ne seriez pas en mesure de passer en revue l'affectation isMulitpleOf16 et de vérifier sa valeur.

Ce n'est qu'un exemple de cas où l'optimisation peut être désactivée. Il peut y en avoir d'autres. L'optimisation peut avoir lieu pendant la phase de chargement du code, plutôt que pendant la phase de génération du code à partir du CLR.

Les runtimes modernes sont assez compliqués, surtout si vous ajoutez le JIT et l'optimisation dynamique au cours de l'exécution. Parfois, je me sens reconnaissant que le code fasse ce qu'il dit.

3voto

Edward Brey Points 8771

C'est un bogue dans le cadre de .NET.

Eh bien, en fait, je ne fais que spéculer, mais j'ai soumis un bogue sur Microsoft Connect pour voir ce qu'ils disent.

2voto

Ben Voigt Points 151460

Je pense que cela est lié à votre autre question. Lorsque je modifie votre code comme suit, la version multi-lignes l'emporte.

oops, seulement sur x86. Sur x64, le multi-ligne est le plus lent et le conditionnel les bat tous les deux haut la main.

class Program
{
    static void Main()
    {
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
        ConditionalTest();
        SingleLineTest();
        MultiLineTest();
    }

    public static void ConditionalTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            if (i % 16 == 0) ++count;
        }
        stopwatch.Stop();
        Console.WriteLine("Conditional test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void SingleLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            count += i % 16 == 0 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }

    public static void MultiLineTest()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        int count = 0;
        for (uint i = 0; i < 1000000000; ++i) {
            var isMultipleOf16 = i % 16 == 0;
            count += isMultipleOf16 ? 1 : 0;
        }
        stopwatch.Stop();
        Console.WriteLine("Multi-line test  --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
    }
}

1voto

romkyns Points 17295

J'ai tendance à voir les choses comme suit : les personnes qui travaillent sur le compilateur ne peuvent faire qu'un certain nombre de choses par an. Si dans ce laps de temps, ils pouvaient implémenter des lambdas ou de nombreuses optimisations classiques, je voterais pour les lambdas. C# est un langage efficace en termes d'effort de lecture et d'écriture de code, plutôt qu'en termes de temps d'exécution.

Il est donc raisonnable que l'équipe se concentre sur les fonctionnalités qui maximisent l'efficacité de la lecture/écriture, plutôt que l'efficacité de l'exécution dans un certain cas particulier (il y en a probablement des milliers).

Au départ, je crois, l'idée était que le JITter fasse toute l'optimisation. Malheureusement, le JIT prend beaucoup de temps et toute optimisation avancée ne fait qu'empirer les choses. Cela n'a donc pas fonctionné aussi bien qu'on aurait pu l'espérer.

Une chose que j'ai constatée en programmant du code très rapide en C#, c'est qu'il arrive assez souvent que l'on se heurte à un goulot d'étranglement GC avant qu'une optimisation comme celle que vous mentionnez ne fasse la différence. Par exemple, si vous allouez des millions d'objets. C# vous laisse très peu de possibilités d'éviter ce coût : vous pouvez utiliser des tableaux de structs à la place, mais le code qui en résulte est vraiment laid en comparaison. Ce que je veux dire, c'est que de nombreuses autres décisions concernant C# et .NET rendent ces optimisations spécifiques moins intéressantes qu'elles ne le seraient dans quelque chose comme un compilateur C++. En fait, ils ont même a abandonné les optimisations spécifiques au CPU dans NGEN En effet, les performances sont échangées contre l'efficacité du programmeur (débogueur).

Ceci dit, j'aimerais amour C# qui utilise réellement les optimisations que le C++ utilisait depuis les années 90. Mais pas au détriment de fonctionnalités comme, par exemple, async/await.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X