18 April 2015

GCC支持用__attribute__为变量、类型、函数、标签指定特殊属性。这些不是编程语言标准里的内容,而属于编译器对语言的扩展。 本文介绍其中的两个属性:aligned和packed。

aligned

aligned属性最常用在变量声明上。它的作用是告诉GCC,为变量分配内存时,要分配在对齐的内存地址上。什么是对齐的内存地址呢?

一般计算机的内存是以字节(byte,等于8bit)为最小单元的。内存地址相当于从0开始的字节偏移数。如果一个内存地址是N的倍数,我们就说它是N字节对齐的(N-byte aligned)。

对于C/C++中的基本数据类型,假设它的长度为n字节,那么该类型的变量会被编译器默认分配到n字节对齐的内存上。例如,char的长度是1字节,char类型变量的地址将是1字节对齐的(任意值均可); int的长度是4字节,所以int类型变量将被分配到4字节对齐的地址上。这种默认情况下的变量对齐方式又称作自然对齐(naturally aligned)。

但是,有时候我们希望改变这种默认情况。这时候就可以使用aligned属性了。例如:

int x __attribute__ ((aligned (16))) = 0;

告诉编译器把变量x分配在16字节对齐的内存地址上,而非默认的4字节对齐。

编译器之所以默认让变量自然对齐,是因为这种对齐情况下的内存访问是最高效的。受限于硬件实现,非对齐内存访问的性能会有所下降;甚至在有些处理器(如DSP、早期的ARM)上,非对齐的内存访问根本就不支持, 将直接引发错误。而我们之所以需要自己指定对齐方式,很多时候是程序优化的需求。例如,在x86平台上要使用SSE指令,所操作的数据在内存中就必须是16字节对齐的。

aligned不仅可以用作变量属性,还能用作函数属性和数据类型属性。它作为函数属性时的作用等价于对函数使用-falign-functions这一优化选项。 当它用作数据类型的属性时,相当于告诉编译器,这一类型的所有变量都要按指定字节数对齐。

aligned与结构体

结构体是一种数据类型,它与aligned有一些特殊的关系。上面提到,aligned可以用于指定数据类型的属性,因此可以用于结构体,例如:

struct __attribute__ ((aligned (8))) my_struct1 {
	short f[3];
};

但这么用不仅会影响为该结构体变量分配内存的位置,还可能影响其内存占用。在上面的例子中,这个结构体内有3个两字节的short,本来只需要占用6字节的内存; 但由于指定了8字节对齐,编译器会在该结构体尾部填充额外的2个字节,使得这个结构体的大小为8字节。为什么要填充多余的字节呢?因为只有将该结构体补足8个字节,才能保证在这个结构体类型的数组中,数组中每个元素 都是8字节对齐的(考虑到数组元素在内存中的连续存放)。

aligned也可以用在结构体的成员上,这时就成了对变量指定属性。GCC的相关文档里提到,C语言规定结构体类型必须至少对齐到其所有成员变量对齐字节数的最小公倍数, 因此指定结构体的对齐完全可以通过指定其中成员变量的对齐来实现,不过前者明显可读性好些。

当aligned属性用于数据类型(比如结构体)的时候,只能增加对齐字节数而不能减小。例如,下面的结构体本身大小有6字节,虽然指定了4字节对齐,但并不能达到目的,最终GCC还是会按8字节对齐处理。

struct __attribute__ ((aligned (4))) my_struct2 {
	short f[3];
};

如果要减小对齐字节数,需要用到下面介绍的packed属性。

packed

packed属性的主要目的是让编译器更紧凑地使用内存。当它用于变量时,告诉编译器该变量应该有尽可能小的对齐,也就是1字节对齐。当它用于结构体时 ,相当于给该结构体的每个成员加上了packed属性,这时该结构体将占用尽可能少的内存。例如:

struct __attribute__ ((packed)) my_struct3 {
	char c;
	int  i;
};

这个结构体被指定了packed,所以它的成员变量将是1字节对齐的,也就是说成员i将紧跟着成员c,从而使得该结构体的实际大小为5字节。 如果不指定packed,由于要满足成员i的4字节对齐要求(它是int型的),编译器将在成员c之后填充3个字节,使得这个结构体实际大小变为8字节。

采用packed属性虽然可以节省内存,但它会导致非对齐的内存访问。例如上述结构体的int型成员变量i,它的内存地址将不是4的倍数,访问它时就是非对齐访问。 当用.或->操作符存取结构体成员时,编译器会保证存取到正确的值;但如果用指针直接访问非对齐的成员变量,就只能指望处理器支持非对齐访问了,否则将会出错。 这也是很多人认为给结构体指定packed属性不太安全的原因。

什么时候用packed?

上面说到使用packed可能“不安全”,但为什么还要用呢?什么时候会需要它呢?

使用packed最重要的场合莫过于处理跟文件格式或网络协议有关的二进制数据了。这些格式或协议是不能容忍多余字节的,所以当用结构体表示其数据时,必须阻止编译器填充字节。 这正是packed的设计初衷。

另外还可以用packed来节省内存,不过这是以牺牲性能为代价的,不是什么好方法。通过适当排列结构体成员的顺序可以使得需要填充的字节数尽可能少,这才是应该考虑的措施。

我在最近的一个项目中需要从RGB像素生成Bitmap(位图)文件。Bitmap文件的头信息是用结构体描述的。 一开始写入磁盘的文件总是不对,分析发现正是因为编译器在我的结构体里填充了多余字节来满足对齐的要求。简单一个packed就解决了这个问题。

但正如前面所说,使用packed属性具有潜在的问题。如果一个结构体被packed了,尽量不要使用指向其内部成员变量的指针,除非你真的知道你在做什么。