难以衡量C / C ++性能

我写了一段C代码,以展示关于优化和分支预测的讨论中的一点。 然后我注意到比我预期的更多样化的结果。 我的目标是用C ++和C之间通用子集的语言编写它,这对于两种语言都是标准兼容的并且相当便携。 它在不同的Windows PC上进行了测试:

#include  #include  /// @return - time difference between start and stop in milliseconds int ms_elapsed( clock_t start, clock_t stop ) { return (int)( 1000.0 * ( stop - start ) / CLOCKS_PER_SEC ); } int const Billion = 1000000000; /// & with numbers up to Billion gives 0, 0, 2, 2 repeating pattern int const Pattern_0_0_2_2 = 0x40000002; /// @return - half of Billion int unpredictableIfs() { int sum = 0; for ( int i = 0; i < Billion; ++i ) { // true, true, false, false ... if ( ( i & Pattern_0_0_2_2 ) == 0 ) { ++sum; } } return sum; } /// @return - half of Billion int noIfs() { int sum = 0; for ( int i = 0; i < Billion; ++i ) { // 1, 1, 0, 0 ... sum += ( i & Pattern_0_0_2_2 ) == 0; } return sum; } int main() { clock_t volatile start; clock_t volatile stop; int volatile sum; printf( "Puzzling measurements:\n" ); start = clock(); sum = unpredictableIfs(); stop = clock(); printf( "Unpredictable ifs took %d msec; answer was %d\n" , ms_elapsed(start, stop), sum ); start = clock(); sum = unpredictableIfs(); stop = clock(); printf( "Unpredictable ifs took %d msec; answer was %d\n" , ms_elapsed(start, stop), sum ); start = clock(); sum = noIfs(); stop = clock(); printf( "Same without ifs took %d msec; answer was %d\n" , ms_elapsed(start, stop), sum ); start = clock(); sum = unpredictableIfs(); stop = clock(); printf( "Unpredictable ifs took %d msec; answer was %d\n" , ms_elapsed(start, stop), sum ); } 

用VS2010编译; / O2优化Intel Core 2,WinXP结果:

 Puzzling measurements: Unpredictable ifs took 1344 msec; answer was 500000000 Unpredictable ifs took 1016 msec; answer was 500000000 Same without ifs took 1031 msec; answer was 500000000 Unpredictable ifs took 4797 msec; answer was 500000000 

编辑:编译器的完整开关:

/ Zi / nologo / W3 / WX- / O2 / Oi / Oy- / GL / D“WIN32”/ D“NDEBUG”/ D“_CONSOLE”/ D“_UNICODE”/ D“UNICODE”/ Gm- / EHsc / GS / Gy / fp:precise / Zc:wchar_t / Zc:forScope /Fp”Release\Trying.pch“/ Fa”Release \“/ Fo”Release \“/ Fd”Release\vc100.pdb”/ Gd / analyze- / errorReport:队列

其他人发布了这样的…用MinGW编译,g ++ 4.71,-O1优化Intel Core 2,WinXP结果:

 Puzzling measurements: Unpredictable ifs took 1656 msec; answer was 500000000 Unpredictable ifs took 0 msec; answer was 500000000 Same without ifs took 1969 msec; answer was 500000000 Unpredictable ifs took 0 msec; answer was 500000000 

他还发布了-O3优化的结果:

 Puzzling measurements: Unpredictable ifs took 1890 msec; answer was 500000000 Unpredictable ifs took 2516 msec; answer was 500000000 Same without ifs took 1422 msec; answer was 500000000 Unpredictable ifs took 2516 msec; answer was 500000000 

现在我有疑问。 这里发生了什么?

更具体地说……固定function如何能够花费如此不同的时间? 我的代码中有什么问题吗? 英特尔处理器有什么棘手的问题吗? 编译器是做什么奇怪的吗? 是不是因为在64位处理器上运行了32位代码?

谢谢你的关注!

编辑:我接受g ++ -O1只是在其他2个调用中重用返回值。 我也接受g ++ -O2和g ++ -O3有缺陷导致优化。 测量速度的显着多样性(450%!!!)似乎仍然是神秘的。

