【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 字节):

地址 数据
0 a
1 填充
2 填充
3 填充
4 b
5 b
6 b
7 b
8 c
9 c
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. 考虑缓存友好性:经常访问的字段放在一起,减少缓存失效。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容