字节对齐
此外,更为麻烦的是对于边界不对齐的 b,还得将其合成一个 4 字节(一部分是来自 于一个 4 字节中的 b0、b1 和 b2,另一部分来自于另一个 4 字节中的 b3),而这又增加了程 序的复杂性,即需要更多的指令来完成。
从以上分析可以看出,采用字节对齐能提高系统性能。而编译器在编译程序时,也会 根据需要选择不同的指令来完成对数据的存取操作。
main0.s
.file "main0.c" .section ".text" .align 4 .global main .type main,#function .proc 04 main: !#PROLOGUE# 0 save %sp, -256, %sp !#PROLOGUE# 1 add %fp, -152, %o0 mov 130, %o2 mov 0, %o1 call memset, 0
现在,我们开始分析采用字节对齐和不采用字节对齐时,CPU 对于内存的访问次数有 何不同。回到图 1,先看看采用字节对齐时的情况,从图中可以看出,当 CPU 需要分别访 问 a 变量和 b 变量时,无论如何都只需要分别进行一次内存存取,图中的花括号表示一次 内存存取操作。对于不采用字节对齐的情况,a 变量无论如何只要进行一次内存操作的, 而 b 变量有可能需要进行二次内存操作,因为这一变量跨越了 4 字节的边界。这里之所以 说有可能,是因为有可能对 b 进行访问之前,可能刚好完成了对 a 的访问,而对 a 访问时, b0、b1 和 b2 也同时读入(或写入)了,这种情况下,只需要读入(或写入)b3 即可。
回到 main0.c 我们可以分析出 main 函数中的 msg 变量是 4 字节边界对齐的,因此 msg.body 是边界不对齐的,其相对于 msg 的偏移是 2 个字节(其前面有一个 2 个字节的 mark 变量)。接着程序将 msg.body 强制转换成了 header_t 结构。最终结果是 pointer 也 是边界不对齐的,这违背了 SPARC 处理器中 ld 指令要求地址边界是 4 字节对齐的限制。 这就是为什么运行这一程序会出现“Bus Error”的原因。
typedef struct {
char a; int b; } type_t;
采用字节对齐时
a
0x0000
pad
pad
pad b0
0x0004
b1
b2
b3
不采用字节对齐时
a
0x0000
b0
b1
b2 b3
0x0004
图 1 type_t 结构的内存布局示意图
在做进一步的分析之前,还需要清楚的是,对于 32 位处理器,其数据总线是 32 位的。 因此,CPU 从内存中存取数据时可以(也只能)一次读入 4 个字节。为此,CPU 从内存 中存取数据时总是以 4 字节为边界进行存取的。如果,我们所写的程序只需要访问内存中 的一个字节,此时也需要从内存中读入 4 个字节吗?是的。对于一次内存所存取的 4 个字 节中,我们是需要存取其中的 1 个字节、2 个字节或是全部 4 个字节,CPU 如何区分呢? 答案是:CPU 提供了不同的指令,而由编译器根据情况选择使用不同的指令。
其中的 stb 指令表示向内存中写入一个字节,当然,这一指令对于存取地址并无边界 对齐的要求,也就不会产生“Bus Error”这种问题了。
在默认的情况下 GCC 采用字节对齐的技术来提高程序的效率,但是有时我们不希望 这种字节对齐处理,比如两个主机进行网络通讯时,我们不需望因为字节对齐而传送多余 的字节。在这种情况下,可以在结构之前加上#pragma pack(1)编译预处理命令,它将告诉 GCC 对其后的数据结构采用一个字节对齐技术进行处理。仍然值得一提的是,不采用字节 对齐将影响程序的运行效率。下面是一段程序采用对齐技术和不采用对齐技术时汇编代码 的对比。
下面的 main1.c 程序能正常的运行。
typedef struct {
short mark; char body[128]; } msg_t;
main1.c
int main () {
msg_t msg = {0}; msg.body[1] = 3; return 0; }
这是因为编译器知道 body[1]是一个字节,因此不会采用类似 ld 这样存取 4 字节的指 令,这可以从 main1.c 的汇编代码看出,如下所示。
为什么这么简单的一个程序在不同的操作系统(其实是处理器)上的运行结果却决然 不同?这其实是一个 CPU 字节对齐所引发的问题,下面我们通过对字节对齐问题的分析来 探究其背后的原理。后面的分析我们全部是针对运行在 32 位 SPARC 处理器上的 Solaris 操作系统进行的。
2 为什么要字节对齐
简单的说来就是为了提高 CPU 的性能,或者说是提高程序运行的效率。当然,在其背 后更有简化 CPU 设计的功效。因此,我们所写的 C 程序为了得到尽可能高的效率就必须 最大限度的满足 CPU 对于字节对齐的要求,编译器在这当中起着至关重要的作用。
main0.c
typedef struct {
short mark; char body[128]; } msg_t;
typedef struct {
char *pointer; } header_t;
int main ()
█1
C 语言中一个字节对齐问题的分析 { msg_t msg = {0}; void *p = ((header_t *)msg.body)->pointer; return 0; }
关键词
C 语言
参考资料
字节对齐
[1] The SPARC Architecture Manual v8
1 问题的引入
下面是一段被简化的程序,分别在 Windows(32 位的 x86 处理器)和 Solaris(32 位 的 SPARC 处理器)上编译和运行,其结果将完全不同。在 Windows 上程序运行正常,但 是在 Solaris 上程序运行会出错,并且会在终端上打印出“Bus Error”以及产生一个“Core dump”文件。
其中的 halfword 是指 2 个字节,word 是指 4 个字节,而 doubleword 是指 8 个字节。 这段话给我们的信息是:当使用 ld 指令从内存中读入一个 4 字节的字时,其地址必须是以 4 字节为边界对齐的。
前面说到 C 语言对于数据结构的对齐还有一个很重要的问题是:C 语言除了对结构进 行对齐外(从结构内部的角度),还需要将进行字节对齐处理过的结构变量(从结构的外部 角度)分配在以 4 字节为边界的地方才有意义,比如图 1 中的 type_t 变量如果不是放在 0x0000 地址(4 字节边界对齐)上,而是放在 0x0001 的地址上,则边界对齐的结构也会 变成边界不对齐。因此,我们可以推理,所有的全局变量或是局部变量,在内存中都应当 分配在 4 字节边界对齐的地方(这样的话编译器的设计最为简单),只有这样 C 语言(编 译器)对于边界对齐的处理才算是完整的。
nop ld [%fp-150], %o0 st %o0, [%fp-156] mov 0, %o0 mov %o0, %i0 nop ret restore .LLfe1: .size main,.LLfe1-main .ident "GCC: (GNU) 3.2.3"
其中需要注意的是 SPARC 处理器中的 ld 指令,这一指令从内存中读入一个 4 字节的
typedef struct {
char *pointer; } header_t;
{0}; void *p = ((header_t *)msg.body)->pointer; return 0; }
由于 header_t 结构中只有一个指针变量 pointer,指针是 32 位的,易于字节对齐。因 此,编译器对于所有 pointer 的访问都采用 32 位的存取指令。这可以通过查看 main0.c 的 汇编代码 main0.s 来验证,如下所示。
typedef struct {
char a; short b; } element_t;
下面我们来分析为什么进行字节对齐能提高运行效率。要对数据结构进行更为高效的
2█
C 语言中一个字节对齐问题的分析
操作,从 CPU 的角度来看就是尽可能减少 CPU 对内存的访问次数。对于 type_t 结构,其 内存布局如图 1 所示,需要指出的是 SPARC 是 big-endian 模式,图中的 b=b0b1b2b3。
对于 C 程序员,大部分情况下我们并不考虑字节对齐问题,这并不是说我们不需要考虑,而是 因为碰到这种问题的情况很少。一方面要在特定的处理器上,而另一方面和我们写的程序也有关系, 只有两个条件同时满足时问题才会出现。因此,结果给我们的感觉是“字节对齐与我无关”。
本文通过对一小段简单的代码在不同处理器上的运行结果引出对字节对齐问题的关注,同时对 其原因进行了分析。
下面的 C 程序编译后运行,在终端上将会打印出“size of type_t is 8”。为什么是 8 而 不是 5 呢?这是因为编译器考虑到了运行效率从而将 type_t 结构进行了 4 字节边界对齐的 处理。
#include <stdio.h>
typedef struct {
char a; int b; } type_t;
int main () {
printf ("size of type_t is %d\n", sizeof (type_t)); return 0; }
这里需要指出的是,编译器会根据具体的结构选择是 4 字节边界对齐还是 2 字节边界 对齐。比如,下面定义的 element_t 结构,其 sizeof 大小应当是 4,而不是 3,更不会是 8。