我正在编写一些基准测试来弄清楚为什么类似的纯算法(在类中没有C ++ lib / .net)在C ++中比在C#中运行得快得多,即使在考虑预期的特征差异时也是如此。虽然这样做我偶然发现了令我感到困惑的这两个测试,但是有没有人知道为什么一个人比另一个慢得多?第二个(在我的机器上需要51毫秒vs 88)的唯一区别是2个数组在方法中而不是在外部声明。在这两种情况下,数组都是在我们开始计时之前创建的。
const int Runs = 100;
const int Width = 5000;
const int Height = 5000;
const int Size = Width * Height;
static int[] Input = Enumerable.Range(0, Size).ToArray();
static int[] Output = new int[Size * 2];
static int SimpleTest()
{
// Removing those 2 lines and using the static arrays instead give substantially slower performance, nearly half the speed!
int[] Input = Enumerable.Range(0, Size).ToArray();
int[] Output = new int[Size * 2];
Stopwatch sw = new Stopwatch();
sw.Start();
for (int run = 0; run < Runs; run++)
{
int InputIndex = 0;
for (int x = 0; x < Width; x++)
{
for (int y = 0; y < Height; y++)
{
int pixel = Input[InputIndex];
var OutputIndex = InputIndex * 2;
Output[OutputIndex] = pixel;
Output[OutputIndex + 1] = pixel;
InputIndex++;
}
}
}
sw.Stop();
return (int)(sw.ElapsedMilliseconds / Runs);
}
答案 0 :(得分:15)
当变量是本地变量时,编译器知道Input
和Output
永远不会改变,这会打开很多优化。
Input
和Output
变量的值可以保存在寄存器中。Input.Length
和Output.Length
可以计算一次并缓存。Input[InputIndex]
和Output[OutputIndex]
永远不会导致数组索引超出范围,因此可以优化边界检查。Input
和Output
的结果从未使用过,所以它可以优化循环到没有!如果使用静态版本,则编译器无法执行这些优化。编译器必须在每次访问时重新加载Input
和Output
,并且必须在每个数组索引操作处执行边界检查,以防另一个线程被修改为Input
或Output
。 / p>
例如,如果另一个线程Input = new int[Size]
,则所有将来的计算都必须使用此备用Input
。如果另一个线程Output = new int[1]
,则代码必须引发IndexOutOfRangeException
。
答案 1 :(得分:5)
使用32位JIT,我相信罪魁祸首就像Raymond Chen所提到的那样,当输入和输出是本地时,输入和输出可以保存在寄存器中,但是每次输入和输出都不需要重新加载。生成的程序集:
对于当地人:
<ul>
<li ng-repeat="d in data | dayFilter:day">{{d}}</li>
</ul>
静力学:
007426F0 mov eax,dword ptr [ebp-18h]
007426F3 mov edi,dword ptr [eax+4]
int pixel = Input[InputIndex];
007426F6 mov eax,dword ptr [ebp-18h]
007426F9 cmp edx,edi
007426FB jae 0074276E
007426FD mov ecx,dword ptr [eax+edx*4+8]
如您所见,011C2718 mov dword ptr [ebp-18h],edx
011C271B mov esi,dword ptr ds:[3BB7E90h]
011C2721 mov eax,dword ptr [esi+4]
011C2724 mov dword ptr [ebp-1Ch],eax
int pixel = Input[InputIndex];
011C2727 mov eax,dword ptr [ebp-1Ch]
011C272A cmp ecx,eax
011C272C jae 011C27A2
011C272E mov edi,dword ptr [esi+ecx*4+8]
访问数据段。
正如您所看到的,在两种情况下都会发生边界检查(mov esi,dword ptr ds:[3BB7E90h]
),这样就无关紧要了,并且实际上没有将循环优化为任何内容。
64位JIT如何避免这个问题超出了我的范围。
以下是两种情况的完整反汇编:
快速版:
cmp-jae
慢版:
for (int x = 0; x < Width; x++) {
007426EB mov dword ptr [ebp-14h],edx
for (int y = 0; y < Height; y++) {
007426EE xor ebx,ebx
007426F0 mov eax,dword ptr [ebp-18h]
007426F3 mov edi,dword ptr [eax+4]
int pixel = Input[InputIndex];
007426F6 mov eax,dword ptr [ebp-18h]
007426F9 cmp edx,edi
007426FB jae 0074276E
007426FD mov ecx,dword ptr [eax+edx*4+8]
var OutputIndex = InputIndex * 2;
00742701 mov esi,edx
00742703 add esi,esi
Output[OutputIndex] = pixel;
00742705 mov eax,dword ptr [ebp-1Ch]
00742708 cmp esi,dword ptr [eax+4]
0074270B jae 0074276E
0074270D mov dword ptr [eax+esi*4+8],ecx
Output[OutputIndex + 1] = pixel;
00742711 inc esi
00742712 mov eax,dword ptr [ebp-1Ch]
00742715 cmp esi,dword ptr [eax+4]
00742718 jae 0074276E
0074271A mov dword ptr [eax+esi*4+8],ecx
InputIndex++;
0074271E inc edx
for (int y = 0; y < Height; y++) {
0074271F inc ebx
for (int y = 0; y < Height; y++) {
00742720 cmp ebx,1388h
00742726 jl 007426F6
for (int x = 0; x < Width; x++) {
00742728 inc dword ptr [ebp-14h]
0074272B cmp dword ptr [ebp-14h],1388h
00742732 jl 007426EE
答案 2 :(得分:2)
我想知道这是否与performance difference between static and member functions类似?静态方法调用不是空检查,而实例函数调用是空检查。
另外,我看到的结果与你不同。静态数组在我的机器上运行时间更短。每次运行64.x ms,大约75.x ms。
这是我使用的完整程序。在OSX上运行Mono C#。
{{1}}