C - 字对齐那些事儿

为什么需要字对齐

  • 硬件原因

    现代计算机中内存空间是按照 byte 划分的。

    从理论上讲:对任何类型的变量的访问都可以从任何地址开始。

    但实际情况是:计算机并非逐个字节读写内存,而是以 一定的字节数 来读写内存。

    如此一来就会对基本数据类型的合法地址作出一些限制。那么就要求各种数据类型按照一定的规则在空间上排列,这就是 对齐 1

    而当某类型的变量所在的地址恰好为其他类型长度的整数倍,则称为 自然对齐

    某些硬件平台只能从某些地址处取某些特定类型的数据,例如否则抛出 硬件异常 2

  • 性能原因

    字节对齐可以 提高CPU访问数据的效率,举个栗子:

    存在一整型变量(假设其大小为4byte),存储的地址为 0x00000001

    在以 4byte 为最小读取单位的机器上,读取该变量需要两次,第一次读取 0x00000000 - 0x00000003 ,第二次读取 0x00000004 - 0x00000008

    若该变量存储地址为 0x00000004,则只需读取一次:0x00000004 - 0x00000008

对齐原则

标准数据类型

前面举了一个整型(标准数据类型)的例子,可以发现其地址只要是其存储大小的整数倍即是对齐的。

示例 3

int main(){

    char a = 'a'; 
    int  b = 1; 
    int* c = &b;
    double d = 3.14159265;
    int e = 2;
    
    
    printf("Addr:%p,sizeof(a):%lu \n",&a,sizeof(a));
    printf("Addr:%p,sizeof(b):%lu \n",&b,sizeof(b));
    printf("Addr:%p,sizeof(c):%lu \n",&c,sizeof(c));
    printf("Addr:%p,sizeof(d):%lu \n",&d,sizeof(d));
    printf("Addr:%p,sizeof(e):%lu \n",&e,sizeof(e));

    system("PAUSE");
    return 0;
}

运行结果
在这里插入图片描述
图示
在这里插入图片描述
发现

  • 其地址均为基本数据类型大小的整数倍;

  • 内存地址并不是紧密挨着啊;

  • 栈上各变量申请的内存,返回的地址是这段连续内存的最小的地址。

    ps:本人系统为 64bit Windows10 ;开发环境是 VSCode ;编译链是 gcc version 8.1.0
    至于 gcc 的默认对齐,依赖于 ABI。绝非广为流传的 4字节对齐 4。不信各位可以自己试试。

非标准数据类型

那么对于非标准数据类型,诸如结构体、数组,又是如何对齐的呢?

  • 数组 :按照基本数据类型对齐,第一个对齐了后面的自然也就对齐了。

  • 联合 :按其包含的长度最大的数据类型对齐。

  • 结构体:结构体中每个数据类型都要对齐 5

    ps:
    自身对齐值 = max{各成员变量对齐值}
    指定对齐值:#pragma pack (value) 指定对齐值value。
    有效对齐值 = min{自身对齐值,当前指定的pack值}
    6

结构体

示例

int main(){

	struct Test_Aligned
	{
    	char a;
    	int b;
    	int c;
	};
    
    printf("sizeof(f):%lu\n",sizeof(struct Test_Aligned));
    
    struct Test_Aligned a;
    printf("a.a addr = %p\n",&a.a);
    printf("a.b addr = %p\n",&a.b);
    printf("a.d addr = %p\n",&a.c);

    system("PAUSE");
    return 0;
}

结果及图示
在这里插入图片描述
在这里插入图片描述
可以发现,结构体中的每个变量仍然符合标准数据类型的规律, 其地址均为成员变量大小的整数倍 。且结构体中的变量在内存中是连续存储的。

当我们把结构体改为

	struct Test_Aligned
	{
    	char a;
    	int b;
    	short c;
	};

其结果为:
在这里插入图片描述
在这里插入图片描述
再修改为以下结构体:

	struct Test_Aligned
	{
    	char a;
    	int b;
    	char c;
	};

其结果为:
在这里插入图片描述
多次实验得:当结构体总大小非 有效对齐值 的整数倍时,将向后扩充以满足该要求。

例如 :

struct Test_Aligned
	{
    	char a;
    	int b;
    	short c;
	};

有效对齐值为 4;因此最后会填充 2byte,使结构体总大小为 12 ,满足被 4 整除的条件 。

再例:

struct Test_Aligned
{
    char a;
    double b;
    short c;
};

结果:
在这里插入图片描述
小结

  • 结构体变量的首地址能够被自身对齐值所整除。

  • 结构体中每个成员相对结构体首地址的偏移都是 成员变量对齐值 的整数倍,如不满足,对前一个成员填充字节以满足。

  • 结构体的总大小为 有效对齐值 的整数倍,如不满足,最后填充字节以满足 1

联合
union Data 
{
	int i; 
	double x; 
	char str[16]; 
};

由于联合中所有成员均从内存中同一地址开始,因此该联合大小为成员中最大存储大小。

位域

