【问题标题】:Why is call by reference so much slower than inline code?为什么引用调用比内联代码慢得多?
【发布时间】:2020-12-10 07:32:12
【问题描述】:

我正在编写一个包含少量粒子(通常为 3 个,不超过 5 个)的物理模拟。

在精简版中,我的代码结构如下:

#include<iostream>

class Particle{
  double x; // coordinate
  double m; // mass
};


void performStep(Particle &p, double &F_external){
   p.x += -0.2*p.x + F_external/p.m; // boiled down, in reality complex calculation, not important here
}
 
int main(){

 dt = 0.001; // time step, not important

 Particle p1;
 p1.x = 5; // some random number for initialization, in reality more complex but not important here
 p.m = 1;

 Particle p2;
 p2.x = -1; // some random numbersfor initialization, in reality more complex but not important here
 p.m = 2;

 Particle p3;
 p3.x = 0; // some random number for initialization, in reality more complex but not important here
 p.m = 3;

 double F_external = 0; // external forces

 for(unsigned long long int i=0; i < 10000000000; ++i){ // many steps, typically 10e9
    F_external = sin(i*dt);
    performStep(p1, F_external);
    performStep(p2, F_external);
    performStep(p3, F_external);
 }

 std::cout << "p1.x: " << p1.x << std::endl;
 std::cout << "p2.x: " << p2.x << std::endl;
 std::cout << "p3.x: " << p3.x << std::endl;

}

我已通过clock() 确定performStep(p, F_external) 调用是我代码中的瓶颈)。 当我尝试进行内联计算时,即用 p1.x += -0.2*p1.x + F_external/p1.m; 替换 performStep(p1, F_external) 时,计算突然快了大约 2 倍。请注意,performStep() 实际上是大约 20 行的大约 60 次基本算术计算,所以如果我只为每个粒子内联它,代码就会变得非常臃肿。

为什么会这样?我正在使用 MinGW64/g++ 和 -O2 标志进行编译。我以为编译器会优化这些东西?

编辑:

这是被调用的函数。请注意,实际上,我使用几个不同的外力计算所有三个坐标 x、y、z。未通过函数传递的变量是SimulationRun 的成员。该算法是四阶leapfrog算法。

void SimulationRun::performLeapfrog_z(const unsigned long long int& i, const double& x, const double& y, double& z, const double& vx, const double& vy, double& vz, const double& qC2U0, 
    const double& U0, const double& m, const double& C4, const double& B2, const double& f_minus, const double& f_z, const double& f_plus, const bool& bool_calculate_xy,
    const double& Find, const double& Fheating) {

    // probing for C4 == 0 and B2 == 0 saves some computation time
    if (C4 == 0) {
        Fz_C4_Be = 0;
    }
    if (B2 == 0 || !bool_calculate_xy) {
        Fz_B2_Be = 0;
    }


    z1 = z + c1 * vz * dt;


    if (C4 != 0 && !bool_calculate_xy) {
        Fz_C4_Be = (-4) * q * C4 * U0 * z1 * z1 * z1;
    }
    else if (C4 != 0 && bool_calculate_xy) {
        Fz_C4_Be = q * C4 * U0 * (-4 * z1 * z1 * z1 + 6 * z1 * (x * x + y * y));
    }
    if (B2 != 0 && bool_calculate_xy) {
        Fz_B2_Be = q * B2 * (-vx * z1 * y + vy * z1 * x);
    }
    acc_z1 = (qC2U0 * (-2) * z1 + Find + Fz_C4_Be + Fz_B2_Be + Fheating) / m;
    vz1 = vz + d1 * acc_z1 * dt;
    z2 = z1 + c2 * vz1 * dt;


    if (C4 != 0 && !bool_calculate_xy) {
        Fz_C4_Be = (-4) * q * C4 * U0 * z2 * z2 * z2;
    }
    else if (C4 != 0 && bool_calculate_xy) {
        Fz_C4_Be = q * C4 * U0 * (-4 * z2 * z2 * z2 + 6 * z2 * (x * x + y * y));
    }
    if (B2 != 0 && bool_calculate_xy) {
        Fz_B2_Be = q * B2 * (-vx * z2 * y + vy * z2 * x);
    }
    acc_z2 = (qC2U0 * (-2) * z2 + +Find + Fz_C4_Be + Fz_B2_Be + Fheating) / m;
    vz2 = vz1 + d2 * acc_z2 * dt;
    z3 = z2 + c3 * vz2 * dt;

    if (C4 != 0 && !bool_calculate_xy) {
        Fz_C4_Be = (-4) * q * C4 * U0 * z3 * z3 * z3;
    }
    else if (C4 != 0 && bool_calculate_xy) {
        Fz_C4_Be = q * C4 * U0 * (-4 * z3 * z3 * z3 + 6 * z3 * (x * x + y * y));
    }
    if (B2 != 0 && bool_calculate_xy) {
        Fz_B2_Be = q * B2 * (-vx * z3 * y + vy * z3 * x);
    }
    acc_z3 = (qC2U0 * (-2) * z3 + Find + Fz_C4_Be + Fz_B2_Be + Fheating) / m;
    vz3 = vz2 + d3 * acc_z3 * dt;

    z = z3 + c4 * vz3 * dt;
    vz = vz3;
}

【问题讨论】:

  • 旁注:你的循环不是要求 10^10 次迭代,这会溢出 unsigned int 吗?
  • 您可能想发布performStep() 的其余部分。传递引用比在 C 中传递一个简单的指针更复杂。我并没有想到这将如何影响performStep() 的效率,但至少看看引用的使用方式可能会指向这里的一些聪明人正朝着正确的方向前进。
  • 或者使用-Ofast 继续编译,并提供编译器必须提供的所有内容。
  • 您说的是boiled down, in reality complex calculation, not important here,但实际上它很关键,因为函数的复杂性是编译器用来决定是否内联的主要标准之一,请提供minimal reproducible example。例如。您的代码确实与 gcc 内联:godbolt.org/z/E5oeW4
  • 另外,如果您使用简单类型,请不要使用 const 引用,只需按值使用它们。它将简化代码并且编译器不必担心取消引用它们(尽管存在差异,您可以将它们视为与指针相同的方式,尽管不必使用箭头 -> ,但它们必须被取消引用)。取消引用是有代价的,在这种情况下是不需要的

标签: c++ performance methods


【解决方案1】:

优化很难,即使对于编译器也是如此。以下是一些优化提示:

  1. 因为你的performStep是热点,所以把它放到一个头文件中(如果你把声明和定义分成头/源),然后添加inline关键字,比如:
// at file xxx.h
inline void performStep(Particle &p, double F_external){
   p.x += -0.2*p.x + F_external/p.m; // boiled down, in reality complex calculation, not important here
}
 
  1. 将您的编译器升级到最新版本。
  2. 使用https://godbolt.org/检查汇编代码。在这种情况下,不必要的取消引用是性能问题。

【讨论】:

  • inline 关键字令人困惑地与编译器内联优化几乎/无关,它主要防止在多个翻译单元中定义相同函数时出现链接器错误
  • 这并不能回答问题,尽管它可能会有所帮助。
猜你喜欢
  • 1970-01-01
  • 2019-01-14
  • 2013-08-08
  • 2012-01-24
  • 2021-01-24
  • 2021-09-09
  • 1970-01-01
  • 1970-01-01
  • 2017-12-26
相关资源
最近更新 更多