uint64转double:为何右移1位后编译生成的汇编代码更简洁?
为何
AsDouble1的实现汇编代码远比AsDouble0简洁直白? 先看两个函数的C实现与对应编译产出的汇编:
; AsDouble0 对应汇编 AsDouble0(unsigned long): # @AsDouble0(unsigned long) movq xmm1, rdi punpckldq xmm1, xmmword ptr [rip + .LCPI0_0] # xmm1 = xmm1[0],mem[0],xmm1[1],mem[1] subpd xmm1, xmmword ptr [rip + .LCPI0_1] movapd xmm0, xmm1 unpckhpd xmm0, xmm1 # xmm0 = xmm0[1],xmm1[1] addsd xmm0, xmm1 addsd xmm0, xmm0 ret
// AsDouble0 C实现 double AsDouble0(uint64_t x) { return x * 2.0; }
; AsDouble1 对应汇编 AsDouble1(unsigned long): # @AsDouble1(unsigned long) shr rdi cvtsi2sd xmm0, rdi addsd xmm0, xmm0 ret
// AsDouble1 C实现 double AsDouble1(uint64_t x) { return (x >> 1) * 2.0; }
造成汇编长度差异的核心原因是x86-64的硬件指令限制,以及编译器对代码正确性的严格遵守:
- x86-64平台没有提供直接把无符号64位整数转成双精度浮点数的原生指令,通用整数转浮点指令
cvtsi2sd仅支持带符号64位整数的转换,一旦输入值超过2^63-1(带符号64位正数的上限),直接用该指令转换会得到错误的负数结果。 - 编译
AsDouble0时,编译器必须保证0~2^64-1全范围的uint64_t输入都能得到正确结果,无法直接使用cvtsi2sd,只能通过SSE指令做位操作模拟转换:先把64位整数拆分拼接成两个临时浮点值,再通过预存的魔数做偏移修正,最后合并结果再完成乘2运算,整套流程自然需要更多指令。 - 编译
AsDouble1时,代码先对无符号整数做了逻辑右移1位的操作,右移后的值范围直接缩小到0~2^63-1,刚好完全落在带符号64位整数的正数表示范围内,不存在转换溢出的问题。编译器识别到这个取值范围变化后,就可以直接调用原生cvtsi2sd完成转换,最后通过浮点加自身(等价于乘2)得到结果,不需要额外的修正逻辑,指令序列自然简洁得多。
不要觉得两个函数数学逻辑近似编译器就该生成一样的代码——所有编译器优化的前提是100%符合语言标准的正确性要求,不会为了缩短指令序列牺牲任何边界场景的计算准确性。
内容的提问来源于stack exchange,提问作者Etienne M
相关产品推荐
相关产品推荐