有时候我们仅需要1bit来存储信息(例如开关变量),为了节省存储空间,C语言又提供了一种数据结构,把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数,允许在程序中按域名进行操作。常称之为“位域”或“位段”。

需要注意的是:位段实现依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。

//定义
struct 位域结构名
{
	类型说明符位域名:位域长度
	//ps:位域名可为空,若如此,则该位域变量仅起填充作用
	//...
};

//使用
位域变量名.位域名
位域大小

如下代码为例:

struct D
{
    unsigned int a:20;
    unsigned int b:22;
    unsigned int c:2;   
}data; 

unsigned int 所占空间为 4byte32bit,位域变量a20bit,接下来b22bit4byte不够分了,于是在下一 unsigned int 上分配,分配完剩下10bit,其中2bit分配给c。最终总占用大小为8byte

如果是以下位域:

struct D
{
    unsigned int a:1;
    long long b:1;
    unsigned int c:2;   
}data; 

则先申请4byte,分配给a 1bit;再申请16byte,分配给b 1bit,再申请4byte,分配给c

所以,最终占用的大小为24byte

为了安全操作,通常会将空余的位进行填充。

例如这样:

    struct D
    {
        unsigned int a:20;
        unsigned int :12;
        unsigned int b:22;
        unsigned int c:2;   
        unsigned int :8;
    }data; 

亦可使用 unsigned int :0; 令下一位域成员与下一个unsigned int 类型对齐。

对齐规则 6

C99规定intunsigned intbool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型的存在。位域作为嵌入式系统中非常常见的一种编程工具,优点在于压缩程序的存储空间。

其对齐规则大致为:

  • 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof()大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

  • 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof()大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

  • 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异;

  • 如果位域字段之间穿插着非位域字段,则不进行压缩;

使用场景

位域的使用主要为下面两种情况 6

  • 当机器可用内存空间较少而使用位域可大量节省内存时。如把结构作为大数组的元素时。

  • 当需要把一结构体或联合映射成某预定的组织结构时。如需要访问字节内的特定位时。

如何修改默认对齐

  • __attribute__

    __attribute__机制是 GCC 的一大特色,可以设置函数属性(Function Attribute)、变量属性(Variable Attribute)和类型属性(Type Attribute)。详细介绍请点击 参考 6

    • __attribute__ ((aligned (number)))

      让所作用的结构体成员对齐在 number 字节自然边界,若结构体体中有成员变量长度大于 number ,则按照最大成员变量长度对齐。

    • __attribute__((packed))

      得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐。亦可理解为取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐 7

    使用示例:

    	struct Test_Aligned
    	{
    	    char a;
    	    int b;
    	    short c;
    	}__attribute__((packed));
    
  • #pragma pack()

    • #pragma pack(number)

      默认对齐大小修改为 number

    • #pragma pack()

      __attribute__((packed))

    使用示例:

    	#pragma pack(8)
    	struct Test_Aligned
    	{
    	    char a;
    	    int b;
    	    short c;
    	};
    	#pragma pack()
    

编程考量

实际上,字节对齐的细节都由编译器来完成,我们不需要特意进行字节的对齐,但并不意味着我们不需要关注字节对齐的问题 1

默认对齐

Linux gcc 没有固定的默认字节对齐,当前推测默认对齐为 自身对齐值

    struct test
    {
    	char a;
    	long double b;
    	short c;
    };

    struct test A;
    printf("%d",sizeof(A));
    //结果为:48

其他的系统要求的对齐可能不相同,依赖与其使用的 ABI 4,因此我们应尽量自己指定默认对齐,以防踩坑。

这里贴上博主 Gerald Kwok 咨询相关问题后得到的官方回函:

在这里插入图片描述

存储优化

通过调整结构体的成员变量位置,以减小结构体存储空间。

例如:

//12byte
struct Test_Aligned
{
	char a;
	int b;
	short c;
};

//8byte
struct Optimization_Aligned
{
    char a;
    short c;
    int b;
};

跨平台问题

在不同平台上,编译器为了使字节对齐可能会对消息结构体进行填充,而不同编译平台可能填充为不同的形式,从而使结构体大小发送改变,大大增加处理器间数据通信的风险。

解决方法:

  • 将对齐格式设置为 1byte 对齐
  • 空置内容填充

此外,跨平台传输还需注意字节序问题(大段存储/小端存储)。

附件

C常见数据类型大小

.指针charshortintunsigned intfloatdoublelong intlong long int
32bit4124448416
64bit8124448816

参考鸣谢


  1. 理一理字节对齐的那些事 ↩︎ ↩︎ ↩︎

  2. ARM的字对齐问题总结 ↩︎

  3. 从鸟哥微博到字节对齐 ↩︎

  4. Linux gcc没有默认对齐数(内赋gcc官方大佬邮件) ↩︎ ↩︎

  5. C语言字节对齐问题 ↩︎

  6. C语言字节对齐问题详解 ↩︎ ↩︎ ↩︎ ↩︎

  7. C语言字节对齐、结构体对齐最详细的解释 ↩︎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值