谈谈一个重要的计算机概念,大家可能都听说过它,但是很少深究,那就是字节序(Endianness)。
字节序指的是,多字节数据的内存排列顺序。这样说比较抽象,使用图形解释就很好懂。
内存好比一排房间,每个字节是一间房。每间房都有门牌号(内存地址),从0号开始,然后是1号、2号......
0号字节的地址小,称为低位内存;3号字节的地址大,称为高位内存。
现在有一个数值abcd要放进这些房间,每个房间放一个数字,那么有两种放法。
第一种放法是,第一位a放在低位地址(0号),最后一位d放在高位地址(3号)。
这种排列称为"大端序"(big-endian,简称 BE),即大头在前,因为a是abcd的大头(最重要的数字)。
第二种放法是,第一位a放在高位地址(3号地址),最后一位d放在低位地址(0号地址)。
这种排列称为"小端序"(little-endian,简称 LE),即小头d在前。
大端序和小端序合称字节序,这两个名字来自18世纪的英国小说《格列佛游记》。某国分成两派,一派认为鸡蛋应该从大头吃起,称为"大端派";另一派认为,鸡蛋应该从小头吃起,称为"小端派"。两派相执不下,谁也无法说服谁,最后甚至为此交战。
对于人类来说,不同字节序的可读性是不一样的。大部分国家的阅读习惯是从左到右阅读。
大端序的最高位在左边,最低位在右边,符合阅读习惯。所以,对于这些国家的人来说,从左到右的大端序的可读性更好。
但是现实中,从右到左的小端序虽然可读性差,但应用更广泛,x86 和 ARM 这两种 CPU 架构都采用小端序,这是为什么?
或者换一种问法,两种不同的字节序为什么会并存,统一规定只使用一种,难道不是更方便吗?
原因是它们有各自的适用场景,某些场景大端序有优势,另一些场景小端序有优势,下面就逐一分析。
小端序优势最明显的,大概就是检查奇偶性,即通过查看个位数,确定某个数字是奇数还是偶数。
以123456为例,大端序从左到右排列,计算机必须一直读到最后一位的个位数6,才能确定这是偶数。
小端序是从右到左排列,个位数在第一位。所以,只要读取第一位,就能确定它是偶数。
一个类似的场景是检查正负号,确定一个数是正数还是负数。
大端序的符号位在左边第一位,小端序的符号位在右边最后一位。所以,大端序有优势,只看第一位就能知道是不是负数。
下一个操作是比较大小。现在有三个数字,需要比较大小:43662576,594,2。
上图是大端序排列,因为是从左到右排列,所以三个数字在右边个位数对齐。比较大小时,计算机就不得不读取每一个数的所有位,直到个位数,再进行比较。
如果改成小端序,就是下面的排列方式。
小端序是从右到左,所以三个数字在第一位对齐。计算机就不需要读取所有位,哪个数字先读不到下一位,就是最小的。比如,2这个数字就没有第二位,所以读到第二位时,就知道它是最小的。
所以,比较大小时,小端序有优势。
接下来,再看乘法操作。
乘法是逐位相乘,每一轮乘法都要向前进位。
上图是大端序的24165乘以3841。大端序的乘法是向左进位,也就是向左边扩展,必须等到每一轮的结果都出来(上例是四轮),再相加统一写入内存。
如果改成小端序的乘法,就不需要等待下一轮的结果,每一轮都可以直接写入内存。
上图是小端序的24165乘以3841。小端序的乘法是向右进位,也就是向右边扩展,左边的边界不变。每一轮结果写入内存后,就不需要移动,后面有变化只需要改动对应的位就行了。
因此,小端序的乘法有明显优势。
上一个例子的从低位开始计算的特性,对于任意精度整数特别有用。任意精度整数又称大整数,可以存放任意大小的整数。
它的内部实现是把整数分成一个个较小的单位,通常是 uint32(无符号32位整数)或 uint64(无符号64位整数),按顺序组合在一起。
如果是大端序,第一个 u64 就是这个整数最大的部分。运算时,一旦这个数发生变化,需要进位,后面的所有位都必须移动和改写。小端序发生进位时,往往就不需要所有位移动。
小端序的另一个好处是,如果逐字节的运算从个位数开始(比如乘法和加法),可以从左到右依次运算一个个 u64,算完上一个再读取下一个。大端序就不行,必须读取整个数以后再进行运算。
最后一个例子是,C 语言有一种 cast 操作,可以强制改变变量的数据类型,比如把32位整数强行改变为16位整数。
上图中,32位整数0x00000001更改为16位整数0x0001,大端序是截去前面两个字节,这时指向这个地址的指针必须向后移动两个字节。
小端序就没有这个问题,截去的是后面两个字节,第一位的地址是不变的,所以指针不需要移动。
综上所述,大端序和小端序各自的优势如下。
如果需要逐位运算,或者需要到从个位数开始运算,都是小端序占优势。反之,如果运算只涉及到高位,或者数据的可读性比较重要,则是大端序占优势。