Go中uint32转换为float32后整数部分与原值不等的原因
现象根本原因
这个问题的核心是IEEE 754标准浮点数的固定有效位长度带来的精度上限,和数值是不是整数没有关系。
浮点数的存储分为符号位、指数位、尾数位三部分,其中尾数位的长度直接决定了可以精确表示的数值范围:
- float32(单精度浮点数):共32位存储,包含1位符号、8位指数、23位尾数,加上规格化表示时隐含的整数最高位
1,总共只能表示24位有效二进制位,换算为十进制约7位有效数字。 - float64(双精度浮点数):共64位存储,包含1位符号、11位指数、52位尾数,加隐含最高位总共可表示53位有效二进制位,换算为十进制约15~17位有效数字。
uint32是32位长度的无符号整数,最大可表示2^32-1,需要32位二进制位才能完整精确存储:
- float64的53位有效位长度大于32位,所有uint32范围内的整数都可以被float64完整精确表示,因此转换后整数部分和原值完全一致。
- float32只有24位有效位,所有大于2^24(即16777216)的整数,都无法被float32精确表示,转换时必须对超出有效位长度的部分做舍入,自然会出现转换后的值和原整数不相等的情况。
为什么转换后差值可能大于1
浮点数的可表示值是离散的,且相邻两个可表示值的间隔随数值增大而指数级变大。
以测试用到的37亿左右的数值为例,这个值处于2^31 ~ 2^32区间,此时float32的指数位对应值为31,相邻两个可表示float32值的间隔为2^(31 - 23) = 2^8 = 256。也就是说,在这个数值区间里,float32只能表示256的整数倍的数,两个相邻可表示值差256,因此舍入后的结果和原数的差值最大可以到128,自然可能出现差值大于1的情况。
float32转换时的舍入规则
Go语言遵循IEEE 754标准的默认舍入模式:就近舍入,中间值向偶数舍入(银行家舍入),具体逻辑为:
- 先将待转换的整数转为规格化二进制形式,即
1.xxxx * 2^e的格式 - 保留小数点后23位作为float32的尾数,超出23位的部分作为待舍入段
- 比较待舍入段的值和最低有效位权重的1/2:
- 如果待舍入段的值小于1/2最低有效位权重:直接舍去后面的位,向下取最近的可表示值
- 如果待舍入段的值大于1/2最低有效位权重:尾数加1进位,向上取最近的可表示值
- 如果待舍入段的值恰好等于1/2最低有效位权重(即刚好在两个可表示值的正中间):将尾数的最后一位凑为偶数(0)
测试用例验证
第一个测试用例(原值0xdeadbeef = 3735928559)
二进制展开为1101 1110 1010 1101 1011 1110 1110 1111,规格化后为1.10111101010110110111110 11101111 * 2^31:
- 小数点后前23位为
10111101010110110111110,作为保留的尾数 - 待舍入段为
11101111,对应十进制值239 - 当前区间最低有效位权重为256,1/2权重为128,239>128,因此向上进位
- 进位后尾数变为
10111101010110110111111,后面补8个0,最终值为1101 1110 1010 1101 1011 1111 0000 0000即3735928576,和原值差17,和运行结果一致。
第二个测试用例(原值0xdeadbeef-238 = 3735928321)
注:给出的二进制对应关系写反了,3735928321最后8位是00000001,3735928320最后8位是00000000
原值规格化后为1.10111101010110110111100 00000001 * 2^31:
- 小数点后前23位为
10111101010110110111100,作为保留的尾数 - 待舍入段为
00000001,对应十进制值1,小于128,因此直接舍去 - 舍去后尾数不变,后面补8个0,最终值为
1101 1110 1010 1101 1011 1110 0000 0000即3735928320,和原值差1,和运行结果一致。
内容的提问来源于stack exchange,提问作者yoer k.
相关产品推荐
相关产品推荐

