你选择了最慢的方式来进行检查。
c*c == a*a + b*b // assuming c is non-negative
这段代码编译后会产生三个整数乘法(其中一个可以提出循环)。即使没有使用pow()
函数,你仍然需要将其转换为double
类型并进行平方根运算,这对于吞吐量来说非常糟糕。(对于延迟也是如此,但现代CPU上的分支预测和推测执行意味着延迟在这里不是一个因素)。
Intel Haswell的SQRTSD指令的吞吐量为每8-14个周期执行一次(source: Agner Fog's instruction tables),因此即使你的sqrt()
版本可以保持FP sqrt执行单元饱和,它仍然比我得到的gcc编译版本慢4倍(下面附有代码)。
您还可以优化循环条件,当条件中的
b < c
不再成立时跳出循环,这样编译器只需要进行一次检查版本。
void foo_optimized()
{
for (int a = 1; a <= SUMTOTAL; a++) {
for (int b = a+1; b < SUMTOTAL-a-b; b++) {
int c = (SUMTOTAL-a) - b;
if ( c*c == a*a + b*b) {
std::cout << "a: " << a << " b: " << b << " c: "<< c << '\n';
std::cout << a * b * c << '\n';
}
}
}
}
这段代码(使用gcc6.2的
-O3 -mtune=haswell
编译)生成了以下内部循环的代码。请在
Godbolt编译器浏览器上查看完整代码。
# a*a is hoisted out of the loop. It's in r15d
.L6:
add ebp, 1 # b++
sub ebx, 1 # c--
add r12d, r14d # ivtmp.36, ivtmp.43 # not sure what this is or why it's in the loop, would have to look again at the asm outside
cmp ebp, ebx # b, _39
jg .L13 ## This is the loop-exit branch, not-taken until the end
## .L13 is the rest of the outer loop.
## It sets up for the next entry to this inner loop.
.L8:
mov eax, ebp # multiply a copy of the counters
mov edx, ebx
imul eax, ebp # b*b
imul edx, ebx # c*c
add eax, r15d # a*a + b*b
cmp edx, eax # tmp137, tmp139
jne .L6
## Fall-through into the cout print code when we find a match
## extremely rare, so should predict near-perfectly
在英特尔Haswell架构中,所有这些指令都是1个微操作。 (而cmp / jcc对宏融合成比较和分支微操作。) 因此,这是10个融合域微操作,
每2.5个周期可以迭代一次。
Haswell以每个时钟周期的速度运行
imul r32,r32
,因此内部循环中的两个乘法不会以每2.5c的速度饱和端口1。这为吸收ADD和SUB从端口1窃取的不可避免的资源冲突留出了空间。
我们甚至没有接近任何其他执行端口瓶颈,因此
前端瓶颈是唯一的问题,在英特尔Haswell及更高版本上应该以每2.5个周期的速度运行。
循环展开可以帮助减少每次检查的uops数量。例如,使用
lea ecx,[rbx + 1]
计算下一次迭代的b + 1,这样我们就可以
imul ebx,ebx
而不需要使用MOV使其非破坏性。
也可以使用强度降低技术:给定
b * b
,我们可以尝试计算
(b-1) * (b-1)
而不使用IMUL。
(b-1) * (b-1) = b*b - 2*b + 1
,因此我们可以尝试使用
lea ecx,[rbx*2-1]
,然后从
b*b
中减去它。 (没有减法寻址模式。嗯,也许我们可以在一个寄存器中保留
-b
,并向零计数,这样我们就可以使用
lea ecx,[rcx + rbx*2-1]
来更新ECX中的
b*b
,给定EBX中的
-b
)。
除非您实际上受到IMUL吞吐量的限制,否则这可能会导致更多uops,并且不会获胜。 在C ++源代码中使用此强度降低技术,可能会很有趣。
你可能也可以使用SSE或AVX将其向量化,同时并行检查4或8个连续的
b值。由于命中非常罕见,只需检查这8个值中是否有任何一个命中,然后在极少数情况下确定是哪一个命中。
另请参阅
x86标签wiki以获取更多优化内容。
pow
是一个浮点函数。其次,只有在运行经过优化的版本时,发布函数的运行时间才有意义。如果你运行未经优化的“调试”版本,则时间没有意义。最后但同样重要的是,如果指数是整数,请勿使用pow。 - PaulMcKenziec*c
溢出的情况,使用c*c = a*a + b*b
会更快,因为乘法(特别是整数乘法)比开方运算更快。 - Roel Schroevenif (b < c && c*c == a*a + b*b)
而不是if (c*c == a*a + b*b && b < c)
:b < c
是一项快速操作,使程序在不需要时跳过相对较慢的计算。 - Roel Schroeven