2.3 紧缩
各编译器都支持结构的紧缩,即连续排列结构的各成员变量,各成员变量之间没有任何填充字节。这时,结构的大小等于各成员变量大小的和。紧缩结构的变量可以放在1n边界,即任意地址边界。
在gcc中可以这样定义紧缩结构:
typedef struct St2Tag {
St1 st1;
char ch2;
} __attribute__ ((packed)) St2;
armcc是这样的:
typedef __packed struct St2Tag {
St1 st1;
char ch2;
} St2;
VC的写法最麻烦:
#pragma pack(1)
typedef struct St2Tag {
St1 st1;
char ch2;
} St2;
#pragma pack()
如果要同时支持gcc、armcc、VC平台,可以把代码写成这样:
#ifdef __GNUC__
#defineGNUC_PACKED __attribute__((packed))
#else
#defineGNUC_PACKED
#endif
#ifdef __arm
#define ARM_PACKED __packed
#else
#define ARM_PACKED
#endif
#ifdef WIN32
#pragma pack(1)
#endif
typedef ARM_PACKEDstruct St2Tag {
St1 st1;
char ch2;
} GNUC_PACKED St2;
#ifdef WIN32
#pragma pack()
#endif
其中:__GNUC__是gcc的预定义宏,__arm__是ARM编译器的预定义宏(__arm和__arm__都可以),可以用它们识别当前的编译器。
2.4 全局设置
在VC中,有的程序员习惯设置整个工程的struct member alignment,这对应于命令行选项“/Zpi”,其中i=1,2,4,8,16。如果将这个值设为1,工程中所有结构都是紧缩排列。紧缩排列会增大代码量,降低结构访问效率。我们应该仅在必要的时候使用紧缩结构。
“/Zp1”是紧缩排列,那么“/Zp2”,“/Zp4”等选项是怎样排列的呢?
设选项“/Zpi”中设定的长度是i,设某个结构成员的基本长度是m,则该结构成员按照m和i中较小的值对齐。例如:如果我们设置了“/Zp2”,则基本长度不大于2的成员按照基本长度对齐,基本长度大于2的成员按照2对齐。
其实,我们不应该使用“/Zp2”这么奇怪的选项,除非有非如此不可的理由。
2.5 紧缩结构的用途
其实最常用的结构对齐选项就是:默认对齐和紧缩。在两个程序,或者两个平台之间传递数据时,我们通常会将数据结构设置为紧缩的。这样不仅可以减小通信量,还可以避免对齐带来的麻烦。假设甲乙双方进行跨平台通信,甲方使用了“/Zp2”这么奇怪的对齐选项,而乙方的编译器不支持这种对齐方式,那么乙方就可以理解什么叫欲哭无泪了。
当我们需要一个字节一个字节访问结构数据时,我们通常都会希望结构是紧缩的,这样就不必考虑哪个字节是填充字节了。我们把数据保存到非易失设备时,通常也会采用紧缩结构,既减小存储量,也方便其它程序读出。
2.6 细节
最后记录一个小细节。gcc编译器和VC编译器都支持在紧缩结构中包含非紧缩结构,例如前面例子中的St2可以包含非紧缩的St1。但对于ARM编译器而言,紧缩结构包含的其它结构必须是紧缩的。如果紧缩的St2包含了非紧缩的St1,编译时就会报错:
error: #1031efinition of "struct St1Tag"in packed "struct St1T2g"must be __packed
3 数据对齐
3.1 CISC和RISC
CPU从指令集的特点上可以分为两类:CISC和RISC。CISC和RISC分别是复杂指令集计算机(Complex Instruction Set Computer)和精简指令集计算机(ReducedInstruction Set Computer)的缩写。
CPU的工作可以看作以下步骤的反复循环:
step 1: 取指令
step 2: 取数据
step 3: 执行指令
step 4: 输出结果
CISC CPU支持很多寻址模式,因此取数据的时间是不确定的。RISC CPU的最大特点是简化了指令的寻址模式,除了Load/Store指令外,其它指令都采用寄存器寻址,即从寄存器读写数据。这种设计使取数据的时间相对稳定,可以简化指令流水线的设计。
一般而言,RISC架构可以降低CPU的复杂性以及允许在同样的工艺水平下生产出功能更强大的CPU,但对于编译器的设计有更高的要求。
3.2 对齐数据访问
RISC CPU的Load/Store指令要求数据是对齐的。长度为4的数据应放在4n边界上,长度为2的数据应放在2n边界上。以ARM CPU的Load为例:
LDR R5,[R4]
LDRSH R7,[R6]
LDRB R9,[R8]
LDR、LDRSH、LDRB分别从存储器读取一个字、半字和字节,放到指定寄存器。例如“LDR R5,[R4]”就是从R4指向的存储单元中读一个字(长度为4),放到R5中。 LDR要求数据地址在4n边界上,否则就会发生错误。LDRSH要求数据地址在2n边界上,否则就会发生错误。
发生什么错误呢?这与具体的CPU有关,在ARM7TDMI上,非对齐访问会导致程序跳到数据访问错误的处理向量,即地址0x00000010处。在ARM920T上,LDR指令可能返回错误的数据。 CISC的CPU支持非对齐的数据读取。
3.3 例子
我们来看一个例子:
// 例子1
void test(void) {
char a[] = {1,2,3,4,5};
int *pi, i;
printf("&a[1]=%pn",&a[1]);
pi = (int *)&a[1];
i = *pi;
printf("0xxn", i);
*pi = 0x11223344;
for(i = 0; i
{
printf("0xx ", a);
}
}
关键是这句: i = *pi; 和 *pi = 0x11223344; 我们知道地址pi指向的4个字节依次是:0x02,0x03,0x04,0x05。在小尾的CPU上,我们期待的输出是0x05040302和0x01 0x44 0x33 0x220x11。让我们看看这段代码在不同平台的运行效果。
3.3.1 PC/Windows
输出结果是:
&a[1]=0x0012FF250x05040302
0x01 0x44 0x33 0x220x11
符合我们的预期,也说明PC的CPU支持非对齐的数据读取。
|