34 votes

Pourquoi l’ordre JIT affecte-t-il les performances?

Pourquoi l'ordre dans lequel les méthodes C# dans .NET 4.0 sont juste-à-temps compilé une incidence sur la rapidité de leur exécution? Par exemple, considérons deux méthodes équivalentes:

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);
}

La seule différence est l'introduction d'une variable locale, ce qui affecte le code assembleur généré et la performance de la boucle. Pourquoi c'est le cas c'est une question dans son propre droit.

Peut-être même étrange, c'est que sur x86 (mais pas x64), l'ordre dans lequel les méthodes sont invoquées a environ 20% de l'impact sur les performances. Appeler les méthodes comme ça...

static void Main()
{
    SingleLineTest();
    MultiLineTest();
}

...et SingleLineTest est plus rapide. (Compiler à l'aide de la Version x86 de configuration, assurant que "Optimiser le code", le réglage est activé, et exécutez le test de l'extérieur de VS2010.) Mais inverser l'ordre...

static void Main()
{
    MultiLineTest();
    SingleLineTest();
}

...et les deux méthodes prennent le même temps (ou presque, mais pas tout à fait, tant que MultiLineTest avant). (Lors de l'exécution de ce test, il est utile d'ajouter des appels à l' SingleLineTest et MultiLineTest pour obtenir des échantillons supplémentaires. Combien et de quel ordre n'a pas d'importance, sauf pour lequel la méthode est appelée en premier.)

Enfin, pour démontrer que JIT l'ordre est important, laissez MultiLineTest première, mais à force SingleLineTest à JITed première...

static void Main()
{
    RuntimeHelpers.PrepareMethod(typeof(Program).GetMethod("SingleLineTest").MethodHandle);
    MultiLineTest();
    SingleLineTest();
}

Maintenant, SingleLineTest est plus rapide encore.

Si vous désactivez "Supprimer JIT sur l'optimisation de charger le module" dans VS2010, vous pouvez placer un point d'arrêt en SingleLineTest et de voir que l'assemblée de code dans la boucle est la même quel que soit JIT ordre; toutefois, l'assemblée de code au début de la méthode varie. Mais comment c'est important quand la majeure partie du temps est passé dans la boucle est perplexe.

Un exemple de projet en démontrant ce comportement est sur github.

Il n'est pas clair comment ce comportement affecte les applications du monde réel. Une préoccupation est qu'il peut faire le réglage des performances volatiles, selon l'ordre des méthodes d'arriver à être appelée en premier. Des problèmes de ce genre serait difficile à détecter à l'aide d'un profileur. Une fois que vous avez trouvé les points chauds et optimisé leurs algorithmes, il serait difficile de le savoir sans beaucoup de deviner et vérifier si d'autres speedup est possible par JITing méthodes tôt.

Mise à jour: Voir aussi le Microsoft Connect à l'entrée de ce problème.

25voto

Ben Voigt Points 151460

Veuillez noter que je ne fais pas confiance à la "Supprimer JIT sur l'optimisation de charger le module" option, j'ai lancer le processus sans débogage et de joindre mon débogueur après le JIT a exécuter.

Dans la version où une seule ligne est plus rapide, c'est - Main:

        SingleLineTest();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  call        dword ptr ds:[0019380Ch] 
            MultiLineTest();
00000009  call        dword ptr ds:[00193818h] 
            SingleLineTest();
0000000f  call        dword ptr ds:[0019380Ch] 
            MultiLineTest();
00000015  call        dword ptr ds:[00193818h] 
            SingleLineTest();
0000001b  call        dword ptr ds:[0019380Ch] 
            MultiLineTest();
00000021  call        dword ptr ds:[00193818h] 
00000027  pop         ebp 
        }
00000028  ret 

Notez que MultiLineTest a été placé sur une frontière d'octet 8, et SingleLineTest sur un 4 limite d'octets.

