赶上加速我的代码?
我写了一些代码来测试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;
}
在我的电脑上,这一直打印出大约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;
}
现在它一直打印出0.69 ... - 它实际上运行得更快! 但为什么?
注意:我使用发布配置对其进行编译,并直接运行EXE文件(在Visual Studio之外)。
编辑:Jon Skeet的优秀分析表明,try-catch在某种程度上导致x86 CLR以更有利的方式使用CPU寄存器(并且我认为我们还不明白为什么)。 我证实了Jon的发现,即x64 CLR没有这种差异,并且它比x86 CLR更快。 我还测试了在Fibo方法中使用int
类型而不是long
类型,然后x86 CLR与x64 CLR一样快。
更新:看起来这个问题已经被Roslyn修复。 同一台机器,相同的CLR版本 - 使用VS 2013进行编译时问题仍然存在,但是使用VS 2015编译时问题会消失。
一位专门理解堆栈使用优化的Roslyn工程师看了一眼,并向我报告,C#编译器生成局部变量存储的方式与JIT编译器注册方式之间的交互似乎存在问题在相应的x86代码中进行调度。 结果是在当地人的加载和存储上代码生成不理想。
由于某些原因,我们都不清楚,当JITter知道块处于try-protected区域时,会避免有问题的代码生成路径。
这很奇怪。 我们会跟进JITter团队,看看我们是否可以得到一个错误输入,以便他们可以解决这个问题。
另外,我们正在为Roslyn改进C#和VB编译器的算法,以确定何时可以使本地人成为“短暂”的 - 也就是只是在堆栈上推送和弹出,而不是在栈上分配一个特定的位置,激活的持续时间。 我们相信,JITter将能够更好地完成寄存器分配,并且如果我们能够更好地提供关于何时可以使得当地人“更早死亡”的更好的提示。
感谢您将这引起我们的注意,并为奇怪的行为道歉。
那么,你对时间进行计时的方式对我来说看起来非常讨厌。 整个循环的时间会更加明智:
var stopwatch = Stopwatch.StartNew();
for (int i = 1; i < 100000000; i++)
{
Fibo(100);
}
stopwatch.Stop();
Console.WriteLine("Elapsed time: {0}", stopwatch.Elapsed);
这样你就不会受到微小时间,浮点运算和累积误差的束缚。
做了这些改变后,看看“非catch”版本是否仍然比“catch”版本慢。
编辑:好吧,我自己尝试过 - 我看到了相同的结果。 很奇怪。 我想知道try / catch是否禁用了一些不良内联,但是使用[MethodImpl(MethodImplOptions.NoInlining)]
并没有帮助...
基本上你需要查看cordbg下的优化JIT代码,我怀疑...
编辑:多一点的信息:
n++;
线仍然可以提高性能,但不会像将其放在整个区块中一样 ArgumentException
),它仍然很快 奇怪的...
编辑:好的,我们已经拆卸...
这是使用C#2编译器和.NET 2(32位)CLR,用mdbg进行反汇编(因为我的机器上没有cordbg)。 即使在调试器下,我仍然可以看到相同的性能效果。 快速版本在变量声明和返回语句之间的所有内容上都使用try
块,只需一个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
反汇编缓慢版本的代码:
[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
在每种情况下, *
显示调试器在简单的“步入”中输入的位置。
编辑:好吧,我现在已经查看了代码,我想我可以看到每个版本是如何工作的...我相信较慢的版本会更慢,因为它使用更少的寄存器和更多的堆栈空间。 对于n
的小数值可能更快 - 但是当循环占用大部分时间时,速度会变慢。
可能try / catch块会强制更多的寄存器被保存和恢复,所以JIT也将这些用于循环......这恰好可以提高整体性能。 目前还不清楚JIT是否在“正常”代码中使用了不多的寄存器是合理的决定。
编辑:刚在我的x64机器上试过这个。 x64 CLR比这个代码上的x86 CLR快得多(速度快3-4倍),而在x64下,try / catch块并没有明显的区别。
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;
}
一个是另一个的通用版本。 用StructArray
替换泛型将使方法相同。 因为StructArray
是一个值类型,所以它得到它自己的通用方法的编译版本。 然而实际的运行时间比专门的方法要长很多,但仅限于x86。 对于x64,时序非常相似。 在其他情况下,我也观察到了x64的差异。