我查看了VS2010生成的代码的反汇编。 它确实内联unpredictableIfs 3次。 内联代码非常相似; 循环是一样的。 它没有内联noIfs 。 它确实推出了noIfs 。 一次迭代需要4个步骤。 noIfs计算就像写了一样,而unpredictableIfs noIfs使用jne来跳过增量。

使用-O1 ,gcc-4.7.1只调用一次unpredictableIfs Ifs并重新使用结果,因为它识别它是一个纯函数,因此每次调用时结果都是相同的。 (我做了,通过查看生成的组件进行validation。)

具有更高的优化级别,函数被内联,并且编译器不再识别它是相同的代码,因此每次函数调用出现在源中时它都会运行。

除此之外,当使用-O1-O2时,我的gcc-4.7.1最好处理unpredictableIfs -O2 (除了重用问题,两者都产生相同的代码),而使用-O3处理noIfs 好得多。 然而,相同代码的不同运行之间的时间在这里是相同的 – 相差或相差10毫秒( clock粒度),所以我不知道什么可能导致您为-O3报告的unpredictableIfs-O3的实质上不同的时间。

对于-O2unpredictableIfs -O2的循环与使用-O1生成的代码相同(寄存器交换除外):

 .L12: movl %eax, %ecx andl $1073741826, %ecx cmpl $1, %ecx adcl $0, %edx addl $1, %eax cmpl $1000000000, %eax jne .L12 

而对于noIfs它是相似的:

 .L15: xorl %ecx, %ecx testl $1073741826, %eax sete %cl addl $1, %eax addl %ecx, %edx cmpl $1000000000, %eax jne .L15 

它在哪里

 .L7: testl $1073741826, %edx sete %cl movzbl %cl, %ecx addl %ecx, %eax addl $1, %edx cmpl $1000000000, %edx jne .L7 

-O1 。 两个循环都在相似的时间内运行, unpredictableIfs的Ifs更快一点。

使用-O3unpredictableIfs -O3的循环变得更糟,

 .L14: leal 1(%rdx), %ecx testl $1073741826, %eax cmove %ecx, %edx addl $1, %eax cmpl $1000000000, %eax jne .L14 

对于noIfs (包括此处的设置代码),它变得更好:

  pxor %xmm2, %xmm2 movq %rax, 32(%rsp) movdqa .LC3(%rip), %xmm6 xorl %eax, %eax movdqa .LC2(%rip), %xmm1 movdqa %xmm2, %xmm3 movdqa .LC4(%rip), %xmm5 movdqa .LC5(%rip), %xmm4 .p2align 4,,10 .p2align 3 .L18: movdqa %xmm1, %xmm0 addl $1, %eax paddd %xmm6, %xmm1 cmpl $250000000, %eax pand %xmm5, %xmm0 pcmpeqd %xmm3, %xmm0 pand %xmm4, %xmm0 paddd %xmm0, %xmm2 jne .L18 .LC2: .long 0 .long 1 .long 2 .long 3 .align 16 .LC3: .long 4 .long 4 .long 4 .long 4 .align 16 .LC4: .long 1073741826 .long 1073741826 .long 1073741826 .long 1073741826 .align 16 .LC5: .long 1 .long 1 .long 1 .long 1 

它一次计算四次迭代,因此, noIfs运行速度几乎是其四倍。

是的,看看64位Linux上gcc的汇编代码,第一种情况,使用-O1,函数UnpredictableIfs确实只调用一次,结果重用。

使用-O2和-O3时,函数内联,并且所需的时间应相同。 在任何一位代码中都没有实际的分支,但是两位代码的转换有些不同,我已经删除了更新“sum”的行[在两种情况下都在%edx ]

UnpredictableIfs:

 movl %eax, %ecx andl $1073741826, %ecx cmpl $1, %ecx adcl $0, %edx addl $1, %eax 

NoIfs:

 xorl %ecx, %ecx testl $1073741826, %eax sete %cl addl $1, %eax addl %ecx, %edx 

正如你所看到的,它并不完全相同,但它做的非常相似。

关于Windows上的结果范围(从1016毫秒到4797毫秒):您应该知道MSVC中的clock()返回经过的挂起时间 。 该标准表示clock()应返回该进程所花费的CPU时间的近似值,而其他实现则可以更好地完成此任务。

鉴于MSVC正在提供挂起时间,如果您的进程在运行一次迭代测试时被抢占,则可能会产生更大的结果,即使代码运行的CPU时间大致相同。

