【C/C++】结构体/类内的内存对齐,一个有意思的特性(重传)

前言

我很久以前写过大量的博文都被我删除了,找了一些有价值的重传一下

主要讲一下C/C++在结构体和类在内存中的存储结构,注意空间和时间往往是反比关系,很多程序优化都符合这个原则,但也不绝对,有时要用好才可以,对于大多数程序员来说,其实都无视了这种细节上的优化。(很多语言的内存对齐都不会自动优化)

现象

struct Example {
    char a;   // 1 字节
    int b;    // 4 字节
    short c;  // 2 字节
};

从直观上看,这个结构体的大小应该是 1 + 4 + 2 = 7 字节。但实际上,很多编译器会对齐它,使其占用 12 字节(也可能使24字节,不同编译器默认结果不同),而不是 7 字节。这是因为编译器会按照一定的规则插入一些“填充字节”来对齐数据。

基本概念

内存对齐是一种让数据在内存中排列得更加高效的方式。它的目的是让 CPU 读取数据时更加快速,因为 CPU 并不是随意读取内存的,而是按照固定的“块”(比如 4 字节、8 字节等)来读取数据。如果数据的存储不对齐,CPU 可能需要多次访问内存才能完整读取一个变量,从而导致性能下降。

内存对齐规则

  1. 对齐原则

    • 每个成员变量的存储地址都必须是它“对齐边界”的整数倍。
    • “对齐边界”通常是该变量类型的大小,比如 int 的对齐边界是 4 字节,short 是 2 字节,char 是 1 字节。
  2. 结构体的总大小

    • 结构体的总大小必须是其“最大对齐边界”的整数倍。
  3. 最大对齐边界

    • 通常来说是结构体内最大基础结构的大小(嵌套结构,也是看嵌套结构内的最大基础数据类型)
    • 当最大基础结构大小小于编译器默认值时
    • 可手动强制设定最大对其边界

按照规则分析最开始的的例子

我们来一步步看看 struct Example 是怎么对齐的(假设编译器默认最大边界是4字节):

  • char a:占 1 字节,但因为 int b 的对齐边界是 4,所以 a 后面会补 3 个字节,使下一个变量 b 的地址是 4 的倍数。
  • int b:占 4 字节,地址是 4 的倍数,刚好对齐。
  • short c:占 2 字节,c 的地址需要是 2 的倍数,剩下 2 个字节用来填充,使整个结构体大小是最大对齐边界(4 字节)的倍数。

最终,内存布局如下(每个字母代表 1 字节):

地址数据
0a
1填充
2填充
3填充
4b
5b
6b
7b
8c
9c
10填充
11填充

结构体总大小是 12 字节


针对内存对齐如何优化

在设计结构体时,内存对齐会影响内存占用和性能,因此合理的结构体设计不仅可以节省空间,还能提升程序的运行效率。


1. 按照从大到小的顺序排列成员变量

不同类型的变量有不同的对齐要求(通常是它们的大小),因此将大对齐边界的变量放在前面,可以减少填充字节的浪费。

// 非优化的结构体
struct Example1 {
    char a;     // 1 字节,对齐 1 字节
    int b;      // 4 字节,对齐 4 字节
    short c;    // 2 字节,对齐 2 字节
};

// 内存布局(16 字节): 
// | a | 填充 | 填充 | 填充 | b | b | b | b | c | c | 填充 | 填充 |

// 优化后的结构体
struct Example2 {
    int b;      // 4 字节,对齐 4 字节
    short c;    // 2 字节,对齐 2 字节
    char a;     // 1 字节,对齐 1 字节
};

// 内存布局(8 字节):
// | b | b | b | b | c | c | a | 填充 |

优化结果

  • 非优化结构体占用 16 字节
  • 优化后结构体占用 8 字节,节省了一半的内存。

2. 避免不必要的小类型交错

多个小类型(如 charshort)交错排列时,会导致填充字节的增加。将相同类型的变量集中在一起,可以减少对齐浪费。

// 不推荐的设计
struct Example {
    char a;     // 1 字节,对齐 1 字节
    short b;    // 2 字节,对齐 2 字节
    char c;     // 1 字节,对齐 1 字节
    int d;      // 4 字节,对齐 4 字节
};

// 内存布局(12 字节):
// | a | 填充 | b | b | c | 填充 | d | d | d | d |