Voici Main pour la version où les deux tournent à la même vitesse:

            MultiLineTest();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  call        dword ptr ds:[00153818h] 

            SingleLineTest();
00000009  call        dword ptr ds:[0015380Ch] 
            MultiLineTest();
0000000f  call        dword ptr ds:[00153818h] 
            SingleLineTest();
00000015  call        dword ptr ds:[0015380Ch] 
            MultiLineTest();
0000001b  call        dword ptr ds:[00153818h] 
            SingleLineTest();
00000021  call        dword ptr ds:[0015380Ch] 
            MultiLineTest();
00000027  call        dword ptr ds:[00153818h] 
0000002d  pop         ebp 
        }
0000002e  ret 

Étonnamment, les adresses choisies par le JIT sont identiques dans les 4 derniers chiffres, même si elle aurait été traitées dans l'ordre inverse. Pas sûr que je crois que plus.

Plus de creuser est nécessaire. Je pense qu'il a été mentionné que le code avant la boucle n'était pas exactement le même dans les deux versions? Cours d'enquête.

Voici la "lenteur" de la version d' SingleLineTest (et j'ai vérifié, les derniers chiffres de l'adresse de fonction n'ont pas changé).

            Stopwatch stopwatch = new Stopwatch();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  mov         ecx,7A5A2C68h 
0000000b  call        FFF91EA0 
00000010  mov         esi,eax 
00000012  mov         dword ptr [esi+4],0 
00000019  mov         dword ptr [esi+8],0 
00000020  mov         byte ptr [esi+14h],0 
00000024  mov         dword ptr [esi+0Ch],0 
0000002b  mov         dword ptr [esi+10h],0 
            stopwatch.Start();
00000032  cmp         byte ptr [esi+14h],0 
00000036  jne         00000047 
00000038  call        7A22B314 
0000003d  mov         dword ptr [esi+0Ch],eax 
00000040  mov         dword ptr [esi+10h],edx 
00000043  mov         byte ptr [esi+14h],1 
            int count = 0;
00000047  xor         edi,edi 
            for (uint i = 0; i < 1000000000; ++i) {
00000049  xor         edx,edx 
                count += i % 16 == 0 ? 1 : 0;
0000004b  mov         eax,edx 
0000004d  and         eax,0Fh 
00000050  test        eax,eax 
00000052  je          00000058 
00000054  xor         eax,eax 
00000056  jmp         0000005D 
00000058  mov         eax,1 
0000005d  add         edi,eax 
            for (uint i = 0; i < 1000000000; ++i) {
0000005f  inc         edx 
00000060  cmp         edx,3B9ACA00h 
00000066  jb          0000004B 
            }
            stopwatch.Stop();
00000068  mov         ecx,esi 
0000006a  call        7A23F2C0 
            Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
0000006f  mov         ecx,797C29B4h 
00000074  call        FFF91EA0 
00000079  mov         ecx,eax 
0000007b  mov         dword ptr [ecx+4],edi 
0000007e  mov         ebx,ecx 
00000080  mov         ecx,797BA240h 
00000085  call        FFF91EA0 
0000008a  mov         edi,eax 
0000008c  mov         ecx,esi 
0000008e  call        7A23ABE8 
00000093  push        edx 
00000094  push        eax 
00000095  push        0 
00000097  push        2710h 
0000009c  call        783247EC 
000000a1  mov         dword ptr [edi+4],eax 
000000a4  mov         dword ptr [edi+8],edx 
000000a7  mov         esi,edi 
000000a9  call        793C6F40 
000000ae  push        ebx 
000000af  push        esi 
000000b0  mov         ecx,eax 
000000b2  mov         edx,dword ptr ds:[03392034h] 
000000b8  mov         eax,dword ptr [ecx] 
000000ba  mov         eax,dword ptr [eax+3Ch] 
000000bd  call        dword ptr [eax+1Ch] 
000000c0  pop         ebx 
        }
