衡量C/C++性能的困难
Difficulties to measure C/C++ performance
我写了一段C代码来展示关于优化和分支预测的讨论中的一点。然后我注意到了比我预想的更加多样化的结果。我的目标是用C++和C之间的通用子集语言编写它,它符合这两种语言的标准,并且相当可移植。它在不同的Windows PC上进行了测试:
#include <stdio.h>
#include <time.h>
/// @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 %dn"
, ms_elapsed(start, stop), sum );
start = clock();
sum = unpredictableIfs();
stop = clock();
printf( "Unpredictable ifs took %d msec; answer was %dn"
, ms_elapsed(start, stop), sum );
start = clock();
sum = noIfs();
stop = clock();
printf( "Same without ifs took %d msec; answer was %dn"
, ms_elapsed(start, stop), sum );
start = clock();
sum = unpredictableIfs();
stop = clock();
printf( "Unpredictable ifs took %d msec; answer was %dn"
, ms_elapsed(start, stop), sum );
}
用VS2010编译/O2优化英特尔酷睿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
其他人发布了这样的。。。使用MinGW、g++4.71、-O1优化编译英特尔酷睿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
现在我有问题了。这是怎么回事?
更具体地说。。。一个固定的函数怎么会花费如此不同的时间?我的代码有问题吗?英特尔处理器有什么问题吗?编译器是否在做一些奇怪的事情?这可能是因为32位代码在64位处理器上运行吗?
感谢关注!
编辑:我承认g++-O1只是在另外两个调用中重用返回的值。我也承认g++-O2和g++-O3存在遗漏优化的缺陷。测量速度的显著差异(450%!!)似乎仍然是个谜。
我查看了VS2010生成的代码的反汇编。它内联unpredictableIfs
3次。内联代码非常相似;循环是一样的。它没有内联noIfs
。它确实推出了noIfs
。它在一次迭代中需要4个步骤。noIfs
类似计算,而unpredictableIfs
使用jne
跳过增量。
对于-O1
,gcc-4.7.1只调用unpredictableIfs
一次并重显结果,因为它识别出它是一个纯函数,所以每次调用结果都是一样的。(我的确实如此,通过查看生成的组件进行了验证。)
在更高的优化级别下,函数是内联的,编译器不再识别它是同一个代码,所以每次函数调用出现在源中时都会运行它。
除此之外,当使用-O1
或-O2
时,我的gcc-4.7.1最好地处理unpredictableIfs
(除了重用问题外,两者都产生相同的代码),而noIfs
在使用-O3
时处理得更好。然而,同一代码的不同运行之间的时间在这里是一致的——相等或相差10毫秒(clock
的粒度),所以我不知道是什么原因导致您为-O3
报告的unpredictableIfs
的时间显著不同。
对于-O2
,unpredictableIfs
的循环与-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
用CCD_ 21。两个循环的运行时间相似,unpredictableIfs
稍微快一点。
对于-O3
,unpredictableIfs
的循环变得更差,
.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
中,在这两种情况下]
不可预测Ifs:
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 ms到4797 ms):您应该知道MSVC中的clock()
返回经过的墙时间。该标准规定clock()
应该返回进程所花费的CPU时间的近似值,而其他实现在这方面做得更好。
考虑到MSVC给墙时间,如果您的进程在运行测试的一次迭代时被抢占,即使代码在大约相同的CPU时间内运行,它也可能给出更大的结果。
还要注意的是,许多Windows PC上的clock()
的分辨率非常糟糕,通常为11-19毫秒。你已经做了足够多的迭代,只有大约1%,所以我不认为这是差异的一部分,但在编写基准测试时注意这一点很好。我知道你想要便携性,但如果你需要在Windows上进行更好的测量,你可以使用QueryPerformanceCounter
,这几乎肯定会给你带来更好的分辨率,尽管这仍然只是一段时间。
更新:在我了解到一个案例的长运行时间持续发生后,我启动了VS2010并复制了结果。我通常会在一些跑步中获得1000毫秒左右的成绩,在其他跑步中获得750毫秒,在莫名其妙的跑步中获得5000毫秒以上的成绩。
观察结果:
- 在所有情况下,不可预测的Ifs()代码都是内联的
- 删除noIfs()代码没有任何影响(所以长时间并不是该代码的副作用)
- 将线程相关性设置为单个处理器没有效果
- 5000毫秒的时间总是较晚的。我注意到后面的实例在循环开始之前有一条额外的指令:
lea ecx,[ecx]
。我不明白为什么这会产生5倍的差异。除此之外,早期和后期的实例都是相同的代码 - 从
start
和stop
变量中删除volatile
会产生更少的长运行、更多的750ms运行和无1000ms运行。(生成的循环代码现在在所有情况下看起来都完全相同,而不是lea
s。) - 从
sum
变量中删除volatile
(但保留它用于时钟计时器),长时间运行可以发生在任何位置 - 如果删除所有
volatile
限定符,则可以获得一致、快速(750毫秒)的运行。(该代码看起来与早期的代码相同,但sum
选择了edi
,而不是ecx
。)
我不知道从这一切中可以得出什么结论,除了volatile
对MSVC具有不可预测的性能影响,所以您应该只在必要时应用它。
UPDATE 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.
除了寄存器选择,我看不出有什么显著的区别。
- 删除一个线程上有数百万个字符串的大型哈希映射会影响另一个线程的性能
- OpenMP阵列性能较差
- 递归列出所有目录中的C++与Python与Ruby的性能
- 大小相等但成员数量不同的结构之间的性能差异
- 为什么constexpr的性能比正常表达式差
- 在类中使用随机生成器时出现性能问题
- 在main()之外初始化std::vector会导致性能下降(多线程)
- 海湾合作委员会 ARM 性能下降
- GCC 和 Clang 代码性能的巨大差异
- 在容量内调整矢量大小时的性能影响
- 了解算法的性能差异(如果以不同的编程语言实现)
- 未达到的情况会影响开关外壳性能
- QStringList vs list<shared_ptr<QString>> 性能比较C++
- 是否总是可以将使用递归编写的程序重写为不使用递归的程序C++,性能观点是什么?
- 哪种方法更好,性能明智
- C++ 特征库:引用的性能开销<>
- 与多个 for 循环与单个 for 循环 wrt 相关的性能从多映射获取数据
- 基于范围的 for 循环range_declaration中各种说明符之间的性能差异
- std::p mr::memory_resource 如何与 std::container 产生性能差异?
- 不同的类或结构初始化方法之间的性能差异是什么?