2个处理器写到不同的物理地址,但是2个物理地址映射到同一个缓存行,这种情况称为缓存行伪共享(False Sharing),造成的危害是:因为缓存一致性协议而生成大量通信,导致性能下降。消除缓存行伪共享的方法是把不相关的2个数据放在不同的缓存行。
可以使用GCC编译器提供的属性“aligned”修饰结构体的字段、结构体或者变量。可以选择指定名称前后带有“__”(2个下划线)的属性名称,在头文件中使用它们,不必担心可能有名称相同的宏,例如使用属性名称“aligned”取代“aligned”。
为了使用方便,自己定义宏__cacheline_aligned如下。目前主流的处理器缓存的行大小是64字节,所以把宏L1_CACHE_LINE_SIZE的默认值设置为64。
#if !defined(L1_CACHE_LINE_SIZE)
#define L1_CACHE_LINE_SIZE 64
#endif
#define __cacheline_aligned __attribute__((__aligned__(L1_CACHE_LINE_SIZE)))
#define __cacheline_aligned attribute((aligned(L1_CACHE_LINE_SIZE))) 可以使用宏__cacheline_aligned修饰结构体的字段,确保这个字段在结构体里面的偏移对齐到一级缓存行的长度,也就是偏移是一级缓存行的长度的整数倍。
可以使用宏__cacheline_aligned修饰结构体,确保这个结构体的长度对齐到一级缓存行的长度,并且类型是这个结构体的变量(包括全局变量和局部变量)的地址对齐到一级缓存行的长度。
可以使用宏__cacheline_aligned修饰变量,确保变量的地址对齐到一级缓存行的长度。
1.动态分配结构体
例如结构体s定义如下,要把字段m1和m2分别放在2个缓存行中,使用宏__cacheline_aligned修饰字段m2可以确保这个字段在结构体里面的偏移对齐到一级缓存行的长度,使用宏__cacheline_aligned修饰结构体可以确保结构体的长度对齐到一级缓存行的长度。要想使得字段m2的地址是一级缓存行的长度的整数倍,必须确保动态分配的结构体的起始地址是一级缓存行的长度的整数倍,并且字段m2在结构体里面的偏移是一级缓存行的长度的整数倍。
struct s {
int m1;
int m2 __cacheline_aligned;
} __cacheline_aligned;
可以使用函数posix_memalign()或aligned_alloc()给结构体动态分配内存,这两个函数的原型如下。
#include <stdlib.h>
int posix_memalign(void **memptr, size_t alignment, size_t size);
void *aligned_alloc(size_t alignment, size_t size);
int posix_memalign(void **memptr, size_t alignment, size_t size); void aligned_alloc(size_t alignment, size_t size); 函数 posix_memalign()分配size字节的内存,把分配的内存的地址存放在memptr中。分配的内存的地址是参数alignment的整数倍,参数alignment必须是2的幂,并且是sizeof(void *)的整数倍。
函数aligned_alloc()是C11标准定义的函数,参数size必须是参数alignment的整数倍。分配的内存的地址是参数alignment的整数倍,参数alignment必须是2的幂,并且是sizeof(void *)的整数倍。函数aligned_alloc()返回分配的内存的地址。
2.静态分配结构体
如果使用宏__cacheline_aligned修饰结构体,那么这个结构体的长度是一级缓存行的长度的整数倍,并且类型是这个结构体的变量(包括全局变量和局部变量)的地址是一级缓存行的长度的整数倍。下面是一个例子。
#include <stdio.h>
#if !defined(L1_CACHE_LINE_SIZE)
#define L1_CACHE_LINE_SIZE 64
#endif
#define __cacheline_aligned __attribute__((__aligned__(L1_CACHE_LINE_SIZE)))
struct s {
int m1;
int m2 __cacheline_aligned;
} __cacheline_aligned;
struct s g;
int main(int argc, char *argv[])
{
struct s v;
printf("sizeof(g)=%lu, &g=%p, &g %% L1_CACHE_LINE_SIZE = %lu\n",
sizeof(g), &g, (((unsigned long)&g) % L1_CACHE_LINE_SIZE));
printf("sizeof(v)=%lu, &v=%p, &v %% L1_CACHE_LINE_SIZE = %lu\n",
sizeof(v), &v, (((unsigned long)&v) % L1_CACHE_LINE_SIZE));
return 0;
}
运行程序,从打印的信息可以看到:g和v的长度都是128字节,地址是64的整数倍。
如果使用宏__cacheline_aligned修饰变量,那么变量的地址是一级缓存行的长度的整数倍,变量的长度不一定是一级缓存行的长度的整数倍。下面是一个例子。
#include <stdio.h>
#if !defined(L1_CACHE_LINE_SIZE)
#define L1_CACHE_LINE_SIZE 64
#endif
#define __cacheline_aligned __attribute__((__aligned__(L1_CACHE_LINE_SIZE)))
struct s {
int m1;
int m2;
};
struct s g __cacheline_aligned;
int main(int argc, char *argv[])
{
struct s v __cacheline_aligned;
printf("sizeof(g)=%lu, &g=%p, &g %% L1_CACHE_LINE_SIZE = %lu\n",
sizeof(g), &g, (((unsigned long)&g) % L1_CACHE_LINE_SIZE));
printf("sizeof(v)=%lu, &v=%p, &v %% L1_CACHE_LINE_SIZE = %lu\n",
sizeof(v), &v, (((unsigned long)&v) % L1_CACHE_LINE_SIZE));
return 0;
}
运行程序,从打印的信息可以看到:g和v的长度都是8字节,地址是64的整数倍。
3.参考文档
(1) GCC的公共函数属性aligned,https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-aligned-function-attribute
(2) GCC的公共变量属性aligned,https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-aligned-variable-attribute
(3) GCC的公共类型属性aligned,https://gcc.gnu.org/onlinedocs/gcc/Common-Type-Attributes.html#index-aligned-type-attribute
(4) posix_memalign, https://man7.org/linux/man-pages/man3/posix_memalign.3.html