我有一些代码会运行数千次,并且想知道什么更快。
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);
}
}
他们每个人。这会更快/更慢吗?如果是这样,多少钱?
答案 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)
int **vals = {{0, 1*3, 2*3}, {0, 1*9, 2*9}, ...}
的静态二维数组,只需加上vals[0][array[1]] + vals[1][array[2]] + ...
答案 6 :(得分:0)
乘以,因为分支非常缓慢