C ++ - 什么会更快:乘法或加法?

时间:2010-08-08 22:37:01

标签: c++ optimization

我有一些代码会运行数千次,并且想知道什么更快。 array是一个30值的短数组,总是保持0,1或2。

result = (array[29] * 68630377364883.0)
       + (array[28] * 22876792454961.0)
       + (array[27] * 7625597484987.0)
       + (array[26] * 2541865828329.0)
       + (array[25] * 847288609443.0)
       + (array[24] * 282429536481.0)
       + (array[23] * 94143178827.0)
       + (array[22] * 31381059609.0)
       + (array[21] * 10460353203.0)
       + (array[20] * 3486784401.0)
       + (array[19] * 1162261467)
       + (array[18] * 387420489)
       + (array[17] * 129140163)
       + (array[16] * 43046721)
       + (array[15] * 14348907)
       + (array[14] * 4782969)
       + (array[13] * 1594323)
       + (array[12] * 531441)
       + (array[11] * 177147)
       + (array[10] * 59049)
       + (array[9]  * 19683)
       + (array[8]  * 6561)
       + (array[7]  * 2187)
       + (array[6]  * 729)
       + (array[5]  * 243)
       + (array[4]  * 81)
       + (array[3]  * 27)
       + (array[2]  * 9)
       + (array[1]  * 3)
       + (b[0]);

如果我使用类似的东西会更快吗?

if(array[29] != 0)
{
    if(array[29] == 1)
    {
        result += 68630377364883.0;
    }
    else
    {
        result += (whatever 68630377364883.0 * 2 is);
    }
}

他们每个人。这会更快/更慢吗?如果是这样,多少钱?

7 个答案:

答案 0 :(得分:12)

这是一个荒谬的过早“优化”。您可能会伤害性能,因为您正在为代码添加分支。错误预测的分支机构非常昂贵。它还使代码更难以阅读。

现代处理器中的乘法比以前快得多,现在可以完成几个时钟周期。

这是一个提高可读性的建议:

for (i=1; i<30; i++) {
    result += array[i] * pow(3, i);
}
result += b[0];

如果您真的担心性能,可以预先计算值为pow(3, i)的数组。

答案 1 :(得分:8)

首先,在大多数体系结构中,错误分支是非常昂贵的(取决于执行管道深度),所以我敢打赌非分支版本更好。

代码的变体可能是:

result = array[29];
for (i=28; i>=0; i--)
    result = result * 3 + array[i];

确保没有溢出,因此result 必须是大于32位整数的类型。

答案 2 :(得分:5)

即使加法比乘法更快,我认为你会因为分支而失去更多。在任何情况下,如果加法比乘法更快,更好的解决方案可能是使用表和索引。

const double table[3] = {0.0, 68630377364883.0, 68630377364883.0 * 2.0};
result += table[array[29]];

答案 3 :(得分:3)

我的第一次优化尝试是删除浮点运算,转而采用整数运算:

uint64_t total = b[0];
uint64_t x = 3;
for (int i = 1; i < 30; ++i, x *= 3) {
    total += array[i] * x;
}

uint64_t不是标准的C ++,但可以广泛使用。您只需要一个适合您平台的C99 stdint版本。

还有可理解性和可维护性的优化 - 这个代码在某一点上是一个循环,你在更换循环时测量了性能差异吗?像这样完全展开甚至可能使程序更慢(以及更少可读),因为代码更大并因此占用更多的指令高速缓存,因此导致其他地方的高速缓存未命中。你只是不知道。

这当然假设你的常数实际上是3的幂 - 我没有打扰检查,这正是我认为你的代码的可读性问题......

答案 4 :(得分:3)

这基本上是做strtoull所做的事情。如果您没有方便的数字作为ASCII字符串提供给strtoull,那么我想你必须编写自己的实现。正如人们所指出的那样,分支是导致性能下降的原因,因此您的功能可能最好以这种方式编写:

#include <tr1/cstdint>
uint64_t base3_digits_to_num(uint8_t digits[30])
{
    uint64_t running_sum = 0;
    uint64_t pow3 = 1;
    for (int i = 0; i < 30; ++i) {
        running_sum += digits[i] * pow3;
        pow3 *= 3;
    }
    return running_sum;
}

我不清楚预先计算你3的能力会带来显着的速度优势。你可以尝试一下自己测试一下。查找表可能给您带来的一个好处是,智能编译器可能会将循环展开为SIMD指令。但是一个非常聪明的编译器应该能够做到这一点并为你生成查找表。

避免浮点也不一定是速度胜利。在过去5年中生产的大多数处理器上,浮点和整数运算大致相同。

检查digits[i]是否为0,1或2并为每种情况执行不同的代码绝对是过去10年中生产的任何处理器的速度损失。 Pentium3 / Pentium4 / Athlon Thunderbird时代的分支开始真正成为一个巨大的打击,Pentium3至少已经有10年了。

最后,您可能会认为这将是代码中的瓶颈。你可能错了。对于任何阅读代码的人来说,正确的实现是最简单和最清晰的实现。然后,如果您想获得最佳性能,请通过分析器运行代码,并找出集中优化工作的位置。当你甚至不知道它是一个瓶颈时,对这个功能进行了大量的贬低是很愚蠢的。

几乎没有人在这里认识到你基本上在进行基础3转换。因此,即使您当前的原始手循环展开也会使您的代码模糊不清,以至于大多数人都不理解它。

编辑:实际上,我查看了程序集输出。在x86_64平台上,查找表不会为您提供任何内容,实际上可能会因为它对缓存的影响而适得其反。编译器生成leaq (%rdx,%rdx,2), %rdx以便乘以3.从表中获取将类似于moveq (%rdx,%rcx,8), %eax,除了需要从内存中获取(这可能非常昂贵)之外,它基本上是相同的速度。因此几乎可以肯定,我的gcc选项-funroll-loops代码明显快于您手动优化的速度。

这里的教训是,编译器的优化工作要比你做得好得多。只需使代码尽可能清晰易读,让编译器完成工作。并且让其他人明白其他优点是使编译器更容易完成其工作。

答案 5 :(得分:0)

  1. 如果你不确定 - 为什么不自己测量呢?
  2. 第二个例子很可能会慢得多,但不是因为增加了 - 错误预测的条件跳转会耗费大量时间。
  3. 如果您只有3个值,最便宜的方法可能是拥有值int **vals = {{0, 1*3, 2*3}, {0, 1*9, 2*9}, ...}的静态二维数组,只需加上vals[0][array[1]] + vals[1][array[2]] + ...
  4. 有些SIMD指令可能比你自己写的任何东西都要快 - 看看那些。再说一遍 - 如果你这么做很多,把它交给GPU可能会更快 - 取决于你的其他计算。

答案 6 :(得分:0)

乘以,因为分支非常缓慢