// 推荐的设计
struct OptimizedExample {
    char a;     // 1 字节,对齐 1 字节
    char c;     // 1 字节,对齐 1 字节
    short b;    // 2 字节,对齐 2 字节
    int d;      // 4 字节,对齐 4 字节
};

// 内存布局(8 字节):
// | a | c | b | b | d | d | d | d |

优化结果

  • 非优化结构体占用 12 字节
  • 优化后结构体占用 8 字节,节省了 4 字节

3. 使用位域(Bit-field)优化小型数据

如果结构体中有多个需要表示小范围数据的成员(如布尔值、枚举等),可以使用**位域(bit field)**来压缩它们的存储空间。

// 不使用位域
struct Flags {
    bool flag1;  // 1 字节
    bool flag2;  // 1 字节
    bool flag3;  // 1 字节
    bool flag4;  // 1 字节
};

// 使用位域
struct BitFlags {
    unsigned int flag1 : 1; // 1 位
    unsigned int flag2 : 1; // 1 位
    unsigned int flag3 : 1; // 1 位
    unsigned int flag4 : 1; // 1 位
};

优化结果

  • 不使用位域的结构体占用 4 字节(每个 bool 单独存储,填充到 4 字节对齐)。
  • 使用位域的结构体仅占用 4 位(被打包在一个 unsigned int 中)。

注意:位域的使用可能导致编译器生成额外的位操作指令,因此需要在性能和空间之间权衡。


4. 避免过小的对齐边界

虽然可以通过 #pragma pack__attribute__((packed)) 来修改对齐规则,使数据不进行对齐(比如按 1 字节对齐),但这可能导致性能下降。

强制取消对齐

#pragma pack(1) // 强制 1 字节对齐
struct Unaligned {
    char a;     
    int b;      
    short c;    
};
#pragma pack() // 恢复默认对齐

上述结构体占用 7 字节,节省了填充字节,但由于变量 b 无法对齐到 4 字节边界,读取时可能导致性能下降。

建议:不要随意使用 #pragma pack__attribute__((packed)),除非在嵌入式系统等对内存占用有极高要求的场景。


5. 使用数据对齐辅助工具

有些编译器提供了调整对齐的工具,可以明确指定结构体或者字段的对齐边界,方便在不同平台上优化。

对齐指令

struct Example {
    char a;     
    int b;      
    short c;    
} __attribute__((aligned(8))); // 强制对齐到 8 字节边界

使用这种方式,可以确保结构体按 8 字节对齐,在特定架构下可能提高访问效率。


6. 避免过多的指针成员

指针的对齐边界通常是 4 字节(32 位系统)8 字节(64 位系统),如果结构体中有大量指针成员,可能会增加对齐填充的浪费。因此,可以考虑减少指针的使用,或者将指针成员单独存储。


7. 合理使用联合体(union)以节省空间

如果多个成员不需要同时存在,可以将它们放入联合体中,共用一块内存。

// 普通结构体
struct Normal {
    int x;  // 4 字节
    double y; // 8 字节
};

// 使用联合体
union Optimized {
    int x;      // 4 字节
    double y;   // 8 字节
};
  • 普通结构体的大小为 12 字节(对齐到 8 字节)。
  • 联合体的大小为 8 字节(两个成员共用一块内存)。

注意:联合体适用于互斥数据,不能同时访问多个成员。


8. 缓存友好性优化

为了提高性能,可以根据实际使用场景设计结构体,使得经常访问的成员尽量存放在一起,减少缓存失效(Cache Miss)。

假设一个结构体被频繁访问:

struct Example {
    int frequently_used;
    char rarely_used[64];
};

在访问 frequently_used 时,rarely_used 可能占据了缓存行,导致浪费。可以优化为:

struct Optimized {
    int frequently_used;
    char padding[60];  // 手动填充,避免缓存浪费
    char rarely_used[64];
};

总结

在设计结构体时,可以从以下几点进行优化:

  1. 大类型优先排列:从大到小排列成员,减少填充字节。
  2. 相同类型集中:将相同对齐边界的成员放在一起,避免交错浪费。
  3. 使用位域压缩小型成员:尤其是布尔值或小范围数据。
  4. 慎用强制取消对齐:避免性能损失。
  5. 减少指针浪费:指针占用对齐空间较大,尽量避免大量使用。
  6. 使用联合体节省空间:当多个成员互斥时,采用联合体共享内存。
  7. 考虑缓存友好性:经常访问的字段放在一起,减少缓存失效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值