为什么pow(int,int)这么慢?

时间:2016-12-10 06:19:59

标签: c++ performance pow cmath

我一直致力于一些项目欧拉练习,以提高我对C ++的了解。

我已经写了以下功能:

if (c == sqrt(pow(a,2)+pow(b,2)) && b < c)

计算时间为17毫秒。

但是,如果我改变了行

if (c == sqrt((a*a)+(b*b)) && b < c)

pow(int, int)

计算在2毫秒内完成。我是否遗漏了{{1}}的一些明显的实现细节,这使第一个表达式的计算速度变慢了?

2 个答案:

答案 0 :(得分:69)

pow()使用实数浮点数并使用公式

pow(x,y) = e^(y log(x))

计算x^y。在致电int之前,double会转换为pow。 (log是自然对数,基于e)

因此使用x^2的{​​p> pow()x*x慢。

根据相关评论进行编辑

  • 使用pow甚至整数指数可能会产生错误的结果( PaulMcKenzie
  • 除了使用带有 double 类型的数学函数外,pow是一个函数调用(x*x isn&t; t)( jtbandes
  • 许多现代编译器实际上会使用常量整数参数优化pow,但不应该依赖它。

答案 1 :(得分:38)

您选择了一种最慢的方法来检查

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发射(下图)。

当条件的b < c部分变为false时,您还可以优化循环条件以摆脱循环,因此编译器只需执行该检查的一个版本。

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);   // gcc won't always transform signed-integer math, so this prevents hoisting (SUMTOTAL-a) :(
        int c = (SUMTOTAL-a) - b;
        // if (b >= c) break;  // just changed the loop condition instead

        // the compiler can hoist a*a out of the loop for us
        if (/* b < c && */ c*c == a*a + b*b) {
            // Just print a newline.  std::endl also flushes, which bloats the asm
            std::cout << "a: " << a << " b: " << b << " c: "<< c << '\n';
            std::cout << a * b * c << '\n';
        }
    }
  }
}

使用此内部循环编译(使用gcc6.2 -O3 -mtune=haswell)代码。请参阅the Godbolt compiler explorer上的完整代码。

# 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

在Intel Haswell上,所有这些指令均为1 uop。 (并且cmp / jcc将宏 - 融合到比较和分支的uops中。)这就是10个融合域uops,which can issue at one iteration per 2.5 cycles

Haswell运行imul r32, r32,每个时钟吞吐量为一次迭代,因此内部循环内的两次乘法不会使端口1饱和,每2.5c两次乘以。这留下了空间来吸收ADD和SUB窃取端口1的不可避免的资源冲突。

我们甚至没有接近任何其他执行端口瓶颈,所以前端瓶颈是唯一的问题,这应该在Intel Haswell及更高版本上每2.5周期运行一次

循环展开可以帮助减少每次检查的uop数量。例如使用lea ecx, [rbx+1]为下一次迭代计算b + 1,因此我们可以imul ebx, ebx而不使用MOV使其具有非破坏性。

强度降低也是可能的:鉴于b*b我们可以尝试在没有IMUL的情况下计算(b-1) * (b-1)(b-1) * (b-1) = b*b - 2*b + 1,也许我们可以执行lea ecx, [rbx*2 - 1]然后从b*b中减去它。 (没有寻址模式可以减去而不是添加。嗯,也许我们可以将-b保留在寄存器中,并向上计数到零,这样我们就可以使用lea ecx, [rcx + rbx*2 - 1]来更新b*b在ECX中,在EBX中给出-b

除非你真的遇到了IMUL吞吐量的瓶颈,否则这可能最终会带来更多的成功,而不是一场胜利。看看编译器在C ++源代码中实现这种强度降低的效果可能会很有趣。

您也可以使用SSE或AVX 对其进行矢量化,并行检查4个或8个连续的b值。由于命中率非常罕见,你只需检查8中的任何一个是否有一个命中,然后在极少数情况下找出匹配的那个。

另请参阅代码wiki以获取更多优化内容。