000000c1  pop         esi 
000000c2  pop         edi 
000000c3  pop         ebp 
000000c4  ret 

Et le "rapide" de la version:

            Stopwatch stopwatch = new Stopwatch();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  mov         ecx,7A5A2C68h 
0000000b  call        FFE11F70 
00000010  mov         esi,eax 
00000012  mov         ecx,esi 
00000014  call        7A1068BC 
            stopwatch.Start();
00000019  cmp         byte ptr [esi+14h],0 
0000001d  jne         0000002E 
0000001f  call        7A12B3E4 
00000024  mov         dword ptr [esi+0Ch],eax 
00000027  mov         dword ptr [esi+10h],edx 
0000002a  mov         byte ptr [esi+14h],1 
            int count = 0;
0000002e  xor         edi,edi 
            for (uint i = 0; i < 1000000000; ++i) {
00000030  xor         edx,edx 
                count += i % 16 == 0 ? 1 : 0;
00000032  mov         eax,edx 
00000034  and         eax,0Fh 
00000037  test        eax,eax 
00000039  je          0000003F 
0000003b  xor         eax,eax 
0000003d  jmp         00000044 
0000003f  mov         eax,1 
00000044  add         edi,eax 
            for (uint i = 0; i < 1000000000; ++i) {
00000046  inc         edx 
00000047  cmp         edx,3B9ACA00h 
0000004d  jb          00000032 
            }
            stopwatch.Stop();
0000004f  mov         ecx,esi 
00000051  call        7A13F390 
            Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
00000056  mov         ecx,797C29B4h 
0000005b  call        FFE11F70 
00000060  mov         ecx,eax 
00000062  mov         dword ptr [ecx+4],edi 
00000065  mov         ebx,ecx 
00000067  mov         ecx,797BA240h 
0000006c  call        FFE11F70 
00000071  mov         edi,eax 
00000073  mov         ecx,esi 
00000075  call        7A13ACB8 
0000007a  push        edx 
0000007b  push        eax 
0000007c  push        0 
0000007e  push        2710h 
00000083  call        782248BC 
00000088  mov         dword ptr [edi+4],eax 
0000008b  mov         dword ptr [edi+8],edx 
0000008e  mov         esi,edi 
00000090  call        792C7010 
00000095  push        ebx 
00000096  push        esi 
00000097  mov         ecx,eax 
00000099  mov         edx,dword ptr ds:[03562030h] 
0000009f  mov         eax,dword ptr [ecx] 
000000a1  mov         eax,dword ptr [eax+3Ch] 
000000a4  call        dword ptr [eax+1Ch] 
000000a7  pop         ebx 
        }
000000a8  pop         esi 
000000a9  pop         edi 
000000aa  pop         ebp 
000000ab  ret 

Juste les boucles, rapide sur la gauche, lent sur la droite:

00000030  xor         edx,edx                 00000049  xor         edx,edx 
00000032  mov         eax,edx                 0000004b  mov         eax,edx 
00000034  and         eax,0Fh                 0000004d  and         eax,0Fh 
00000037  test        eax,eax                 00000050  test        eax,eax 
00000039  je          0000003F                00000052  je          00000058 
0000003b  xor         eax,eax                 00000054  xor         eax,eax 
0000003d  jmp         00000044                00000056  jmp         0000005D 
0000003f  mov         eax,1                   00000058  mov         eax,1 
00000044  add         edi,eax                 0000005d  add         edi,eax 
00000046  inc         edx                     0000005f  inc         edx 
00000047  cmp         edx,3B9ACA00h           00000060  cmp         edx,3B9ACA00h 
0000004d  jb          00000032                00000066  jb          0000004B 

Les instructions sont identiques (relative sauts, le code machine est identique, même si le démontage montre des adresses différentes), mais l'alignement est différent. Il y a trois sauts. l' je chargement d'une constante 1 est aligné dans la version lente et pas dans la version rapide, mais il est un peu des questions, étant donné que le saut n'est pris 1/16 de l'époque. Les deux autres sauts ( jmp après le chargement d'une constante zéro, et jb de répéter la totalité de la boucle) sont pris en millions de fois plus, et sont alignés dans le "rapide" de la version.

