Pum*_*ces 10 c# performance jit compilation roslyn
我的一位同事一直在阅读罗伯特·C·马丁的"清洁代码",并参加了关于使用许多小功能而不是减少大功能的部分.这引发了关于这种方法的性能后果的争论.因此,我们编写了一个快速程序来测试性能,并对结果感到困惑.
对于初学者来说,这是该功能的正常版本.
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = a + b + 1;
}
}
return a;
}
Run Code Online (Sandbox Code Playgroud)
这是我制作的版本将功能分解为小功能.
static double TinyFunctions()
{
double a = 0;
for (int i = 0; i < s_OuterLoopCount; i++)
{
a = Loop(a);
}
return a;
}
static double Loop(double a)
{
for (int i = 0; i < s_InnerLoopCount; i++)
{
double b = Double(i);
a = Add(a, Add(b, 1));
}
return a;
}
static double Double(double a)
{
return a * 2;
}
static double Add(double a, double b)
{
return a + b;
}
Run Code Online (Sandbox Code Playgroud)
我使用秒表类来计算函数的时间,当我在调试中运行它时,得到了以下结果.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 377 ms;
TinyFunctions Time = 1322 ms;
Run Code Online (Sandbox Code Playgroud)
这些结果对我来说很有意义,尤其是在调试中,因为函数调用会产生额外的开销.当我在发布中运行它时,我得到以下结果.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 173 ms;
TinyFunctions Time = 98 ms;
Run Code Online (Sandbox Code Playgroud)
这些结果让我感到困惑,即使编译器通过内置所有函数调用来优化TinyFunction,怎么能让它快〜57%?
我们尝试在NormalFunctions中移动变量声明,它基本上对运行时没有影响.
我希望有人知道发生了什么,如果编译器可以很好地优化TinyFunction,为什么它不能对NormalFunction应用类似的优化.
在环顾四周时,我们发现有人提到将函数分解出去可以让JIT更好地优化寄存器中的内容,但是NormalFunctions只有4个变量,所以我觉得很难相信这解释了巨大的性能差异.
如果有人能提供任何见解,我将不胜感激.
更新1 如下所述,Kyle更改操作顺序会对NormalFunction的性能产生巨大影响.
static double NormalFunction()
{
double a = 0;
for (int j = 0; j < s_OuterLoopCount; ++j)
{
for (int i = 0; i < s_InnerLoopCount; ++i)
{
double b = i * 2;
a = b + 1 + a;
}
}
return a;
}
Run Code Online (Sandbox Code Playgroud)
以下是此配置的结果.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 91 ms;
TinyFunctions Time = 102 ms;
Run Code Online (Sandbox Code Playgroud)
这更符合我的预期,但仍然留下了为什么操作顺序可以达到约56%的性能影响的问题.
此外,我然后尝试使用整数运算,我们又回到没有任何意义.
s_OuterLoopCount = 10000;
s_InnerLoopCount = 10000;
NormalFunction Time = 87 ms;
TinyFunctions Time = 52 ms;
Run Code Online (Sandbox Code Playgroud)
无论操作顺序如何,这都不会改变.
通过更改一行代码,我可以使性能匹配更好:
a = a + b + 1;
Run Code Online (Sandbox Code Playgroud)
将其更改为:
a = b + 1 + a;
Run Code Online (Sandbox Code Playgroud)
要么:
a += b + 1;
Run Code Online (Sandbox Code Playgroud)
现在你会发现它NormalFunction实际上可能会稍快一些,你可以通过将Double方法的签名更改为"修复"它:
int Double( int a ) { return a * 2; }
Run Code Online (Sandbox Code Playgroud)
我想到了这些变化,因为这是两个实现之间的不同之处.在此之后,它们的性能非常相似,TinyFunctions只有几个百分点(如预期的那样).
第二个更改很容易解释:NormalFunction实现实际上会加倍int,然后将其转换为double(fild在机器代码级别使用操作码).原始Double方法加载double第一个,然后加倍,我希望稍微慢一点.
但这并不能解释运行时差异的大部分.这几乎完全取决于我先做的订单变更.为什么?我真的不知道.机器代码的区别如下:
Original Changed
01070620 push ebp 01390620 push ebp
01070621 mov ebp,esp 01390621 mov ebp,esp
01070623 push edi 01390623 push edi
01070624 push esi 01390624 push esi
01070625 push eax 01390625 push eax
01070626 fldz 01390626 fldz
01070628 xor esi,esi 01390628 xor esi,esi
0107062A mov edi,dword ptr ds:[0FE43ACh] 0139062A mov edi,dword ptr ds:[12243ACh]
01070630 test edi,edi 01390630 test edi,edi
01070632 jle 0107065A 01390632 jle 0139065A
01070634 xor edx,edx 01390634 xor edx,edx
01070636 mov ecx,dword ptr ds:[0FE43B0h] 01390636 mov ecx,dword ptr ds:[12243B0h]
0107063C test ecx,ecx 0139063C test ecx,ecx
0107063E jle 01070655 0139063E jle 01390655
01070640 mov eax,edx 01390640 mov eax,edx
01070642 add eax,eax 01390642 add eax,eax
01070644 mov dword ptr [ebp-0Ch],eax 01390644 mov dword ptr [ebp-0Ch],eax
01070647 fild dword ptr [ebp-0Ch] 01390647 fild dword ptr [ebp-0Ch]
0107064A faddp st(1),st 0139064A fld1
0107064C fld1 0139064C faddp st(1),st
0107064E faddp st(1),st 0139064E faddp st(1),st
01070650 inc edx 01390650 inc edx
01070651 cmp edx,ecx 01390651 cmp edx,ecx
01070653 jl 01070640 01390653 jl 01390640
01070655 inc esi 01390655 inc esi
01070656 cmp esi,edi 01390656 cmp esi,edi
01070658 jl 01070634 01390658 jl 01390634
0107065A pop ecx 0139065A pop ecx
0107065B pop esi 0139065B pop esi
0107065C pop edi 0139065C pop edi
0107065D pop ebp 0139065D pop ebp
0107065E ret 0139065E ret
Run Code Online (Sandbox Code Playgroud)
除了浮点运算的顺序之外,opcode-for-opcode是相同的.这产生了巨大的性能差异,但我对x86浮点运算知之甚少,无法确切知道原因.
使用新的整数版本,我们看到其他好奇的东西.在这种情况下,似乎JIT试图变得聪明并应用优化,因为它转为:
int b = 2 * i;
a = a + b + 1;
Run Code Online (Sandbox Code Playgroud)
变成这样的东西:
mov esi, eax ; b = i
add esi, esi ; b += b
lea ecx, [ecx + esi + 1] ; a = a + b + 1
Run Code Online (Sandbox Code Playgroud)
当a存储在ecx寄存器i中eax,并且b在esi.
而TinyFunctions版本变成了类似的东西:
mov eax, edx
add eax, eax
inc eax
add ecx, eax
Run Code Online (Sandbox Code Playgroud)
凡i在edx,b在eax,而a在ecx这一次.
我认为对于我们的CPU架构,这个LEA"技巧"(这里解释)最终比仅使用ALU慢.仍然可以更改代码以获得两者之间的性能排列:
int b = 2 * i + 1;
a += b;
Run Code Online (Sandbox Code Playgroud)
这最终迫使NormalFunction方法最终变得mov, add, inc, add像在TinyFunctions方法中出现一样.