【翻译】线程间伪共享的避免和识别
英文原文:
Avoiding and Identifying False Sharing Among Threads.
http://software.intel.com/en-us/articles/avoiding-and-identifying-false-sharing-among-threads/
该页面有个pdf下载链接,本文是按照这个pdf的内容翻译的,【】里的内容是我根据自己的理解添加的,中文不明确的地方括号中加上了对应的英文原文。
摘要Abstract
在对称多处理器(SMP)系统中,每个处理器都有各自的本地cache(local cache)。内存系统必须保证cache一致性(cache coherence)。伪共享(false sharing)发生在不同处理器上的线程修改位于同一个cache line的变量这种情景下【cache被划分为cache line,cache line是cache与主存进行交换的单位,是能被cache处理的内存chunks,chunk的大小即为cache line size,典型的大小为32,64及128 Bytes。关于cache line的更多信息,可以参考文末参考链接1】。这会导致cache line失效并强制刷新,因此导致性能下降。本文涉及了检测和修改伪共享的方法。
本文是《开发多线程应用的Intel手册》(The Intel Guide for Developing Multithreaded Applications)系列文章的一部分。该手册提供了Intel平台上开发高效多线程应用的指引。
背景Background
伪共享是对称多处理器(SMP)系统中一个著名的性能问题,在SMP中,每个处理器都有各自的本地cache。伪共享发生在如下情况下:不同处理器上的线程修改了位于同一个cache line上的数据,如图1所示。
图1. 伪共享发生情景:不同处理器上的线程修改了位于同一个cache line上的数据,这导致cache line失效,强制内存更新来维护cache一致性。
这种情况被称为伪共享(false sharing)是因为每个线程并非真正共享同样的变量。访问同样的变量,或者说是真共享(true sharing),要求同步编程构造来保证有序的数据访问(require programmatic synchronization constructs to ensure ordered data access)【?】。
下面代码中红色那行代码会导致伪共享:
double sum=0.0, sum_local[NUM_THREADS];
#pragma omp parallel num_threads(NUM_THREADS)
{
int me = omp_get_thread_num();
sum_local[me] = 0.0;
#pragma omp for
for (i = 0; i < N; i++)
sum_local[me] += x[i] * y[i];
#pragma omp atomic
sum += sum_local[me];
}
sum_local数组的的访问导致可能(potential潜在的)的伪共享。这个数组长度与线程数目有关,并且足够小,能放到同一个cache line中。当并行执行的时候,线程修改不同但邻接的sum_local数组元素时(红色那行代码),将导致所有处理器的cache line失效。
在图1中,thread 0和1请求位于同一个cache line中相邻的变量。这个cache line被加载到CPU0和CPU1的cache中,即使两个线程修改了不同的变量(红色和蓝色箭头),这个cache line也失效了,这导致了一个主存更新来保持cache一致性。
【下面这段讲MESI协议,读者可以直接读参考链接2,这个讲的更清晰】
为了保证多cache下数据一致性,Intel处理器(是多处理器架构)服从MESI(Modified/Exclusive/Shared/Invalid)协议【对cache line的状态标记,更多关于MESI的信息可以阅读参考链接2】。当cache line第一次加载进来之后,处理器将cache line标记为 Exclusive【Exclusive表示这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中。】,只要这个cache line被标记为 Exclusive,后续的加载(subsequent loads【应该指读内存指令】)能够免费地(【无内存开销】)使用这个cache line中的数据。如果这个处理器在总线上看到这个cache line被其他处理器加载,这个处理器会将这个cache line标记为Shared【Shared表示这行数据有效,数据和内存中的数据一致,数据存在于很多Cache中】。如果这个处理器写(store【应该是指CPU写】)了一个标记为’S’【 Shared的缩写】的cache line,这个cache line被标记为 Modified【Modified表示这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中】,同时,这个处理器向所有其他处理器发送’Invalid’ cache line消息【因为该写操作导致其他CPU的cache中该数据失效】。如果处理器看到被标记为M的cache line被其他处理器访问,则这个处理器将该cache line写回主存,并将cache line标记为Shared【此时,所有其他CPU的后续访问该数据时,读到的是新的数据】,其他处理器访问这个cache line时,将会导致一个cache miss【它们要么是从未被加载到cache过,要么是从Shared状态变成了Invalid状态】。
当cache line被标记为Invalid时,处理器之间频繁地协调【应指通过MESI协议维护cache一致性】,导致cache line数据被写回内存并随后又加载到cache中。伪共享加剧了协调过程【应指伪共享情况下,协调发生得更频繁了】,并显著降低了应用程序性能。
当编译器意识到伪共享时,编译器会在伪共享发生时做一些消除伪共享的工作。例如,当上面这段代码加上优化选项编译时,编译器会使用线程私有的临时数据来消除伪共享。这段代码只会在不加优化选项编译时,才会发生伪共享。
建议Advice
避免伪共享的主要方法是代码检查(code inspection)。当线程访问全局变量或者动态分配的共享数据结构是伪共享的潜在来源。注意到伪共享可能不太容易识别出来(obscured),因为线程访问的是完全不同的而实际上碰巧在主存中相邻的全局变量。线程局部存储或者局部变量不会是伪共享的来源。
运行时检测方法是使用Intel® VTune™ Performance Analyzer 或者 Intel® Performance Tuning Utility (Intel PTU, available at http://software.intel.com/en-us/articles/intel-performance-tuning-utility/)。这个方法依赖于基于事件的采样(event-based sampling)来发现cache line暴漏出来的可视的影响(visiable effects),然而,这些影响并未区分真共享和伪共享。
【接下来的四段内容是讲这个工具的,略】
一旦检测到,我们有几种方法来改正伪共享(correct false sharing)。目的是保证导致伪共享的变量在主存中离得足够远,以便它们不会出现在同一个cache line中。
一种技术是使用编译指示,来强制使每一个变量对齐。下面的代码显式了编译器使用__declspec( align(n) ) 此处n=64,按照cache line边界对齐。
__declspec (align(64)) int thread1_global_variable;
__declspec (align(64)) int thread2_global_variable;
当使用数组时,在cache line尾部填充padding来保证数据元素在cache line边界开始。如果不能够保证数组按照cache line边界对齐,填充数据结构【数组元素】使之是cache line大小的两倍。下面的代码显式了填充数据结构使之按照cache line对齐。并且通过__declspec( align(n) )语句来保证数组也是对齐的。如果数组是动态分配的,你可以增加分配的大小,并调整指针来对其到cache line边界。
struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v; // Frequent read/write access variable
unsigned long start;
unsigned long end;
// expand to 64 bytes to avoid false-sharing
// (4 unsigned long variables + 12 padding)*4 = 64
int padding[12];
};
__declspec (align(64)) struct ThreadParams Array[10];
另外一种降低伪共享的可能的办法是使用线程局部数据拷贝(thread-local copies of data)。线程局部数据拷贝能够被频繁读取和修改,并且只将结果完全拷贝回去(only when complete, copy the result back to the data structure)【应指该线程随意读并且修改,但只写回一次】。下面的代码显式了使用局部拷贝来避免伪共享。
struct ThreadParams
{
// For the following 4 variables: 4*4 = 16 bytes
unsigned long thread_id;
unsigned long v; //Frequent read/write access variable
unsigned long start;
unsigned long end;
};
void threadFunc(void *parameter)
{
ThreadParams *p = (ThreadParams*) parameter;
// local copy for read/write access variable
unsigned long local_v = p->v;
for(local_v = p->start; local_v < p->end; local_v++)
{
// Functional computation
}
p->v = local_v; // Update shared data structure only once
}
使用指南Usage Guidelines
要避免伪共享,但要保守地(sparingly)使用这些技术。过度使用会阻碍处理器可用cache的有效使用【因为加了一些padding,有浪费】。即使在多处理器共享cache设计中,也建议避免伪共享。在多处理器共享cache设计中,最大化cache利用率带来的少许可能的好处相比软件支持多种不同的cache架构带来的维护费用来说得不偿失。
额外的阅读资源【Intel的三个产品链接,略】
扩展阅读链接
1. 理解cache line http://hickey.in/?p=326
2. MESI协议 http://blog.csdn.net/muxiqingyang/article/details/6615199