【问题标题】:Why does JIT order affect performance?为什么 JIT 顺序会影响性能?
【发布时间】:2012-05-11 12:24:27
【问题描述】:

为什么 .NET 4.0 中的 C# 方法的即时编译顺序会影响它们的执行速度?例如,考虑两种等效方法:

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

唯一的区别是引入了一个局部变量,它会影响生成的汇编代码和循环性能。为什么会这样是question in its own right

可能更奇怪的是,在 x86(但不是 x64)上,调用方法的顺序对性能有大约 20% 的影响。像这样调用方法...

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

...和SingleLineTest 更快。 (使用 x86 Release 配置进行编译,确保启用“优化代码”设置,并从 VS2010 外部运行测试。)但是颠倒顺序...

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

...并且这两种方法都需要相同的时间(几乎,但不完全是,只要 MultiLineTest 之前)。 (运行此测试时,添加一些对SingleLineTestMultiLineTest 的额外调用以获取额外样本很有用。多少和什么顺序无关紧要,除了首先调用哪个方法。)

最后,为了证明 JIT 顺序很重要,先离开 MultiLineTest,但强制 SingleLineTest 先进行 JIT...

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

现在,SingleLineTest 又快了。

如果在VS2010中关闭“Suppress JIT optimization on module load”,可以在SingleLineTest下下断点,看到循环中的汇编代码不管JIT顺序都是一样的;但是,方法开头的汇编代码会有所不同。但是,当大部分时间都花在循环中时,这有多重要。

sample project demonstrating this behavior 在 github 上。

目前尚不清楚这种行为如何影响实际应用程序。一个问题是它会使性能调整变得不稳定,这取决于恰好首先调用方法的顺序。使用分析器很难检测到此类问题。一旦你找到了热点并优化了它们的算法,如果不进行大量猜测,就很难知道是否可以尽早通过 JITing 方法来实现额外的加速。

更新:有关此问题,另请参阅 Microsoft Connect entry

【问题讨论】:

  • "...对性能有大约 20% 的影响" 我得到了大约 8%
  • (实际指令的)对齐方式是否相同?要么按照另一种方法进行 JIT,要么对序言进行微小的更改,都可能导致对齐问题。
  • @lukas 在两台配备 Intel Core i5s 的计算机上,SingleLineTest 的平均时间为 1412 毫秒和 1490 毫秒,MultiLineTest 的平均时间为 1773 毫秒和 1792 毫秒。这可以实现 26% 和 20% 的加速。对于每台计算机,加速的标准偏差为 2%。我本来希望看到机器之间的一些差异,但 8% 令人惊讶。
  • 在移动 Core i7(第一代)上,我得到了大约 2000 和 2400,所以差不多是 20%。

标签: c# .net performance compiler-construction jit


【解决方案1】:

所以为了得到一个明确的答案......我怀疑我们需要深入研究拆卸。

不过,我有一个猜测。 SingleLineTest() 的编译器将方程的每个结果存储在堆栈上,并根据需要弹出每个值。但是,MultiLineTest() 可能正在存储值并且必须从那里访问它们。这可能会导致错过几个时钟周期。从堆栈中获取值会将其保存在寄存器中。

有趣的是,改变函数编译的顺序可能就是调整垃圾收集器的动作。因为isMultipleOf16 是在循环中定义的,所以处理起来可能很有趣。您可能希望将定义移到循环之外,看看会发生什么变化...

【讨论】:

  • 由于isMultipleOf16 是一个值类型,它存储在堆栈中并且不被GC 触及。除了秒表和控制台输出之外,GC 不应该做任何事情。
  • " 由于 isMultipleOf16 是一个值类型,它存储在堆栈中,不会被 GC 触及" 这是不正确的。没有这样的保证。 JITer 可以将值类型移动到堆中。但我想这里不是这样。
  • 如果将定义移出循环会有所不同吗?
  • @Andrew:我试过了,将isMultipleOf16 移出循环并不重要。至少,您似乎可以指望编译器这么多。
  • @Andrew:我确实看过反汇编并将我的想法放在上面问题的正文中。如果您愿意,我提供了有关如何自己检查的提示(最简单的方法是从 github 上获取示例项目的 Zip 并使用它)。
【解决方案2】:

请注意,我不信任“在模块加载时抑制 JIT 优化”选项,我在没有调试的情况下生成进程,并在 JIT 运行后附加调试器。

在单行运行速度更快的版本中,这是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 

请注意,MultiLineTest 已放置在 8 字节边界上,SingleLineTest 已放置在 4 字节边界上。

这里是Main,表示两者以相同速度运行的版本:

            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 

令人惊讶的是,JIT 选择的地址在最后 4 位上相同,尽管据称它以相反的顺序处理它们。我不确定我是否相信了。

需要更多的挖掘。我认为有人提到循环之前的代码在两个版本中并不完全相同?要去调查。

这是SingleLineTest 的“慢”版本(我检查了,函数地址的最后一位数字没有改变)。

            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 

还有“快速”版本:

            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 

只有循环,左边快,右边慢:

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 

指令是相同的(相对跳转,机器码是相同的,即使反汇编显示不同的地址),但对齐方式不同。有三个跳跃。加载常量1je 在慢速版本中对齐,而不是在快速版本中对齐,但这并不重要,因为该跳转只占用了 1/16 的时间。其他两个跳转(jmp 在加载一个常量零之后,jb 重复整个循环)需要数百万次,并在“快速”版本中对齐。

我认为这是确凿的证据。

【讨论】:

  • “函数地址的最后一位数字没有改变”对我来说也是如此,但我的反汇编输出是 104 行长,这是一个 75。
  • 是的。构建选项卡:目标是 x86,检查优化代码,我从 Debug -> Windows -> Disassembly 获得了 asm
  • @lukas:你能不能把它粘贴到“答案”或 pastebin/ideone/whatever 上,这样我可以看到你有什么?
  • 你说的是4字节对齐吗? “快速” jmp 只是对齐一个,不是吗?
  • @lukas: jmp 44hjb 32h 都比 jmp 5Dhjb 4Bh 对齐得更好。哦,对不起,我把循环结束和循环中的条件弄混了,不是吗?说明已修复。
【解决方案3】:

我在 i5-2410M 2,3Ghz 4GB ram 64bit Win 7 上的时间是 2400 和 2600。

这是我的输出:单身优先

启动进程后附加调试器

            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 

多优先:

            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

【讨论】:

  • 看到那些nop 说明了吗?我认为这没有优化,在调试器内运行抑制优化。
  • @Ben 是的,我稍后附加了调试器,但输出有点不同。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-17
  • 1970-01-01
  • 2015-01-10
  • 2015-11-26
  • 1970-01-01
相关资源
最近更新 更多