正是因为编译器会对结构进行打包,所以不同计算机上字长的变化,还导致了另一个问题。C编译器在字(word)的边界上对齐字长,当具有一个字长的数据后面紧接着一个较小的数据时,这种方法会产生内存空缺(不过也有例外,比如说当有足够多的小数据刚好可以填充一个字时)。
一些聪明的程序员在声明联合时,往往在其中会带有两个或更多的结构,其中一个结构刚好填充联合,另一个则可以用来从不同的角度来看待这个联合,参见例3(a)。假设这段代码是为16位字长的计算机所写,int为16位,long为32位,那么存取这个结构的代码将会得到正常的映射关系(如图1),而例3(b)中的代码也会按预期的那样工作。可是,如果这段代码一旦移植到另一台具有32位字长的计算机上时,映射关系就改变了。如果新计算机上的编译器允许你使用16位的int,那么字的对齐就会像图2所示了,或者如果编译器遵循K&R约定,那么int将会和一个字(32比特)一样长,对齐就如图3所示,在任一情况下,这都将导致问题。
![]() ![]() ![]() |
例3:结构打包和字对齐
(a)
| union parse_hdr { struct hdr { char data1; char data2; int data3; int data4; } hdr; struct tkn { int class; long tag; } tkn; } parse_item; |
(b)
| char *ptr = msgbuf; parse_item.hdr.data1 = *ptr++; parse_item.hdr.data2 = *ptr++; parse_item.hdr.data3 = (*ptr++ << 8 | *ptr++); parse_item.hdr.data4 = (*ptr++ << 8 | *ptr++); if (parse.tkn.class >= MIN_TOKEN_CLASS && parse.tkn.class <= MAX_TOKEN_CLASS) { interpret_tag(parse.tkn.tag); } |
在第一个情况中(图2),tag域不是像期望的那样线性拉长,而被填充了一些垃圾。而在第二个情况中(图3),无论是class还是tag域,都不再有意义,两个char值因为被打包成一个int,所以也都不再正确。再次强调,首先不要假设标准数据类型大小一样,其次还要了解它们是怎样被映射成其他数据类型的,这才是书写可移植代码的最好方法。
机器寻址特性
几乎所有的处理器都在字边界上以字为单位进行寻址,而且通常都为此作了一些优化。另有一些的处理器允许其他类型的寻址,如以字节为单位寻址、或在半个字边界上以半字为单位寻址,甚至还有一些处理器有辅助硬件允许在奇数边界上同时以字和半字进行寻址。
寻址机制在不同计算机上会有所变化,最快的寻址模式是在字边界上以字为单位进行寻址。其他方式的寻址需要辅助硬件,通常都会对内存访问增加了一些时钟周期。而这些过多的模式和特殊硬件的支持,是与RISC处理器的设计初衷背道而驰的,就拿克雷计算机来说,就只支持在字边界上以字为单位进行寻址。
在那些不提供多种数据类型寻址方式的计算机上,编译器可以提供一些模拟。例如:编译器可以生成一些指令,当读取一个字时,通过移位和屏蔽,来找到所想要的位置,以此来模拟在字中的半字寻址,但这会需要额外的时钟周期,并且代码体积会更大。
从这点上来说,位域的效率是非常低的,在以位域来取出一个字时,它们产生的代码体积最大。当你存取同一个字中的其他位域时,又需要对包含这个位域字的内存,重新进行一遍寻址,这就是典型的以空间换时间。
当在设计数据结构时,我们总是想用可以保存数据的最小数据类型,来达到节省空间的目的。我们有时小气得经常使用char和short,来存取位特域,这就像是为了节省一角钱,而花了一元钱。储存空间上的高效,会付出在程序速度和体积上隐藏的代价。
试想你只为一个紧凑结构分配了一小点的空间,但却产生了大量的代码来存取结构中的域,而且这段代码还是经常执行的,那么,会因为非字寻址,而导致代码运行缓慢,而且充斥了大量用于提取域的代码,程序体积也因此增大。这些额外代码所占的空间,会让你前面为节省空间所做的努力付之东流。
在高级数据结构中,特定的比特定位已不是必须的了,应在所有的域中都使用字(word),而不要操心它们所占用的空间。特别是在程序某些依赖于机器的部分,应该为字准备一个typedef,如下:
| /*在这台计算机上,int是一个字长*/ typedef word int; |
在高级结构中,对所有域都使用字有如下好处:
a.. 对其他计算机架构的可移植性
b.. 编译器可能生成最快的代码
c.. 处理器可能最快地访问到所需内存
d.. 绝对没有结构对齐的意外发生
必须也承认,在某些时候,是不能做到全部使用字的。例如,有一个很大的结构,但不会被经常存取,如果使用了数千个字的话,体积将会增大25%,但使用字通常会节省空间、提高执行速度,而且更具移植性。
以下是我们的结论:
书写跨平台移植的代码,其实是件简单的事情。最基本的规则是,尽可能地隐藏机器字长的细节,用非常精确的数据元素位大小来映射物理数据结构。或者像前面所建议的,为高级编程使用高级数据结构,而低级编程使用低级数据结构,当阐明高级数据结构时,对标准C的标量类型,不要作任何假设。



