“0x80是负0,其实就是0,因为二进制最高位是符号位,为1表示这个数字是负数。”
“你想用十六进制表示-2是吧?那-2加上一个256等于254,254就是0xFE,所以-2就是0xFE”
师弟如是说到。
于是我问他一句:“int8_t可以表示的最小的负数是-128,你怎么表示呢?”
师弟在草稿纸上边写边说:“-128+256=128, 128用十六进制表示是0x80,所以……啊这……这不是负0吗”师弟终于说不下去了。
本来和师弟在讨论某个网络在量化过程中由于出现负向饱和造成误差的问题,结果师弟这一番言论直接震惊到我了。他一直认为0xFF是int8_t能表示的最小的负数,一直认为0x80是“负0”,一直采用“负数+256”的方式得到十六进制的表示……
师弟呀师弟,名校硕士毕业、两家公司、两年多工作经验,那么多次面试笔试,咋就拦不住你呢
其实关于有符号和无符号数,Jungle在老东家工作的时候就碰到过由其引发的bug(由“有符号数”和“无符号数”引发的一个bug!)。这篇文章介绍了关于有符号数与无符号数的基础知识:
以int8_t和uint8_t为例,分别表示有符号的8位整型和无符号的8位整型。
对无符号数uint8_t:
对有符号数int8_t:
C语言允许在各种?的数字数据类型之间强制转换,把一个有符号数赋给一个无符号数(或者反过来),结果是各个位不变,但会改变解释这些位的方式。
如上图,同样是0xB3,如果是int8_t,则为-77;如果是uint8_t,则是179。
项目中稍不注意,则可能碰上由有符号数和无符号数引起的问题。比如两个正数相加或者相乘,结果却为负数或者结果反而更小:
#include <stdint.h>
#include <stdio.h>
int main(int argc, char* argv[])
{
int8_t a = 200;
int8_t b = 2;
int8_t c = a * b;
printf("c = %d\n", c);
uint8_t d = 240;
uint8_t e = 100;
uint8_t f = d + e;
printf("f = %d\n", f);
return 0;
}
上面的代码输出:
c = -112
f = 84
这些看起来简简单单的基础知识,却可能引发难以定位的bug、程序crash甚至更严重的灾难。除了上文提到的指纹算法中的问题,最近我们在调试对齐神经网络中的参数,发现某个节点的output和算法组给出的参数对不上,最后定位到原因是算法组是用int64保存的中间结果进行计算,而我是用int32饱和处理后再计算。在压测某个转换函数时,发现代码会概率性crash,原来是数值超出了类型所能表示的范围,溢出后的数早已不是设想的数值。
也许你会想,只要我用int64或者uint64就好了呗!表示的范围足够大了!
如果这样,只怕是有的面试笔试会拦住你了。压缩可执行程序大小、减少网络传输字节数量、压缩权重范围、神经网络量化……各式各样的应用都追求更小的size。以加快程序运行速度、减少占用的存储空间。掌握好这些基础,才能减少犯错的概率,即便偶然出错,也能很快从原理上定位修复bug。