衡量C/C++性能的困难

Difficulties to measure C/C++ performance

本文关键字:性能 C++ 衡量      更新时间:2023-10-16

我写了一段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的时间显著不同。

对于-O2unpredictableIfs的循环与-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稍微快一点。

对于-O3unpredictableIfs的循环变得更差,

.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毫秒以上的成绩。

观察结果:

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

除了寄存器选择,我看不出有什么显著的区别。