1.对齐和填充
在现代处理器上,你的C编译器在内存里对基本的C数据类型的存放方式是受约束的,为的是内存访问更快。
在x86或ARM处理器上,基本的C数据类型的存储一般并不是起始于内存中的任意字节地址。除了字符外,其他每种类型都有对齐要求。
类型 | 起始地址 |
字符 | 任意 |
2字节短整型 | 起始于偶地址 |
4字节整型 | 起始于被4整除的地址 |
8字节长整型 | 起始于被8整除的地址 |
双精度浮点型 | 起始于被8整除的地址 |
浮点型 | 起始于被4整除的地址 |
带符号与不带符号间没有差别。这个行话叫:在x86和ARM上,基本C语言的类型是自对齐(self-aligned).
数据与数据间。由于受他们起始位置的影响,数据与数据间存放的地址会有间隙,我们把这无用的地址(间隙)称为“水坑”。
2.结构体对,填充和重构
在C语言里,结构体的地址与它第一个成员的地址是相同的——没有前置填充。注意:在C++里,看上去像结构体的类可能不遵守这个规则!(遵不遵守依赖于基类和虚拟内存函数如何实现,而且因编译器而不同。)
不但结构体内会存在自对齐和填充,结构体和结构体之间也会存在尾随填充(trailingpadding)和跨步地址(stride address)。这些都会造成结构体在内存中的浪费,因此重构结构体很重要,这也是结构体封装的艺术。
第一件需要注意的事情是,“水坑”仅发生于两个地方。一个是大数据类型(有更严格的对齐要求)的存储区域紧跟在一个较小的数据类型的存储区域之后。另一个是结构体自然结束于它的跨步地址之前,需要填充,以使下一个实例可以正确对齐。
消除“水坑”的最简单的方法是按对齐的降序来对结构体成员重排序。就是说:所有指针对齐的子域在前面,因为在64位的机器上,它们会有8字节。接下来是4字节的整型;然后是2字节的短整型;然后是字符域。
3.重构时的困难
(1)难以处理的标量问题:数据类型大小的不确定性。(可用sizeof()来检查存储大小)
(2)可读性和缓存局部性:你应该做的事情是保持可读性——把相关的和同时访问的数据组合到毗邻的区域——这也会提高缓存行的局部性。这都是用代码的数据访问模式的意识,聪明地重排序的原因。
如果你的代码有多线程并发访问一个结构体,就会有第三个问题:缓存行反弹(cache line bouncing)。为了减少代价高昂的总线通信,你应该组织你的数据,使得在紧凑的循环中,从一条缓存行中读取,而在另一条缓存行中写。
是的,这与之前关于把相关数据组成同样大小的缓存行块的指南有些矛盾。多线程是困难的。缓存行反弹以及其它的多线程优化问题是十分高级的话题,需要整篇关于它们的教程。这里我能做的最好的就就是让你意识到这些问题的存在。
4. 转载资料