有符号定点小数的31bit问题

背景

定点小数就是小数位恒定的小数,在信号处理等领域应用广泛,它的表示格式类似于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<(因为在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汇编为例):
有符号定点小数的31bit问题_第1张图片
可以看出,加了个类型强转后,汇编代码增加了11条指令的额外处理,里面有循环右移和条件跳转,比原版复杂多了,以后有时间再分析。

总结

有符号数一定要注意溢出问题。

你可能感兴趣的:(c语言)