Je pense que c'est le smoking gun.

0voto

Andrew Points 2910

Donc, pour une réponse définitive... je pense que nous aurions besoin de creuser dans la dis-assemblée.

Cependant, j'ai une supposition. Le compilateur pour l' SingleLineTest() magasins chaque résultat de l'équation sur la pile et de la pop chaque valeur en tant que de besoin. Cependant, l' MultiLineTest() peuvent stocker des valeurs et d'avoir accès à eux à partir de là. Cela pourrait causer quelques cycles d'horloge pour être manquée. Où que l'accaparement des valeurs en dehors de la pile sera le garder dans un registre.

Fait intéressant, la modification de l'ordre de la fonction de compilation peut être ajuster le garbage collector actions. Parce qu' isMultipleOf16 est définie à l'intérieur de la boucle, il peut être manipulé drôle. Vous pouvez déplacer la définition en dehors de la boucle et de voir ce qui change...

0voto

lukas Points 7789

Mon temps est 2400 et 2600 sur RAM Ram 7bit i5-2410M 2,3Ghz 4 Go 64 bits.

Voici ma sortie: Single first

Après avoir démarré le processus puis attaché le débogueur

             SingleLineTest();
            MultiLineTest();
            SingleLineTest();
            MultiLineTest();
            SingleLineTest();
            MultiLineTest();
--------------------------------
SingleLineTest()
           Stopwatch stopwatch = new Stopwatch();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  mov         ecx,685D2C68h 
0000000b  call        FFD91F70 
00000010  mov         esi,eax 
00000012  mov         ecx,esi 
00000014  call        681D68BC 
            stopwatch.Start();
00000019  cmp         byte ptr [esi+14h],0 
0000001d  jne         0000002E 
0000001f  call        681FB3E4 
00000024  mov         dword ptr [esi+0Ch],eax 
00000027  mov         dword ptr [esi+10h],edx 
0000002a  mov         byte ptr [esi+14h],1 
            int count = 0;
0000002e  xor         edi,edi 
            for (int i = 0; i < 1000000000; ++i)
00000030  xor         edx,edx 
            {
                count += i % 16 == 0 ? 1 : 0;
00000032  mov         eax,edx 
00000034  and         eax,8000000Fh 
00000039  jns         00000040 
0000003b  dec         eax 
0000003c  or          eax,0FFFFFFF0h 
0000003f  inc         eax 
00000040  test        eax,eax 
00000042  je          00000048 
00000044  xor         eax,eax 
00000046  jmp         0000004D 
00000048  mov         eax,1 
0000004d  add         edi,eax 
            for (int i = 0; i < 1000000000; ++i)
0000004f  inc         edx 
00000050  cmp         edx,3B9ACA00h 
00000056  jl          00000032 
            }
            stopwatch.Stop();
00000058  mov         ecx,esi 
0000005a  call        6820F390 
            Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
0000005f  mov         ecx,6A8B29B4h 
00000064  call        FFD91F70 
00000069  mov         ecx,eax 
0000006b  mov         dword ptr [ecx+4],edi 
0000006e  mov         ebx,ecx 
00000070  mov         ecx,6A8AA240h 
00000075  call        FFD91F70 
0000007a  mov         edi,eax 
0000007c  mov         ecx,esi 
0000007e  call        6820ACB8 
00000083  push        edx 
00000084  push        eax 
00000085  push        0 
00000087  push        2710h 
0000008c  call        6AFF48BC 
00000091  mov         dword ptr [edi+4],eax 
00000094  mov         dword ptr [edi+8],edx 
00000097  mov         esi,edi 
00000099  call        6A457010 
0000009e  push        ebx 
0000009f  push        esi 
000000a0  mov         ecx,eax 
000000a2  mov         edx,dword ptr ds:[039F2030h] 
000000a8  mov         eax,dword ptr [ecx] 
000000aa  mov         eax,dword ptr [eax+3Ch] 
000000ad  call        dword ptr [eax+1Ch] 
000000b0  pop         ebx 
        }