另请注意,许多Windows PC上的clock()具有非常糟糕的分辨率,通常为11-19 ms。 你已经做了足够的迭代,只有1%左右,所以我不认为这是差异的一部分,但是在尝试编写基准时要注意这一点很好。 我知道你的可移植性,但如果你需要在Windows上更好的测量,你可以使用QueryPerformanceCounter几乎肯定会给你更好的分辨率,尽管它仍然只是过去的时间。

更新:在我了解到一个案例的长运行时间一直在发生之后,我启动了VS2010并重现了结果。 对于某些运行,我通常得到大约1000毫秒的东西,对于其他运行,我通常得到750毫秒,对于莫名其妙的运行,我得到5000毫秒。

观察:

  1. 在所有情况下都内联了不可预测的Ifs()代码。
  2. 删除noIfs()代码没有任何影响(因此长时间不是该代码的副作用)。
  3. 将线程关联性设置为单个处理器无效。
  4. 5000毫秒时间总是后来的情况。 我注意到后面的例子在循环开始之前有一条额外的指令: lea ecx,[ecx] 。 我不明白为什么这应该产生5倍的差异。 除此之外,早期和后期的实例都是相同的代码。
  5. startstop变量中删除volatile产生更少的长时间运行,更多的750 ms运行,并且没有1000 ms运行。 (生成的循环代码现在在所有情况下都看起来完全相同,而不是lea s。)
  6. sum变量中移除volatile (但保留时钟定时器),长时间运行可以在任何位置发生。
  7. 如果删除所有volatile限定符,则会获得一致,快速(750 ms)的运行。 (代码看起来与之前的代码相同,但edi被选为sum而不是ecx 。)

我不确定从这一切得出什么结论,除了volatile有MSVC的不可预测的性能后果,所以你应该只在必要时应用它。

更新2:我看到一致的运行时差异与volatile的使用有关,即使反汇编几乎相同。

随着波动:

 Puzzling measurements: Unpredictable ifs took 643 msec; answer was 500000000 Unpredictable ifs took 1248 msec; answer was 500000000 Unpredictable ifs took 605 msec; answer was 500000000 Unpredictable ifs took 4611 msec; answer was 500000000 Unpredictable ifs took 4706 msec; answer was 500000000 Unpredictable ifs took 4516 msec; answer was 500000000 Unpredictable ifs took 4382 msec; answer was 500000000 

每个实例的反汇编如下所示:

  start = clock(); 010D1015 mov esi,dword ptr [__imp__clock (10D20A0h)] 010D101B add esp,4 010D101E call esi 010D1020 mov dword ptr [start],eax sum = unpredictableIfs(); 010D1023 xor ecx,ecx 010D1025 xor eax,eax 010D1027 test eax,40000002h 010D102C jne main+2Fh (10D102Fh) 010D102E inc ecx 010D102F inc eax 010D1030 cmp eax,3B9ACA00h 010D1035 jl main+27h (10D1027h) 010D1037 mov dword ptr [sum],ecx stop = clock(); 010D103A call esi 010D103C mov dword ptr [stop],eax 

没有不稳定:

 Puzzling measurements: Unpredictable ifs took 644 msec; answer was 500000000 Unpredictable ifs took 624 msec; answer was 500000000 Unpredictable ifs took 624 msec; answer was 500000000 Unpredictable ifs took 605 msec; answer was 500000000 Unpredictable ifs took 599 msec; answer was 500000000 Unpredictable ifs took 599 msec; answer was 500000000 Unpredictable ifs took 599 msec; answer was 500000000 start = clock(); 00321014 mov esi,dword ptr [__imp__clock (3220A0h)] 0032101A add esp,4 0032101D call esi 0032101F mov dword ptr [start],eax sum = unpredictableIfs(); 00321022 xor ebx,ebx 00321024 xor eax,eax 00321026 test eax,40000002h 0032102B jne main+2Eh (32102Eh) 0032102D inc ebx 0032102E inc eax 0032102F cmp eax,3B9ACA00h 00321034 jl main+26h (321026h) stop = clock(); 00321036 call esi // The only optimization I see is here, where eax isn't explicitly stored // in stop but is instead immediately used to compute the value for the // printf that follows. 

除了寄存器选择,我没有看到显着的差异。