#include "stdio.h"
union float_int{
unsigned int i;
float f;
};
float float_pow2(float n, int pow){
union float_int u;
u.f = n;
unsigned int exp = ((u.i>>23)&0xff) + pow;
u.i = (u.i&0b10000000011111111111111111111111) | (exp<<23);
return u.f;
}
int main(){
float n = 3.14;
float result = 0;
for(int i = 0; i < 1000000000; i++){
// Uncomment one of the four
// result += n/2.01f;
// result += n/2.0f;
// result += n/2;
result += float_pow2(n,-1);
// Prevent value re-use
// n == 100003.14 by the time the loop ends, and the result will be in float range
n += 0.01f;
}
printf("%f\n", result);
}
2条答案
按热度按时间lrpiutwd1#
如果你要乘以或除以一个常数,一个中等质量的编译器应该优化它。在许多平台上,硬件乘法指令可能是最佳的。
对于乘以(或除以)2的幂,
std::ldexp(x, p)
将x
乘以2p
,其中p
是int
(如果p
取反,则除以)。我不希望在大多数平台上比简单乘法有太大的好处,因为手动(软件)指数操作必须包括上溢和下溢检查,因此在大多数情况下,所得的指令序列不太可能比硬件乘法有所改进。ffdz8vbo2#
TL;DR
使用带有
/
运算符的默认除法长应答
我写了一个浮点2次幂乘法/除法的半工作按位实现(不考虑NaN,Infinity和0,正如Peter Cordes指出的那样),但发现它的性能仍然比2次幂除法的原生
/
略差,尽管比非2次幂除法好。这表明GCC对2的乘方进行了某种优化,我们可以通过查看它在Godbolt上生成的x86-64程序集来证实这一点。
对于2的幂,
1/2.0f = 0.5f
可以精确地表示而不会引入任何舍入误差,因此n/2.0f
完全等价于n * 0.5f
。GCC知道即使没有-ffast-math
也可以安全地进行优化。Binary floating-point对mantissa * 2^exp
使用以2为底的指数,因此2的幂值(包括像0.5 = 2^-1
这样的负幂)可以精确地用1.0尾数表示。性能
代码使用GCC
-O3
编译。如果没有优化,编译器将不会内联float_pow2
,并且将在每个语句之后存储/重新加载,因此自定义函数的性能将更差,因为它是用多个语句完成的。内置除法性能,除数为2.01f(x86
divss
)内置2.0f除数除法性能(x86
mulss
)内置除数为2的除法性能(x86
mulss
)自定义除法性能(float_pow2)
(GCC将数据复制到整数寄存器并返回,而不是使用SSE 2向量整数数学指令。)
关于准确度,在进行的10次测试中,最后一次测试的标准偏差为0.018秒。其他人似乎落在类似的范围内的一致性。
2.0f
和2.0
的性能几乎相同,根据godbolt.org,它们确实被编译成了同一个程序集。对基准测试实际测量的内容进行性能分析(本节由@Peter Cordes撰写):
基准测试测量
float
加法的延迟,或者循环体的总吞吐量成本(如果更高的话)。(例如,如果编译器无法优化除法到乘法,请参阅Floating point division vs floating point multiplication)。或者使用
float_pow2
,在Intel CPU上会很复杂:https://uica.uops.info/对GCC的asm循环的Skylake和Ice Lake的预测(从Godbolt复制/粘贴)非常接近测量结果:对于float_pow2
循环,每次迭代约4.9个循环,而对于4.0c用于/= 2
(aka*= 0.5f
)循环。4.9c / 4c = 1.21的性能比非常接近0.968s/0.798s = 1.21它的分析表明,它不像
divss
那样是一个吞吐量瓶颈(如果执行单元不必等待输入准备就绪,它可以在每次迭代2.25个周期内运行那么多的工作)。理论上,仅两个addss
依赖链就有4个周期长。(Skylake具有2个/时钟FP add/穆尔吞吐量,具有4个周期延迟。但是在某些周期中,当
addss
准备执行时,它必须等待一个周期,因为不同的uop是oldest one ready for that execution port。(可能是因为movd eax, xmm0
和addss xmm0, xmm2
都在等待相同的XMM 0输入,即上一次迭代中addss xmm0, xmm2
的结果。这是n += 0.01f
。在这些addss xmm0, xmm2
uop中,它们被调度到端口0,在那里它们遇到了这种资源冲突,这延迟了关键路径依赖链上的进程,n += 0.01f
)因此,我们在这里真正测量的是由
float_pow2
的额外工作所产生的资源冲突,这些额外工作会干扰两个FP添加延迟瓶颈。如果加法运算在输入准备就绪后没有立即运行,则无法弥补损失的时间。这是因为它是延迟瓶颈,而不是吞吐量瓶颈。使用多个n1 += 0.02f
/n2 += 0.02f
等展开可以避免这种情况,但编译器不能在没有-ffast-math
的情况下这样做,因为它会引入不同的舍入误差。mulss
仅是一条指令,理论上可能会产生相同的瓶颈,但在这种情况下,uop调度往往会奏效,因此它不会从关键路径窃取周期。顺便说一句,由乘法(或其他一些操作)连接的两个加法链的依赖模式与Latency bounds and throughput bounds for processors for operations that must occur in sequence相同