如何分析cpp/assembly代码的性能

How do I analyze performance of cpp / assembly code

本文关键字:assembly 代码 性能 cpp      更新时间:2023-10-16

我正在尝试了解更多关于如何分析我更常用的方法的性能的信息。

我已经尝试过使用rand()和计时对我的方法的大量调用作为性能测量方法,但我也想通过了解汇编代码的作用来了解更多关于如何测量性能的信息。

例如,我读到有人试图优化sgn函数(C/C++中有标准的符号函数(signum,sgn)吗?)所以我觉得这将是一个很好的开始。我去了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, %xmm2n"
"  vcmpgtss  %xmm2, %xmm0, %xmm1n"
"  vcmpgtss  %xmm0, %xmm2, %xmm3n"
"  vmovd     %xmm1, %eaxn"
"  vmovd     %xmm3, %edxn"
"  negl      %eaxn"
"  negl      %edxn"
"  subl      %edx, %eaxn"
"  retn"
);
extern "C" int sgn_v2(float);
__asm__(
"sgn_v2:n"
"  vxorps    %xmm1, %xmm1, %xmm1n"
"  vcomiss   %xmm1, %xmm0n"
"  ja        ..B2.3n"
"  vcmpgtss  %xmm0, %xmm1, %xmm0n"
"  vmovd     %xmm0, %eaxn"
"  retn"
"  ..B2.3:n"
"  movl      $1, %eaxn"
"  retn"
);
#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

因此,无分支的结果显然更好@Jim建议我研究分支预测器是如何工作的,但我仍然找不到计算管道"满"的方法…

通常情况下,时间是一种非常嘈杂的测量方法,尤其是当您在单个运行/进程中按顺序测量事物时,这意味着一个接一个的交错事件可能会增加噪声。正如您所提到的,分支对管道有很大影响,根据经验,分支较少的代码应该表现得更好,通常,对性能起作用的两个主要因素是参考位置和分支预测,而在更复杂的情况下,如使用多线程时,还有其他因素。为了回答您的问题,我想说,最好使用诸如perf之类的工具,例如,它可以指示缓存未命中的数量和分支未命中预测,这应该是一个很好的指示,通常,根据您正在开发的平台,您可能能够找到一个适当的工具来查询CPU的性能计数器。此外,您应该真正生成一组随机值,并在两个函数中使用完全相同的值,这样就可以消除std::rand()执行过程中的噪声。最后要记住,代码的性能会因编译器、编译选项(显然)和目标体系结构的不同而有所不同,但无论如何,您可以应用的一些逻辑都应该保持不变,因为在您的示例中,没有条件分支的代码应该总是表现得更好。如果你真的很挑剔,你应该仔细阅读英特尔的手册(尤其是avx)。