2014-09-18 43 views
3

我想更多地了解如何分析我更经常使用的方法的性能表现。如何分析的CPP /汇编代码

我使用兰特()和时序大量调用我的方法作为绩效衡量的方法试过,但我也想了解更多有关如何通过了解汇编代码是干什么来衡量绩效。

例如,我读过关于试图优化sgn功能的人(Is there a standard sign function (signum, sgn) in C/C++?),所以我认为这将是一个很好的开始。我去http://gcc.godbolt.org和生成的ASM为以下代码(与-march=core-avx2 -fverbose-asm -Ofast -std=c++11 ICC):

int sgn_v1(float val) 
{ 
    return (float(0) < val) - (val < float(0)); 
} 

int sgn_v2(float val) 
{ 
    if (float(0) < val)  return 1; 
    else if (val < float(0)) return -1; 
    else      return 0; 
} 

这产生以下组件

L__routine_start__Z6sgn_v1f_0: 
sgn_v1(float): 
     vxorps %xmm2, %xmm2, %xmm2       #3.38 
     vcmpgtss %xmm2, %xmm0, %xmm1       #3.38 
     vcmpgtss %xmm0, %xmm2, %xmm3       #3.38 
     vmovd  %xmm1, %eax         #3.38 
     vmovd  %xmm3, %edx         #3.38 
     negl  %eax           #3.38 
     negl  %edx           #3.38 
     subl  %edx, %eax         #3.38 
     ret              #3.38 

L__routine_start__Z6sgn_v2f_1: 
sgn_v2(float): 
     vxorps %xmm1, %xmm1, %xmm1       #8.3 
     vcomiss %xmm1, %xmm0         #8.18 
     ja  ..B2.3  # Prob 28%      #8.18 
     vcmpgtss %xmm0, %xmm1, %xmm0       # 
     vmovd  %xmm0, %eax         # 
     ret              # 
..B2.3:       # Preds ..B2.1 
     movl  $1, %eax          #9.12 
     ret              #9.12 

个我的分析开始了与事实sgn_v1有9个指令和sgn_v2有6个或5个指令取决于跳跃的结果。有关sgn_v1是如何网点,似乎这是一件好事,以前的帖子会谈,我认为这意味着在sgn_v1多个指令可以在同一时间执行。我去了http://www.agner.org/optimize/instruction_tables.pdf,我无法在haswell部分为这些操作提供资金(p187-p202)。

如何分析呢?

编辑:

回应@ Raxvan的意见,我跑了下面的测试程序

extern "C" int sgn_v1(float); 
__asm__(
"sgn_v1:\n" 
" vxorps %xmm2, %xmm2, %xmm2\n" 
" vcmpgtss %xmm2, %xmm0, %xmm1\n" 
" vcmpgtss %xmm0, %xmm2, %xmm3\n" 
" vmovd  %xmm1, %eax\n" 
" vmovd  %xmm3, %edx\n" 
" negl  %eax\n" 
" negl  %edx\n" 
" subl  %edx, %eax\n" 
" ret\n" 
); 

extern "C" int sgn_v2(float); 
__asm__(
"sgn_v2:\n" 
" vxorps %xmm1, %xmm1, %xmm1\n" 
" vcomiss %xmm1, %xmm0\n" 
" ja  ..B2.3\n" 
" vcmpgtss %xmm0, %xmm1, %xmm0\n" 
" vmovd  %xmm0, %eax\n" 
" ret\n" 
" ..B2.3:\n" 
" movl  $1, %eax\n" 
" ret\n" 
); 

#include <cstdlib> 
#include <ctime> 
#include <iostream> 

int main() 
{ 
    size_t N = 50000000; 
    std::clock_t start = std::clock(); 
    for (size_t i = 0; i < N; ++i) 
    { 
    sgn_v1(float(std::rand() % 3) - 1.0); 
    } 
    std::cout << "v1 Time: " << (std::clock() - start)/(double)(CLOCKS_PER_SEC/1000) << " ms " << std::endl; 

    start = std::clock(); 
    for (size_t i = 0; i < N; ++i) 
    { 
    sgn_v2(float(std::rand() % 3) - 1.0); 
    } 
    std::cout << "v2 Time: " << (std::clock() - start)/(double)(CLOCKS_PER_SEC/1000) << " ms " << std::endl; 

    start = std::clock(); 
    for (size_t i = 0; i < N; ++i) 
    { 
    sgn_v2(float(std::rand() % 3) - 1.0); 
    } 
    std::cout << "v2 Time: " << (std::clock() - start)/(double)(CLOCKS_PER_SEC/1000) << " ms " << std::endl; 

    start = std::clock(); 
    for (size_t i = 0; i < N; ++i) 
    { 
    sgn_v1(float(std::rand() % 3) - 1.0); 
    } 
    std::cout << "v1 Time: " << (std::clock() - start)/(double)(CLOCKS_PER_SEC/1000) << " ms " << std::endl; 
} 

而且我得到以下结果:

g++-4.8 -std=c++11 test.cpp && ./a.out 
v1 Time: 423.81 ms 
v2 Time: 657.226 ms 
v2 Time: 666.233 ms 
v1 Time: 436.545 ms 

所以网点的结果显然更好; @吉姆的建议我看看分支预测器是如何工作的,但我仍然无法找到计算管道如何“满”的一种方式......

+4

简单的方法:编写一些使用该函数的代码,并测量时间差。这应该给你一个第一和第二个功能之间的比例。另一种方法是使用类似Intel Vtune的分析器(或者Visual Studio 2013内置的分析器)。有一点需要注意的是,这可能会给你不同的结果**基于你正在使用的编译器,优化和cpu体系结构** – Raxvan 2014-09-18 16:15:40

+2

无分支意味着没有跳转指令。这很好,因为如果CPU是流水线的,它可以一次执行多条指令。如果有分支,CPU可能不得不放弃它在管道中做的一些工作,如果它的分支预测不正确。 (http://en.wikipedia.org/wiki/Branch_predictor) – Jim 2014-09-18 17:52:20

+0

你的'sgn_v1'和'sgn_v2'应该声明为'inline' – 2014-09-18 20:17:04

回答

0

一般时间是一个非常有噪声测量结果尤其是当你连续测量的东西进入单个运行/过程,这意味着一个接一个地交错事件,可能会增加噪音。正如你所提到的,分支机构对流水线有重大影响,并且作为一条经验法则,分支较少的代码应该表现更好,一般而言,在性能中扮演角色的两个主要因素是参考和分支预测的局部性,而在更复杂的情况下当使用多线程时,还有其他因素。要回答你的问题,我会说最好使用诸如perf这样的工具,它可以指示缓存未命中的数量和分支未命中预测,这应该给出一个很好的指示,一般取决于你正在开发的平台能够找到可能查询CPU性能计数器的适当工具。此外,您应该真的生成一组随机值,​​并使用与两个函数完全相同的值,以便从std :: rand()的执行中消除噪声。最后要记住的是,根据不同的编译器,编译选项(显然)和目标体系结构,代码的执行方式会有所不同,但是,您可以应用的某些逻辑应该保持不变,例如,在您的示例中,没有条件的代码 - 分支应该总能更好地执行。如果你想真正挑剔它,你应该阅读英特尔的手册(尤其是avx)。