通常采用字节、字、双字和四字数据类型时CPU的运行效率最高 ,但有时我们使用的数据不见得刚好是8位、16位、32位或64位的。这种情况下,可以将不同的位串进行打包(Packing)以节省内存,避免为了将特定数据字段和字节或其他数据长度对齐而产生的浪费。
以日期格式04/02/01为例。需要三个数值来表示日期:月、日和年。月用到了1~12的值,至少需要4位才能表示。日用到了1~31的值,需要5位表示。年(假设用到了0~99之间的值)需要7位。三个值总共需要16位(4+5+7),即2个字节。这样可以将日期数据打包为2个字节,如果每个值都使用一个字节则需要3个字节。对于每个需要存储的日期,可以节省1个字节的内存,如果需要存储的日期数据很多,节约的内存将非常可观。可以按照图3-6所示来排列位。
图3-6 短打包日期格式(16位)
MMMM 4位代表月,DDDDD5位代表日,YYYYYYY 7位代表年。表示一个数据的一组位称为一个位字段(Bit Field)。我们可以用$4101表示2001年4月2日:
打包值的空间效率很高(即内存占用很少),但计算效率却很低(很慢!)。原因在于需要额外的指令从各个位字段中解压缩打包数据。执行额外的指令需要时间(还要使用额外的字节来保存这些指令)。因此,要仔细思考打包数据字段是否值得。下面这段 HLA/x86代码展示了16位日期格式的打包和解包。
别忘了还有Y2K 问题,只用两位数字表示年份会有问题。图3-7中的日期格式更好。
图3-7 长打包日期格式(32位)
32位变量的位数比保存日期所需的位数要多得多,即便可以表示0~65535这么多年份,这种格式还能给月字段和日字段各分配一个完整的字节。应用程序可以将这两个字段当作字节对象进行操作,这样在仅支持字节访问的处理器上能够避免字段打包和解包的开销。还会压缩年字段占用的位,但是65536年已经足够了(就算从现在开始,我们的软件也不可能用63000年)。
有人可能会说这不是打包日期格式。毕竟,最终需要三个数值,其中两个正好是1字节,第三个值至少需要2个字节。这种“打包”日期格式与未打包的版本一样都用了4个字节,而不是最少的位。因此,这里的打包实际上指的是包装(Packaged)或封装(Encapsulated)。将数据打包成一个双字变量,程序就可以将日期当作单个数据而不是三个独立变量来处理。这意味着大多数时候操作日期数据只会用到一条机器指令,而不是三条独立的指令。
长打包日期格式和图3-6中的短日期格式之间还有一个区别:将Year、Month、Day字段重新进行了排列。这样可以简单地使用无符号整数来比较两个日期。例如下面这段HLA/汇编代码:
如果不同的日期字段被保存在不同的变量中或者以不同的方式组织,那么就不能这样简单地比较Date1和Date2了。即使没有节省多少空间,但打包数据让某些计算变得更加简单、更加高效了(这一点与打包数据的常见情况不同)。
一些高级语言内置了对打包数据的支持。例如,在C语言中可以定义如下结构体:
在此结构体中声明的每一个字段都是无符号对象,分别是4位、8位、4位、8位和8位。每条声明语句后的: n 指定了编译器为指定字段分配的最小位数。
可惜无法展示C/C++编译器是如何将32位双字的值分配给这些字段的,因为C/C++编译器的实现者可以随意使用自认为合适的方式来实现位字段分配。位串的位排列是任意的(例如,编译器可以把最终对象的第28~31位分配给bits0_3字段)。编译器还可以在字段之间插入额外的位,或者让每个字段占用更多的位(实际效果和在字段之间插入额外的位是一样的)。大多数C编译器都会尽量不增加多余的位,但是不同的编译器做法不同(尤其是在不同的CPU上)。因此,几乎可以确定C/C++结构体的位字段声明是不可移植的,编译器处理这些字段的方式是不可预期的。
使用编译器内置数据打包功能的优点是,数据的打包和解包都由编译器自动完成。以下面这段C/C++代码为例,编译器将自动生成必要的机器指令来存储和获取各个位字段: