背景
定点小数
就是小数位恒定的小数,在信号处理等领域应用广泛,它的表示格式类似于S1.7(有符号,整数部分1bit,小数部分7bit)、U0.8(无符号,没有整数部分,小数部分8bit)。
假如小数是0.5,按U0.8来表示的话,在内存里的整数值就是128,因为U0.8把整数1细分成2^8份,0.5 = 1/2 * 1.0 = 1/2 * 256 = 128
同理,如果内存里的整数是128,按U0.8来解析,则就是128 / (2 ^ 8) = 1/2 = 0.5
故障
最近发现公司某个算法工作异常,算法同事修复后,我看了下代码改动,就是给表示分数部分的1 << N
里的1前面加了个(unsinged int)
的强制类型转换。看不太懂这个操作,1本来就是整型啊,为啥还要强转成整型?
float int2float(int vin, unsigned int frac_bits) {
float vout = (float)vin / (float)(1 << frac_bits); // 1前面加(unsinged int)强转
return vout;
}
分析
猜想
想起来故障是算法计算S0.31定点小数时出现的,S0.31有什么特别的地方吗?有,它是有符号数,即最高位是符号位,所以上述函数的frac_bits参数如果传个31,则1 << 31
的结果就不是2147483648,而是-2147483648,后面就都错了。
验证猜想
阅读算法的代码,函数int2float
的功能应该是将输入的整数vin(定点小数在内存里是按整数存储的),除以小数部分的最大值1<<frac_bits
(因为在CPU里整数1被细分成2^frac_bits份),商就是CPU里的小数。
编写下列验证代码:
include<stdio.h>
#define FRAC_BIT (31)
float int2float(int vin, unsigned int frac_bits) {
float vout = (float)vin / (float)(1 << frac_bits);
return vout;
}
float int2float_ok(int vin, unsigned int frac_bits) {
float vout = (float)vin / (float)((unsigned int)1 << frac_bits);
return vout;
}
int main() {
printf("a = %f\n", int2float(3351969, FRAC_BIT));
printf("a = %f\n", int2float_ok(3351969, FRAC_BIT));
return 0;
}
输出结果:
a = -0.001561
a = 0.001561
3351969按照S0.31来换算的话,应该是0.001561的,但因为它除以负的分数部,导致算出来的定点小数变成了负值!
如果将FRAC_BIT从31减小成30,则问题就不会出现:
a = 0.003122
a = 0.003122
可以看到,两个int2float函数的输出结果一致。
所以这是个仅在左移31bit才会触发的符号相关BUG
为什么加个类型强转就能解决?
我猜测是强制分数部变成无符号的,确保分母一定是正数,这样才能正确反映定点数在内存里的表示(可能是负整数),至于实际差异在哪,还是得看汇编代码(以x86-64汇编为例):
可以看出,加了个类型强转后,汇编代码增加了11条指令的额外处理,里面有循环右移和条件跳转,比原版复杂多了,以后有时间再分析。
总结
有符号数一定要注意溢出问题。