当前位置: 首页 > 知识库问答 >
问题:

为什么JIT订单会影响性能?

姬雪松
2023-03-14

为什么。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);
}

唯一的区别是引入了局部变量,这会影响生成的汇编代码和循环性能。为什么会这样,这本身就是一个问题。

可能更奇怪的是,在x86(而不是x64)上,调用方法的顺序对性能有大约20%的影响。调用如下方法。。。

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

...单线测试速度更快。(使用x86版本配置编译,确保启用了“优化代码”设置,并从VS2010外部运行测试。)但是颠倒顺序。。。

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

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

最后,为了证明JIT顺序很重要,请将多行测试放在第一位,但强制将单线测试放在第一位。。。

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

现在,单线测试又快了一点。

如果在VS2010中关闭“在模块加载时抑制JIT优化”,则可以在SingleLineTest中放置一个断点,并可以看到循环中的汇编代码是相同的,无论JIT顺序如何;但是,方法开头的汇编代码各不相同。但当大部分时间都花在循环中时,这又有什么关系呢?这令人困惑。

github上有一个演示此行为的示例项目。

目前尚不清楚这种行为如何影响实际应用程序。一个问题是,它可能会使性能调整变得不稳定,这取决于第一次调用方法的顺序。这类问题很难用探查器检测出来。一旦您找到了热点并优化了它们的算法,如果不进行大量猜测和检查,就很难知道早期JIT方法是否可以实现额外的加速。

更新:请参阅此问题的Microsoft Connect条目。

共有3个答案

韦俊英
2023-03-14

我的时间是2400和2600在i5-2410M2,3Ghz 4GB内存64位Win 7。

这是我的输出:单人优先

启动进程并附加调试器后

            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
范玄裳
2023-03-14

所以要得到一个明确的答案。。。我怀疑我们需要深入调查这次分拆。

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

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

鲁博赡
2023-03-14

请注意,我不信任“在模块加载时抑制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 

请注意,已将多行测试放置在8字节边界上,将单线测试放置在4字节边界上。

以下是两个版本以相同速度运行时的主版本:

            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位数字中是相同的,尽管据称JIT处理它们的顺序相反。我再也不相信了。

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

这是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 

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

我认为这是确凿的证据。

 类似资料:
  • 问题内容: 有在。根据文件我应该 如果您打算将URL连接用于输出,请将DoOutput标志设置为true,否则将其设置为false。 现在,我正好面对这个问题 -Java运行时将请求转换为一次调用,而服务器仅响应请求。我想了解如果我从代码中删除代码会发生什么。 这究竟会影响什么?假设我将其设置为-现在可以做什么,现在不能做什么?我可以执行请求吗?在此方法的上下文中,“输出”是什么? 问题答案: 如

  • 下面是vb中的一个特殊情况。我搞乱了SuppressKeyPress属性,发现了一些奇怪的东西。 形势 假设我有一个名为的输入框,我希望名称不带任何数字,当插入数字时,会弹出一个MessageBox并报告错误。 在这种情况下,由于某种奇怪的原因,如果我键入一个数字,它将被写入文本框中,尽管我抑制了按键。 我发现,如果我删除MsgBox行,数字将不会出现在输入框中。 问题 这是怎么回事?为什么Msg

  • 问题内容: 问题:Java中的异常处理是否真的很慢? 传统观点以及许多Google的研究结果都表明,不应将异常逻辑用于Java中的常规程序流程。通常有两个原因, 它确实很慢-甚至比常规代码慢一个数量级(给出的原因各不相同), 和 这是混乱的,因为人们期望仅在异常代码中处理错误。 这个问题是关于#1的。 例如,此页面将Java异常处理描述为“非常慢”,并将这种缓慢与异常消息字符串的创建相关联-“然后

  • 自从 OpenResty 1.5.8.1 版本之后,默认捆绑的 Lua 解释器就被替换成了 LuaJIT,而不再是标准 Lua。单从名字上,我们就可以直接看到这个新的解释器多了一个 JIT,接下来我们就一起来聊聊 JIT。 先看一下 LuaJIT 官方的解释:LuaJIT is a Just-In-Time Compilerfor the Lua programming language。 Lua

  • 问题内容: varchar列上的索引是否会使查询运行缓慢?我可以将其设为int。而且我不需要做LIKE%比较。 问题答案: varchar列上的索引是否会使查询运行缓慢? 不,不是的。 如果优化器决定使用索引,则查询将运行得更快。 该表上的s / s / s会变慢,但不太可能引起注意。 我不需要做LIKE%比较 请注意,使用: …将 不 使用索引,但以下内容将: 关键是在字符串的左侧使用通配符,这

  • 根据定义: noNamespaceSchema位置属性引用没有目标命名空间的XML Schema文档。 这个属性将如何改变解析的结果? 例如,以这个XML为例: 参考此模式: 我从架构中删除了以下命名空间声明: 即使在引用XML中不使用noNamespaceSchemaLocation属性,也不会引发错误。为什么我们一开始就需要这个属性?