000000b1  pop         esi 
000000b2  pop         edi 
000000b3  pop         ebp 
000000b4  ret 
 

Multi d'abord:

             MultiLineTest();

            SingleLineTest();
            MultiLineTest();
            SingleLineTest();
            MultiLineTest();
            SingleLineTest();
            MultiLineTest();
--------------------------------
SingleLineTest()
            Stopwatch stopwatch = new Stopwatch();
00000000  push        ebp 
00000001  mov         ebp,esp 
00000003  push        edi 
00000004  push        esi 
00000005  push        ebx 
00000006  mov         ecx,685D2C68h 
0000000b  call        FFF31EA0 
00000010  mov         esi,eax 
00000012  mov         dword ptr [esi+4],0 
00000019  mov         dword ptr [esi+8],0 
00000020  mov         byte ptr [esi+14h],0 
00000024  mov         dword ptr [esi+0Ch],0 
0000002b  mov         dword ptr [esi+10h],0 
            stopwatch.Start();
00000032  cmp         byte ptr [esi+14h],0 
00000036  jne         00000047 
00000038  call        682AB314 
0000003d  mov         dword ptr [esi+0Ch],eax 
00000040  mov         dword ptr [esi+10h],edx 
00000043  mov         byte ptr [esi+14h],1 
            int count = 0;
00000047  xor         edi,edi 
            for (int i = 0; i < 1000000000; ++i)
00000049  xor         edx,edx 
            {
                count += i % 16 == 0 ? 1 : 0;
0000004b  mov         eax,edx 
0000004d  and         eax,8000000Fh 
00000052  jns         00000059 
00000054  dec         eax 
00000055  or          eax,0FFFFFFF0h 
00000058  inc         eax 
00000059  test        eax,eax 
0000005b  je          00000061 
0000005d  xor         eax,eax 
0000005f  jmp         00000066 
00000061  mov         eax,1 
00000066  add         edi,eax 
            for (int i = 0; i < 1000000000; ++i)
00000068  inc         edx 
00000069  cmp         edx,3B9ACA00h 
0000006f  jl          0000004B 
            }
            stopwatch.Stop();
00000071  mov         ecx,esi 
00000073  call        682BF2C0 
            Console.WriteLine("Single-line test --> Count: {0}, Time: {1}", count, stopwatch.ElapsedMilliseconds);
00000078  mov         ecx,6A8B29B4h 
0000007d  call        FFF31EA0 
00000082  mov         ecx,eax 
00000084  mov         dword ptr [ecx+4],edi 
00000087  mov         ebx,ecx 
00000089  mov         ecx,6A8AA240h 
0000008e  call        FFF31EA0 
00000093  mov         edi,eax 
00000095  mov         ecx,esi 
00000097  call        682BABE8 
0000009c  push        edx 
0000009d  push        eax 
0000009e  push        0 
000000a0  push        2710h 
000000a5  call        6B0A47EC 
000000aa  mov         dword ptr [edi+4],eax 
000000ad  mov         dword ptr [edi+8],edx 
000000b0  mov         esi,edi 
000000b2  call        6A506F40 
000000b7  push        ebx 
000000b8  push        esi 
000000b9  mov         ecx,eax 
000000bb  mov         edx,dword ptr ds:[038E2034h] 
000000c1  mov         eax,dword ptr [ecx] 
000000c3  mov         eax,dword ptr [eax+3Ch] 
000000c6  call        dword ptr [eax+1Ch] 
000000c9  pop         ebx 
        }
000000ca  pop         esi 
000000cb  pop         edi 
000000cc  pop         ebp 
000000cd  ret
 

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