3. 数据类型与内存布局:基本数据类型的内存占用、结构体对齐与填充、位域的使用与性能权衡、缓存行对齐
大家好,我是你们的老朋友。今天咱们聊聊C语言里一个特别实在的话题——数据类型和内存布局。说实话,很多C语言开发者写了几年代码,可能都没太在意过这些细节。但我要告诉你,这些细节恰恰是性能优化的金矿。
我刚开始做嵌入式开发那会儿,就吃过内存布局的亏。一个结构体没处理好,整个系统的内存占用直接翻倍。从那以后,我就养成了一个习惯:每定义一个结构体,都会在心里默默算一遍它的内存布局。
3.1 基本数据类型的内存占用
先说说最基础的东西。C语言的基本数据类型,在不同平台上占用的字节数是不一样的。这一点,很多新手容易忽略。
| 类型 | 32位系统 | 64位系统 |
|---|---|---|
| char | 1字节 | 1字节 |
| short | 2字节 | 2字节 |
| int | 4字节 | 4字节 |
| long | 4字节 | 8字节 |
| float | 4字节 | 4字节 |
| double | 8字节 | 8字节 |
| 指针 | 4字节 | 8字节 |
你看,long和指针在32位和64位系统上差别很大。我个人习惯在跨平台代码里用int32_t、uint64_t这类定长类型,省心。
sizeof() 运算符确认当前平台的实际大小。别靠猜,靠测。
3.2 结构体对齐与填充
结构体对齐,这是个老生常谈的问题,但也是坑最多的地方。说白了,CPU读取内存不是按字节来的,而是按字(4字节或8字节)来的。如果数据没对齐,CPU就得读两次,性能直接打折扣。
举个例子:
struct BadLayout {
char a; // 1字节
int b; // 4字节
char c; // 1字节
};
你猜这个结构体占多少字节?直觉上1+4+1=6字节。但实际呢?
printf("%zu\n", sizeof(struct BadLayout)); // 输出12
为什么会这样?因为编译器会在a后面填充3个字节,让b对齐到4字节边界。然后在c后面再填充3个字节,让整个结构体大小是4的倍数。
我曾经在一个网络协议栈项目里,就因为结构体没对齐,导致数据包解析总是出错。排查了整整两天才发现是填充字节搞的鬼。
怎么优化?调整成员顺序:
struct GoodLayout {
int b; // 4字节
char a; // 1字节
char c; // 1字节
// 填充2字节
};
这样只占8字节,省了4个字节。你想想看,如果结构体数组有10万个元素,那就是400KB的差距。
#pragma pack(1) 强制取消对齐。虽然省了内存,但会严重降低访问速度。除非你明确知道自己在做什么。
3.3 位域的使用与性能权衡
位域是个好东西,能把多个小标志位塞进一个整型里。比如:
struct Flags {
unsigned int is_valid : 1;
unsigned int is_dirty : 1;
unsigned int is_locked : 1;
unsigned int reserved : 29;
};
这个结构体只占4字节,却能表示3个布尔值。如果不用位域,每个布尔值用int,那就是12字节。
但是,位域有代价。访问位域成员时,编译器需要生成额外的移位和掩码指令。我做过测试,频繁访问位域比直接访问整型慢了大约30%。
所以我的建议是:
- 如果内存是瓶颈(比如嵌入式设备),用位域
- 如果性能是瓶颈(比如高频交易系统),别用位域,直接用整型
3.4 缓存行对齐
这个知识点,说实话,很多资深工程师都不一定重视。但它在多线程编程里至关重要。
现代CPU的缓存行一般是64字节。当两个线程同时修改同一个缓存行里的不同变量时,就会发生"伪共享"(False Sharing)。
看个例子:
struct Data {
int thread1_counter; // 线程1频繁修改
int thread2_counter; // 线程2频繁修改
};
这两个变量很可能在同一个缓存行里。线程1修改thread1_counter时,会把这个缓存行标记为"脏"。线程2要修改thread2_counter时,发现缓存行失效了,得重新从内存加载。一来一回,性能就崩了。
怎么解决?用缓存行对齐:
struct Data {
int thread1_counter;
char padding[60]; // 填充到64字节
int thread2_counter;
};
或者用C11的alignas:
struct alignas(64) Data {
int thread1_counter;
int thread2_counter;
};
我记得有一次优化一个多线程日志系统,加了缓存行对齐后,吞吐量直接提升了3倍。嗯,这里要注意,缓存行对齐会浪费一些内存,但在多线程场景下,这点代价完全值得。
getconf LEVEL1_DCACHE_LINESIZE,Windows下用 wmic cpu get L2CacheSize。
知识体系总览
下面这张图,是我自己总结的数据类型与内存布局的核心逻辑。你看一眼,就能把今天讲的内容串起来。
好了,今天的内容就到这里。记住一句话:内存布局不是玄学,是科学。你每多理解一点,代码就快一分。