什么是CPU cache line和cache line bouncing?
什么是Cache Line?
由处理器进行的高速缓存空间分配负责管理CPU与主存储器之间的数据传输关系。当处理器访问尚未位于高速缓存中的内存一部分时,其会加载被访问地址周边的一块内存至高速缓存中,并期望其很快即被再次使用。由高速缓存处理的数据单位被称为memory blocks(记忆体区块),这些块的尺寸则被称为cache line size(Cache Line Size)。通常情况下,默认使用的快取行尺寸包括32位、64位及128位三种标准配置。一个拥有64字节长的 cache lines 的 64KB 级快速存储单元总计包含 1024 个 cache lines(Cache Lines)。
借助于gcc编译器中的特定编译指令__attribute__,我们可以实现结构体在内存中的布局优化。
#include <iostream>
using namespace std;
#define CACHELINE_SIZE 64
#define CACHELINE_ALIGNMENT __attribute__((aligned(CACHELINE_SIZE)))
// http://gcc.gnu.org/onlinedocs/gcc-4.5.2/gcc/Type-Attributes.html
void PrintStruct1() {
struct MyStruct {
int a; // 4
char b; // 1
double c; // 8
}; // 结构体对齐,变成4+1+3+8 = 16
// sizeof()打印结构体的总大小,offset()打印每个成员相对于结构体开头的偏移量
printf("Sizeof MyStruct1: %zu bytes\n", sizeof(MyStruct)); // 16
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 4
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 8
}
// __packed__: This attribute, attached to struct or union type definition,
// specifies that each member (other than zero-width bitfields) of the
// structure or union is placed to minimize the memory required.
void PrintStruct2() {
struct MyStruct {
int a;
char b;
double c;
} __attribute__ ((__packed__));
printf("Sizeof MyStruct2: %zu bytes\n", sizeof(MyStruct)); // 13
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 4
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 5
}
// __aligned__: This attribute specifies a minimum alignment (in bytes) for variables of the specified type.
void PrintStruct3() {
struct MyStruct {
int a;
char b;
double c;
} __attribute__ ((__aligned__(64))); // 指定64字节对齐
printf("Sizeof MyStruct3: %zu bytes\n", sizeof(MyStruct)); // 64
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 4
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 8
}
void PrintStruct4() {
struct MyStruct {
int a CACHELINE_ALIGNMENT; // 指定64字节对齐
char b;
double c;
};
printf("Sizeof MyStruct4: %zu bytes\n", sizeof(MyStruct)); // 64
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 4
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 8
}
void PrintStruct5() {
struct MyStruct {
int a;
char b CACHELINE_ALIGNMENT;
double c;
};
printf("Sizeof MyStruct5: %zu bytes\n", sizeof(MyStruct)); // 128
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 64
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 72
}
void PrintStruct6() {
struct MyStruct {
int a;
char b;
double c CACHELINE_ALIGNMENT;
};
printf("Sizeof MyStruct6: %zu bytes\n", sizeof(MyStruct)); // 128
printf("Offset of 'a': %zu bytes\n", offsetof(MyStruct, a)); // 0
printf("Offset of 'b': %zu bytes\n", offsetof(MyStruct, b)); // 4
printf("Offset of 'c': %zu bytes\n", offsetof(MyStruct, c)); // 64
}
int main() {
PrintStruct1();
PrintStruct2();
PrintStruct3();
PrintStruct4();
PrintStruct5();
PrintStruct6();
return 0;
}
代码解读
什么是cache line bouncing?
多个共享内存处理器按照缓存线粒度运行,在传输数据时会传递一个缓存线(始终保持对齐),因此缓存线本质上是一个一直在跳跃(bouncing)的球。例如:
假设处理器1试图获取自旋锁(spinlock)。通常占据8字节内存空间,并对其对齐。首先,在这种情况下:若处理器1的缓存中未找到该self-protecting lock identifier,则会将其对应的缓存行加载到内存中。若该self-protecting lock identifier当前未被占用,则会立即获取该资源。完成后立即释放该资源。接着,在这种情况下: 处理器2也会尝试获取同一个self-protecting lock identifier。从缓存中取出相关缓存行,并将其加载到自己的缓存区。接着,在这种情况下: 处理器2也会立即获取该资源。完成后立即释放该资源。
再次使用上述两个步骤,在具有两个处理器之间的缓存线中实现重叠。最糟糕的情况是:如果缓存线由64字节组成,并且每个自旋锁仅有8字节,在同一缓存线包含多个自旋锁的情况下,“false sharing”的现象会更加明显。具体而言,在同一个缓存线中存在自旋锁A和自旋锁B时,则会导致以下情况:处理器1仅需获取自旋锁A、而处理器2则仅需获取自旋锁B。因此,在跨处理器操作过程中,“false sharing”现象不可避免地会发生。这种情况被称为false sharing——虽然这两个处理器并未真正共享任何数据内容,在数据交错的过程中会产生显著的影响。
因此,当一个变量经常需要跨线程读写时,应该将其按照cache line对齐。
Cache Line Bouncing Test: https://arighi.blogspot.com/2008/12/cacheline-bouncing.html
