Ere*_*mez 1463 .net c# clr try-catch performance-testing
我写了一些代码来测试try-catch的影响,但看到了一些令人惊讶的结果.
static void Main(string[] args)
{
Thread.CurrentThread.Priority = ThreadPriority.Highest;
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.RealTime;
long start = 0, stop = 0, elapsed = 0;
double avg = 0.0;
long temp = Fibo(1);
for (int i = 1; i < 100000000; i++)
{
start = Stopwatch.GetTimestamp();
temp = Fibo(100);
stop = Stopwatch.GetTimestamp();
elapsed = stop - start;
avg = avg + ((double)elapsed - avg) / i;
}
Console.WriteLine("Elapsed: " + avg);
Console.ReadKey();
}
static long Fibo(int n)
{
long n1 = 0, n2 = 1, fibo = 0;
n++;
for (int i = 1; i < n; i++)
{
n1 = n2;
n2 = fibo;
fibo = n1 + n2;
}
return fibo;
}
Run Code Online (Sandbox Code Playgroud)
在我的电脑上,这始终打印出一个大约0.96的值.
当我使用try-catch块在Fibo()中包装for循环时,如下所示:
static long Fibo(int n)
{
long n1 = 0, n2 = 1, fibo = 0;
n++;
try
{
for (int i = 1; i < n; i++)
{
n1 = n2;
n2 = fibo;
fibo = n1 + n2;
}
}
catch {}
return fibo;
}
Run Code Online (Sandbox Code Playgroud)
现在它一直打印出0.69 ...... - 它实际上运行得更快!但为什么?
注意:我使用Release配置编译它并直接运行EXE文件(在Visual Studio外部).
编辑:Jon Skeet的优秀分析表明try-catch在某种程度上导致x86 CLR在这种特定情况下以更有利的方式使用CPU寄存器(我认为我们还没理解为什么).我确认Jon发现x64 CLR没有这个区别,并且它比x86 CLR更快.我还测试了使用int
Fibo方法中的long
类型而不是类型,然后x86 CLR和x64 CLR一样快.
更新:看起来这个问题已由Roslyn修复.相同的机器,相同的CLR版本 - 在使用VS 2013编译时问题仍然如上所述,但在使用VS 2015编译时问题就消失了.
Eri*_*ert 1023
专门了解堆栈使用优化的Roslyn工程师之一看了一下这个并告诉我,C#编译器生成局部变量存储的方式与JIT编译器注册方式之间的交互似乎存在问题在相应的x86代码中进行调度.结果是在本地的加载和存储上生成次优代码.
由于某些原因我们所有人都不清楚,当JITter知道该块在try-protected区域时,可以避免有问题的代码生成路径.
这很奇怪.我们将跟进JITter团队,看看我们是否可以输入错误,以便他们可以解决这个问题.
此外,我们正在努力改进Roslyn到C#和VB编译器的算法,以确定何时可以使本地变为"短暂" - 也就是说,只是在堆栈上推送和弹出,而不是在堆栈上分配特定位置激活的持续时间.我们相信JITter能够更好地完成寄存器分配,如果我们给出更好的提示,可以让当地人更早地"死".
感谢您引起我们的注意,并为奇怪的行为道歉.
Jon*_*eet 727
好吧,你对事情进行计时的方式对我来说非常讨厌.对整个循环进行计时会更加明智:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
Run Code Online (Sandbox Code Playgroud)
这样你就不会受到微小时序,浮点运算和累积误差的影响.
进行了更改后,查看"非捕获"版本是否仍然比"catch"版本慢.
编辑:好的,我自己尝试过 - 我看到的结果相同.很奇怪.我想知道try/catch是否禁用了一些不良的内联,但使用[MethodImpl(MethodImplOptions.NoInlining)]
反而没有帮助...
基本上你需要在cordbg下查看优化的JITted代码,我怀疑......
编辑:一些信息:
n++;
线条周围仍然可以提高性能,但不会将其放在整个块周围ArgumentException
在我的测试中),它仍然很快奇怪的...
编辑:好的,我们有拆卸......
这是使用C#2编译器和.NET 2(32位)CLR,使用mdbg进行反汇编(因为我的机器上没有cordbg).即使在调试器下,我仍然会看到相同的性能影响.快速版本try
在变量声明和return语句之间使用一个块,只使用一个catch{}
处理程序.显然慢速版本是相同的,除了没有try/catch.调用代码(即Main)在两种情况下都是相同的,并且具有相同的程序集表示(因此它不是内联问题).
快速版本的反汇编代码:
[0000] push ebp
[0001] mov ebp,esp
[0003] push edi
[0004] push esi
[0005] push ebx
[0006] sub esp,1Ch
[0009] xor eax,eax
[000b] mov dword ptr [ebp-20h],eax
[000e] mov dword ptr [ebp-1Ch],eax
[0011] mov dword ptr [ebp-18h],eax
[0014] mov dword ptr [ebp-14h],eax
[0017] xor eax,eax
[0019] mov dword ptr [ebp-18h],eax
*[001c] mov esi,1
[0021] xor edi,edi
[0023] mov dword ptr [ebp-28h],1
[002a] mov dword ptr [ebp-24h],0
[0031] inc ecx
[0032] mov ebx,2
[0037] cmp ecx,2
[003a] jle 00000024
[003c] mov eax,esi
[003e] mov edx,edi
[0040] mov esi,dword ptr [ebp-28h]
[0043] mov edi,dword ptr [ebp-24h]
[0046] add eax,dword ptr [ebp-28h]
[0049] adc edx,dword ptr [ebp-24h]
[004c] mov dword ptr [ebp-28h],eax
[004f] mov dword ptr [ebp-24h],edx
[0052] inc ebx
[0053] cmp ebx,ecx
[0055] jl FFFFFFE7
[0057] jmp 00000007
[0059] call 64571ACB
[005e] mov eax,dword ptr [ebp-28h]
[0061] mov edx,dword ptr [ebp-24h]
[0064] lea esp,[ebp-0Ch]
[0067] pop ebx
[0068] pop esi
[0069] pop edi
[006a] pop ebp
[006b] ret
Run Code Online (Sandbox Code Playgroud)
用于慢速版本的反汇编代码:
[0000] push ebp
[0001] mov ebp,esp
[0003] push esi
[0004] sub esp,18h
*[0007] mov dword ptr [ebp-14h],1
[000e] mov dword ptr [ebp-10h],0
[0015] mov dword ptr [ebp-1Ch],1
[001c] mov dword ptr [ebp-18h],0
[0023] inc ecx
[0024] mov esi,2
[0029] cmp ecx,2
[002c] jle 00000031
[002e] mov eax,dword ptr [ebp-14h]
[0031] mov edx,dword ptr [ebp-10h]
[0034] mov dword ptr [ebp-0Ch],eax
[0037] mov dword ptr [ebp-8],edx
[003a] mov eax,dword ptr [ebp-1Ch]
[003d] mov edx,dword ptr [ebp-18h]
[0040] mov dword ptr [ebp-14h],eax
[0043] mov dword ptr [ebp-10h],edx
[0046] mov eax,dword ptr [ebp-0Ch]
[0049] mov edx,dword ptr [ebp-8]
[004c] add eax,dword ptr [ebp-1Ch]
[004f] adc edx,dword ptr [ebp-18h]
[0052] mov dword ptr [ebp-1Ch],eax
[0055] mov dword ptr [ebp-18h],edx
[0058] inc esi
[0059] cmp esi,ecx
[005b] jl FFFFFFD3
[005d] mov eax,dword ptr [ebp-1Ch]
[0060] mov edx,dword ptr [ebp-18h]
[0063] lea esp,[ebp-4]
[0066] pop esi
[0067] pop ebp
[0068] ret
Run Code Online (Sandbox Code Playgroud)
在每种情况下,*
调试器在简单的"步入"中输入的显示.
编辑:好的,我现在已经查看了代码,我想我可以看到每个版本的工作方式......我相信较慢的版本较慢,因为它使用较少的寄存器和更多的堆栈空间.对于n
可能更快的小值- 但是当循环占用大部分时间时,它会变慢.
try/catch块可能会强制保存和恢复更多的寄存器,因此JIT也会将这些寄存器用于循环......这样可以提高整体性能.目前尚不清楚JIT是否合理地决定不在 "普通"代码中使用尽可能多的寄存器.
编辑:刚试了我的x64机器.在64位CLR是多快(约3-4倍的速度)比该代码在x86 CLR,并在x64的try/catch块不会使一个显着的差异.
Jef*_*Sax 114
Jon的反汇编显示,两个版本之间的区别在于快速版本使用一对寄存器(esi,edi
)来存储慢速版本不存在的局部变量之一.
JIT编译器对包含try-catch块的代码与不代码的代码的寄存器使用做出了不同的假设.这导致它做出不同的寄存器分配选择.在这种情况下,这有利于try-catch块的代码.不同的代码可能导致相反的效果,所以我不认为这是一种通用的加速技术.
最后,很难说哪些代码最终会以最快的速度运行.寄存器分配和影响它的因素之类的是低级实现细节,我不知道任何特定技术如何能够可靠地生成更快的代码.
例如,请考虑以下两种方法.它们改编自现实生活中的例子:
interface IIndexed { int this[int index] { get; set; } }
struct StructArray : IIndexed {
public int[] Array;
public int this[int index] {
get { return Array[index]; }
set { Array[index] = value; }
}
}
static int Generic<T>(int length, T a, T b) where T : IIndexed {
int sum = 0;
for (int i = 0; i < length; i++)
sum += a[i] * b[i];
return sum;
}
static int Specialized(int length, StructArray a, StructArray b) {
int sum = 0;
for (int i = 0; i < length; i++)
sum += a[i] * b[i];
return sum;
}
Run Code Online (Sandbox Code Playgroud)
一个是另一个的通用版本.替换泛型类型StructArray
会使方法相同.因为它StructArray
是一个值类型,所以它获得了自己的泛型方法的编译版本.然而实际运行时间明显长于专用方法,但仅适用于x86.对于x64,时间几乎完全相同.在其他情况下,我也观察到了x64的差异.
Han*_*ant 70
这看起来像一个内联变坏的情况.在x86内核上,抖动具有ebx,edx,esi和edi寄存器,可用于本地变量的通用存储.ecx寄存器在静态方法中可用,它不必存储它.计算通常需要eax寄存器.但这些是32位寄存器,对于long类型的变量,它必须使用一对寄存器.其中edx:用于计算的eax和用于存储的edi:ebx.
在慢速版本的反汇编中,这是最突出的,既不使用edi也不使用ebx.
当抖动找不到足够的寄存器来存储局部变量时,它必须生成代码以从堆栈帧加载和存储它们.这会降低代码速度,它会阻止名为"寄存器重命名"的处理器优化,这是一种内部处理器核心优化技巧,它使用寄存器的多个副本并允许超标量执行.这允许多个指令同时运行,即使它们使用相同的寄存器.没有足够的寄存器是x86内核的常见问题,在x64中解决,它有8个额外的寄存器(r9到r15).
抖动将尽力应用另一代码生成优化,它将尝试内联您的Fibo()方法.换句话说,不要调用方法,而是在Main()方法中生成内联方法的代码.非常重要的优化,例如,免费提供C#类的属性,给它们一个字段的性能.它避免了调用方法和设置堆栈帧的开销,节省了几纳秒.
有几个规则可以确定何时可以内联方法.它们没有完全记录,但已在博客文章中提及过.一个规则是当方法体太大时不会发生.这会破坏内联的收益,它会生成太多不适合L1指令缓存的代码.这里适用的另一个硬性规则是,当包含try/catch语句时,不会内联方法.这一背后的背景是异常的实现细节,它们捎带到Windows的内置支持SEH(结构异常处理),它是基于堆栈帧的.
抖动中的寄存器分配算法的一种行为可以通过使用该代码来推断.它似乎知道抖动何时试图内联一个方法.似乎使用的一条规则是只有edx:eax寄存器对可以用于具有long类型的局部变量的内联代码.但不是edi:ebx.毫无疑问,因为这对调用方法的代码生成过于不利,edi和ebx都是重要的存储寄存器.
所以你得到了快速版本,因为抖动预先知道方法体包含try/catch语句.它知道它永远不会被内联,所以很容易使用edi:ebx来存储long变量.你得到了慢版本,因为抖动事先并不知道内联不起作用.它只在生成方法体的代码后才发现.
然后,缺陷是它没有返回并重新生成该方法的代码.考虑到它必须运行的时间限制,这是可以理解的.
在x64上不会发生这种减速,因为对于一个它有8个寄存器.另一个是因为它可以在一个寄存器(如rax)中存储一个long.当使用int而不是long时,不会发生减速,因为抖动在选择寄存器时具有更大的灵活性.
mil*_*lla 20
我已经把它作为一个评论,因为我真的不确定这可能是这种情况,但我记得它不是一个尝试/除了声明涉及修改垃圾处理机制的方式编译器工作,因为它以递归方式从堆栈中清除对象内存分配.在这种情况下可能没有要清除的对象,或者for循环可能构成垃圾收集机制识别出足以强制执行不同收集方法的闭包.可能不是,但我认为值得一提,因为我没有看到它在其他地方讨论过.
归档时间: |
|
查看次数: |
102507 次 |
最近记录: |