在对称多处理(SMP)系统中,每个处理器都有自己的本地缓存,伪共享是一个众所周知的性能问题。当不同处理器上的线程修改位于同一缓存行的变量时,就会发生伪共享。这种情况被称为伪共享,因为每个线程实际上并没有共享对同一个变量的访问。如果确实需要访问同一个变量,即真共享,那么就需要使用程序同步构造来确保有序的数据访问。
伪共享问题可以通过代码检查来避免。线程访问全局或动态分配的共享数据结构时,可能会产生伪共享。值得注意的是,伪共享可能因为线程访问的全局变量在内存中相对接近而被掩盖。线程本地存储或局部变量可以排除在伪共享的来源之外。
使用Intel® VTune™性能分析器或Intel®性能调优工具(Intel PTU)可以检测伪共享。这些工具依赖于基于事件的采样,发现缓存行共享暴露出的性能可见效果。但是,这些效果并不能区分真共享和伪共享。
在基于Intel® Core™ 2处理器的系统上,配置VTune分析器或Intel PTU来采样MEM_LOAD_RETIRED.L2_LINE_MISS和EXT_SNOOP.ALL_AGENTS.HITM事件。在基于Intel® Core i7处理器的系统上,配置采样MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM事件。如果在Intel® Core™ 2处理器系列CPU的某些代码区域,EXT_SNOOP.ALL_AGENTS.HITM事件的发生频率占INST_RETIRED.ANY事件的百分比或更多,或者在Intel® Core i7处理器系列CPU上MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM事件的发生频率很高,那么可能遇到了真共享或伪共享。检查MEM_LOAD_RETIRED.L2_LINE_MISS和MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM事件集中的代码,以确定内存位置是否位于同一缓存行上,从而引起伪共享。
Intel PTU提供了预定义的配置文件,用于收集有助于定位伪共享的事件。这些配置文件包括"Intel® Core™ 2处理器系列 - 竞争使用"和"Intel® Core™ i7处理器系列 - 真假共享"。Intel PTU数据访问分析通过监控不同线程访问的同一缓存行的不同偏移量来识别伪共享候选。当在数据访问视图中打开分析结果时,内存热点窗格将提供有关缓存行粒度的伪共享提示,如图2所示。
图2显示了Intel PTU内存热点窗格中的伪共享。内存偏移量32和48(缓存行地址0x00498180)被ID=59线程和ID=62线程在工作函数中访问。由于ID=59线程完成的数组初始化,还有一定的真共享。
粉色用于提示缓存行的伪共享。注意与缓存行及其相应偏移量相关的MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM的高数值。
一旦检测到伪共享,有几种技术可以纠正。目标是确保引起伪共享的变量在内存中相隔足够远,以至于不能位于同一缓存行上。虽然以下不是详尽无遗的列表,但讨论了三种可能的方法。
一种技术是使用编译器指令强制个别变量对齐。以下源代码演示了使用__declspec(align(n))的编译器技术,其中n等于64(64字节边界),以对齐缓存行边界上的个别变量。
__declspec(align(64)) int thread1_global_variable;
__declspec(align(64)) int thread2_global_variable;
当使用数据结构数组时,将结构填充到缓存行的末尾,以确保数组元素从缓存行边界开始。如果不能确保数组在缓存行边界上对齐,将数据结构填充到缓存行大小的两倍。以下源代码演示了将数据结构填充到缓存行边界,并确保数组也使用编译器__declspec(align(n))语句对齐,其中n等于64(64字节边界)。如果数组是动态分配的,可以增加分配大小并调整指针以与缓存行边界对齐。
struct ThreadParams {
unsigned long thread_id;
unsigned long v;
unsigned long start;
unsigned long end;
int padding[12];
};
__declspec(align(64)) struct ThreadParams Array[10];
struct ThreadParams {
unsigned long thread_id;
unsigned long v;
unsigned long start;
unsigned long end;
};
void threadFunc(void *parameter) {
ThreadParams *p = (ThreadParams*) parameter;
unsigned long local_v = p->v;
for (local_v = p->start; local_v < p->end; local_v++) {
// 功能计算
}
p->v = local_v;
// 只更新一次共享数据结构
}