为什么 gcc -O3 对于相同的功能会产生截然不同的汇编?

bre*_*nzo 8 c++ optimization assembly gcc compiler-optimization

我在物理引擎中有一个循环,它检测如下碰撞:

  // now check for collisions
  // we only allow 1 collision per 2 partcles per frame so the
  // one with the lower index will always "collide" first
  for (size_t j = 0; j < m_particles.size(); j++) {
    for (size_t k = j + 1; k < m_particles.size(); k++) {
      m_particles[j].collide(m_particles[k]);
    }
  }
Run Code Online (Sandbox Code Playgroud)

我进行了更改,在内部对粒子使用双缓冲区。(顺便说一句;这样做是为了我可以检测到错误并返回 1 个时间步来纠正)。双缓冲区是用两个指针实现的,我在这个循环之后交换了这两个指针。所以现在我们有:

  // now check for collisions
  // we only allow 1 collision per 2 partcles per frame so the
  // one with the lower index will always "collide" first
  for (size_t j = 0; j < m_current_particles->size(); j++) {
    for (size_t k = j + 1; k < m_current_particles->size(); k++) {
      (*m_current_particles)[j].collide((*m_current_particles)[k]);
    }
  }

std::swap(m_current_particles, m_previous_particles);
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,容器都是std::vector.

使用相同的驱动程序,第二个速度慢 10 倍以上。函数内部没有任何collide()变化,我可以通过创建一个仅切换到双缓冲区而没有其他任何更改的构建来重现。

collide()最终我打开了分析工具,果然……尽管来源相同,但该功能的程序集完全不同。在任何一种情况下,都有一个显式的子callq例程collide,因此在前一种情况下,它不会将碰撞函数内联到循环中。

在前一种情况下:

  • operator粒子 Vector(我自己的向量实现,而不是 )成员上的所有 Vector重载std::vector均已转换为内联汇编。
  • 大量使用%xmm寄存器
  • 由一堆专用浮点乘法、加法、sqrt、指令组成。
  • 自包含的子例程,只有条件跳转回开始,正如您所期望的那样。

在性能回归的情况下:

  • 最小内联,上下文切换以调用所有 Vector 操作的子例程。
  • 几乎没有使用%xmm寄存器,一切都是%rax%rbx等等。
  • 即使检查子例程,它们似乎也没有得到很好的优化(更多的汇编,而不是大量使用专用的 mul/sqrt 指令)。
  • 上下文切换多次会带来巨大的性能开销。

尽我所能,我无法说服编译器使用新代码生成类似于原始程序集的内容。

  • 尝试标记所有inline被调用的collide()内容(尽管我听说这些天编译器基本上会忽略你,如果你这样做)。
  • 为了好玩,尝试通过仅访问成员并在函数中执行它来手动编写所有数学运算,而不是调用它们的函数/运算符。没有骰子。不过,确实让我的性能提升了 7 倍,而不是 10 倍。
  • 在循环内以不同的方式调用函数。
  • 创建auto& local_particles = m_current_particles本地变量,以防编译器对指针的范围感到不安。
  • 在本地复制数组并调用它,使其看起来与原始循环完全相同,然后将数组复制回来。运气不好,即使解决了也无法扩展。
  • 向神恳求。

这是否只是编译器疯狂的大脑在前一种情况下以正确的方式排列的情况,或者我的调用约定以某种方式破坏了我在这两个来源之间坚持的不变量?

这是调用图的相关部分:

前一个案例(所有内容都已内联):

在此输入图像描述

回归(显式调用是显而易见的):

在此输入图像描述

需要折叠以合理地适应,下面的调用collide()operator-Vector 和 Vector 构造函数本身的重载,用于创建临时对象。

编辑:在前一种情况下使用 -O2 ,它会内联昂贵的向量减法运算符,但不会内联临时的 ctor 调用。运行速度大约是 -O3 的 1.3 倍。回归情况下的-O2 和-O3 看起来本质上是相同的。

在此输入图像描述