结果也应该是整数吗? x^-n 只是1/x^n,对于除1 之外的任何x,它都会舍入为零。例如pow(16, -2) 是 1/256。
对于整数返回值,只需检查正数n 或返回1 或0。对于 FP 返回值,您可以使用绝对值的无符号实现,并有条件地取倒数。
对于较大的n 幅度,您可能希望使用基于 FP exp/log 的实现(请参阅我的 cmets 和 How can I write a power function myself?),而不是循环位实现。
对于具有无符号指数(或有符号正数)的纯整数,使用通常的指数右移算法并将结果相乘,可以实现一个不错的无分支实现
如果当前位已设置。 (请参阅https://eli.thegreenplace.net/2009/03/21/efficient-integer-exponentiation-algorithms 了解算法背后的数学原理和 Python 代码。)
我们可以使用shr 右移和 CMOV 对移出的位,并循环分支剩余的值。
这个版本在 x86-64 System V 相同的寄存器中传递 args,但它可以在 32 位模式下进行汇编。您当然可以将其调整为您喜欢的任何调用约定;它需要 4 个寄存器,因此您可能需要在 32 位调用约定中保存/恢复调用保留的 reg。
它与直接移植 Python 实现的 x86-64 C 编译器类似,但比它更好。 (https://godbolt.org/z/L9Kb98 gcc / clang 用 test sil,1/cmov` 构造循环,与 shr 结果上的循环分支分开。)
;; untested
; integer power
; args in EDI,ESI (like the x86-64 System V calling convention)
; returns in EAX
; clobbers: EDX, EDI, ESI
ipown: ; (int a (EDI), unsigned n (ESI))
mov eax, 1 ; res = 1
test edi,edi
jz .zero_exponent
.loop:
mov edx, eax ; tmp = res
imul eax, edi ; res *= a (will be overwritten with old res if n&1 == 0)
imul edi, edi ; a*=a
shr esi, 1 ; n>>=1 setting ZF according to result, and CF= bit shifted out (old_n&1)
cmovnc eax, edx ; res = tmp if the bit was zero so we don't do res *= a this time
jnz .loop
.zero_exponent:
ret
在 Broadwell 或更高版本的 Intel CPU 或 AMD Ryzen 上,我们有 1 个周期 CMOV 和 3 个周期延迟imul,这有望在每次迭代中运行 4 个周期(imul -> 通过 EAX 的 cmov 依赖链)。
imul 在现代 x86 上完全流水线化(或至少在 AMD Bulldozer 系列上充分流水线化),但每个时钟的吞吐量仅为 1,因此两个 imul 指令之间存在潜在的资源冲突,可能都在等待edi 准备好。但是通过 EDI 的 3 周期 dep 链应该领先于 4 周期 imul/cmov 链,因此在 imul eax,edi 和 imul edi,edi 都准备好开始的任何周期中,最旧的就绪优先调度应该使正确的选择并启动imul eax,edi。
注意mov edx,eax 不在关键路径:它可以与imul 并行运行。如果我完成了tmp *= edi,mov 将处于关键路径上,并且会在不消除整数寄存器的情况下影响 CPU 的延迟。
当然,max loop trip-count只有32(如果设置了指数的高位),所以乱序执行可以看穿这个直到循环结束(希望能解决循环-在乘法到达那里之前退出错误预测)。
循环中的指令很少(与其吞吐量相比),因此它应该能够与之前/之后的独立指令显着重叠。
预期延迟约为4 cycles *trip_count = 4 * log2(n),即4 * 指数中最高设置位的位置。
对于这个的 FP 版本,x87 实际上可能对fcmov 很感兴趣。否则,您可能会使用移位和 SSE4 blendvps 根据另一个寄存器的高位进行混合。 0.0 是加法恒等式,而不是乘法恒等式,因此与比较结果进行“与”运算不仅有效。