两种平方根倒数表达式的性能差异探究(考虑CPU流水线)
这是个特别接地气的性能优化疑问,我来结合现代CPU的实际执行逻辑拆解一下:
先明确两个表达式的核心差异
我们先把数学上等价的两个表达式摆出来:
- 写法A:
1 / sqrt(x) - 写法B:
(1 / x) * sqrt(x)
你的初始思路逻辑上没大问题:写法A里除法必须等开方结果,是严格的写后读依赖(RAW),总延迟理论上是sqrt延迟 + 除法延迟;写法B里1/x和sqrt(x)理论上没有依赖,可以并行执行,总延迟应该是max(sqrt延迟, 除法延迟) + 乘法延迟,看起来应该更快。但实际测试却反过来,问题出在这几个关键地方:
1. 现代CPU中FP除法和开方的“特殊”执行逻辑
你提到的FP除法、开方的10-20+周期延迟是对的,但要注意:这类指令不是完全流水线化的,或者说它们的流水线深度和吞吐率和乘法这类简单指令完全不同。
比如Intel Skylake及以后的CPU,FP除法/开方单元的吞吐率是每2-3周期才能启动一个新指令,而乘法是每周期能启动1-2个。这意味着,即使写法B里1/x和sqrt(x)可以并行启动,它们也会抢占同一个执行单元的资源——现代CPU的FP除法/开方单元通常只有1个,而乘法单元有多个。所以这两个指令其实没法真正“并行执行”,反而会因为争抢同一个单元,导致实际延迟并没有你预期的那么低。
2. 写法A的编译器优化空间更大
编译器对1 / sqrt(x)这类常见表达式的优化支持更成熟:
- 很多编译器会直接把它映射到更高效的指令序列,甚至在某些场景下会用近似算法(比如著名的Fast InvSqrt的现代变体),而写法B因为是“非标准”的等价写法,编译器可能不会做特殊优化。
- 另外,写法A的指令序列更短:只有开方+除法两步,而写法B多了一步乘法,虽然乘法延迟低,但它是额外的指令,会占用前端解码、分派的资源,在循环密集的场景下(比如你测试的1到1G的循环),前端压力也会影响整体性能。
3. 精度带来的隐性影响(虽你暂不考虑,但会间接影响性能)
写法B的精度损失比写法A大:1/x会放大x的精度误差,再乘以sqrt(x)后,误差会进一步累积。有些编译器会因为检测到写法B的精度问题,自动插入额外的精度校正指令,或者不会启用某些激进的优化,这也会悄悄拉低性能。
什么时候写法B可能更快?
其实也不是完全没有适用场景:
- 如果你的CPU有独立的FP除法单元和开方单元(部分服务器级CPU可能有),那写法B的并行性就能真正发挥出来,此时总延迟会接近你预期的
max(sqrt延迟, 除法延迟) + 乘法延迟。 - 另外,如果x的取值范围非常特殊(比如都是2的幂次),编译器可能会对写法B做特殊的位运算优化,此时性能可能反超。但这种场景非常小众。
关于你提到的“既有sqrtsd指令又有sqrt函数调用”的小疑问
你说编译后既有sqrtsd指令又有sqrt函数调用,这大概率是编译器的“保守优化”:在某些边界情况(比如x为0、NaN、无穷大)下,sqrt函数会处理这些特殊值,而sqrtsd指令本身不会做错误处理。编译器为了保证语义正确,会在循环外做一次特殊值检查,然后循环内用sqrtsd指令,所以你会看到两种形式同时存在——这部分其实不影响循环内的核心性能。
总结
写法A之所以更快,核心是因为现代CPU的FP除法/开方单元的硬件限制(单单元、低吞吐率),加上编译器对标准表达式的优化更到位。你的初始思路的理论模型忽略了硬件执行单元的资源限制,所以和实际结果有偏差。
内容来源于stack exchange




