C++ 在堆上分配相同类型的变量会花费非常不同的时间

C++ Allocating same type of variables on the heap costs tremendously different amount of time

我在处理 运行 大数据集时遇到了性能问题。为了简单起见,我记下了以下代码:

double *a = new double();
for (int i = 0; i < 1000000; i++){ 
    double x = 0;
    double y = 0;
    for (int j = 0; j < 1000; j++){
        x = 1000;
        y++;
    }
    *a = x; //*a = y;
}

这需要将近 0 毫秒。但是,如果我将 y 分配给 *a:

double *a = new double();
for (int i = 0; i < 1000000; i++){ 
    double x = 0;
    double y = 0;
    for (int j = 0; j < 1000; j++){
        x = 1000;
        y++;
    }
    *a = y; //*a = x;
} 

这需要 763 毫秒,比第一种情况要长得多。我发现这是由循环中 y 的相对更复杂的计算引起的。但我不知道为什么会这样。如果我改变

*a = y;

double temp=y;
*a = temp;

这仍然花费将近 763 毫秒。似乎无论我如何传递值,我都无法有效地将 y 的值分配给 *a 。谁能解释为什么在完成内部循环后 y 与 x 明显不同?为什么即使我将 y 的值转移到其他临时变量,仍然需要很长时间才能将该值分配给 *a? (顺便说一句,如果 'a' 是 double 而不是指向 double 的指针,则分配 y 和 x 的值没有区别。)

double *a = new double();
// OUTER LOOP:
for (int i = 0; i < 1000000; i++){ 
  double x = 0;
  double y = 0;
  // INNER LOOP:
  for (int j = 0; j < 1000; j++){
    x = 1000;
    y++;
  }
  *a = x; //*a = y;
}

在您的 OUTER LOOP 中,您重复将 *a 分配为 xy

在您的 INNER LOOP 中,您要么将 x 重复设置为 1000,要么增加 y 1000 次。

现在,编译器知道 x=1000 后跟 x=1000 相当于执行一次。因此,按如下方式优化您的代码真的很容易:

double *a = new double();
// OUTER LOOP:
for (int i = 0; i < 1000000; i++){ 
  constexpr double x = 1000;
  double y = 0;
  // INNER LOOP:
  for (int j = 0; j < 1000; j++){
    y++;
  }
  *a = x; //*a = y;
}

然后

for (int i = 0; i < 1000000; i++){ 
  double y = 0;
  // INNER LOOP:
  for (int j = 0; j < 1000; j++){
    y++;
  }
  *a = 1000; //*a = y;
}

然后

for (int i = 0; i < 1000000; i++){ 
  double y = 0;
  // INNER LOOP:
  for (int j = 0; j < 1000; j++){
    y++;
  }
  //*a = y;
}
*a = 1000; 

因为这些操作中的每一个都是合法的。完成后,您对 y 所做的所有工作都没有副作用(因为在这种情况下我们从未将其分配给 *a),因此变量 y 被消除:

for (int i = 0; i < 1000000; i++){ 
  // INNER LOOP:
  for (int j = 0; j < 1000; j++){
  }
}
*a = 1000; 

这使得那些循环为空。并且可以消除空循环(编译器甚至不必证明它们终止了!),留下:

*a = 1000; 

另一方面,在 y 上执行 y++ 1000 次通常 而不是 与执行 y += 1000 相同由于 y 开始时可能足够大,浮点舍入会导致问题。在这种情况下,这是不正确的,因为将 +1 添加到 0. 1000 次时不会发生舍入,但在 general 中则不正确。因为证明没有舍入是困难的——而且完全正确更难——编译器作者可能没有处理这种情况。

这留下了更复杂的代码来优化,编译器很难确定循环的每次迭代是否完全相同,因此您观察到优化器失败了。

在这种情况下它可以优化多少我们必须检查反汇编。

在您分配 *a = y 的情况下,程序需要结束循环才能知道要为 a.

分配什么值

另一种情况,因为你没有修改x的值,而是一直赋值一些constexpr,所以可以从循环中取出来,实际上这个循环从来没有真正执行过,因为它不对外界有什么影响。