【问题标题】:Difficulties to measure C/C++ performance难以衡量 C/C++ 性能
【发布时间】:2013-02-04 14:38:48
【问题描述】:

我编写了一段 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 %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 /分析- /errorReport:queue

其他人发了这样的……用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

现在我有问题。这是怎么回事?

更具体地说......一个固定的功能怎么会花费如此不同的时间?我的代码有问题吗?英特尔处理器有什么棘手的问题吗?编译器是否在做一些奇怪的事情?可能是因为 32 位代码在 64 位处理器上运行?

感谢关注!

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

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

【问题讨论】:

  • 你真的检查过你的“if”和“noIfs”在编译后实际上是不同的吗?如今,许多编译器都知道,对于你正在做的事情,做一些数学运算比条件跳转更好......
  • @MatsPetersson 使用 VS2010 生成的代码完全不同。我将编辑帖子进行解释。
  • @ÖöTiib 请务必向我们展示您在使用 VS2010 编译时使用的命令行开关。
  • 很难写出好的速度测试。编译器比你想象的要聪明得多。
  • @PeteBecker 这就是我问的原因。如果很难,那么值得学习。 ;)

标签: c++ c performance optimization measurement


【解决方案1】:

对于-O1,gcc-4.7.1 只调用unpredictableIfs 一次并重新使用结果,因为它识别出它是一个纯函数,所以每次调用的结果都是一样的。 (我的确实如此,通过查看生成的程序集进行验证。)

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

除此之外,我的 gcc-4.7.1 在使用 -O1-O2 时最好处理 unpredictableIfs(除了重用问题,两者都产生相同的代码),而 noIfs 被处理 使用-O3 会更好。然而,相同代码的不同运行之间的时间在这里是一致的 - 等于或不同 10 毫秒(clock 的粒度),所以我不知道是什么可能导致您为 @987654330 报告的 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

-O1。两个循环的运行时间相似,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 的运行速度几乎是当时的四倍。

【讨论】:

  • 已通过 4.7.2 验证。当将诸如 printf 之类的副作用插入函数时,也不会发生对 -O1 的优化。
  • 什么?不知何故,我觉得这应该是一个 GCC 错误。
  • @JonasWielicki 为什么? unpredictableIfs 的结果始终相同,可以在编译时计算。或者您是否对为什么不通过更积极的优化重用它感到困惑?
  • @JonasWielicki 为什么?因为用-O2-O3 重用结果不够聪明,还是因为它用-O1 重用结果?
  • @NikBougalis 当然不是因为内联!我认为这是一个错误,-O2 及更高版本的代码实际上变得更糟。
【解决方案2】:

对了,看gcc在64位Linux上的汇编代码,第一种情况,加上-O1,函数UnpredictableIfs确实只被调用了一次,结果被复用了。

使用 -O2 和 -O3,函数是内联的,所花费的时间应该相同。任何一段代码也没有实际的分支,但是这两位代码的翻译有些不同,我已经删除了更新“sum”的行[在%edx中]

不可预测的如果:

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

如您所见,它并不完全相同,但做的事情却非常相似。

【讨论】:

    【解决方案3】:

    关于 Windows 上的结果范围(从 1016 毫秒到 4797 毫秒):您应该知道 MSVC 中的 clock() 返回 elapsed wall time。标准规定clock() 应该返回CPU time spent by the process 的近似值,其他实现在这方面做得更好。

    鉴于 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 会产生更少 个长时间运行,更多的是750 毫秒的运行,而没有1000 毫秒的运行。 (生成的循环代码现在在所有情况下看起来都完全相同,而不是 leas。)
    6. sum 变量中删除volatile(但为时钟计时器保留它),长时间运行可以发生在任何位置。
    7. 如果您删除所有volatile 限定符,您将获得一致、快速(750 毫秒)的运行。 (代码看起来与之前的代码相同,但为 sum 选择了 edi 而不是 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.
    

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

    【讨论】:

    • 这通常是一个使微小测量变得无用的问题。在测量持续一秒钟的事物时,它还会导致测试之间出现 1-2% 的波动。它无法解释 472% 的差异。差异是可重复的。再次运行测试,我经常得到完全相同的数字。
    • 我没有意识到长期持续发生。
    • 删除 volatile 是个坏主意。 volatile 强制以与代码中完全相同的顺序将函数调用的结果分配给变量。编译器可以证明函数 clock() 和 noIfs() 是不相关的,但仍然无法使用 volatile 优化时序。 volatile 没有其他影响。所测量的测试不含任何挥发性物质,因此它不会影响它们的性能。
    • 我一直和你在一起,直到你说 volatile 不会影响性能,因为它确实会以一致且戏剧性的方式影响性能。
    • 不,它没有。其他东西确实会影响性能。缺乏 volatile 使得有时将对 clock() 的调用放到错误的地方。我从反汇编程序验证。我进行了更多测试,只是对它们进行排序会导致不可预测的Ifs() 运行有时 1300 毫秒有时 1000 毫秒,有时 4800 毫秒 不稳定与否,甚至人类也能感受到差异。
    猜你喜欢
    • 1970-01-01
    • 2011-08-12
    • 1970-01-01
    • 2013-12-29
    • 2011-09-23
    • 1970-01-01
    • 2011-08-29
    • 2021-03-20
    • 2019-01-26
    相关资源
    最近更新 更多