避免并发现线程之间的假共享

避免并发现线程之间的假共享 (PDF 218KB)

摘要

在对称多处理器 (SMP) 系统中,每个处理器均有一个本地高速缓存。 内存系统必须保证高速缓存的一致性。 当不同处理器上的线程修改驻留在同一高速缓存行中的变量时就会发生假共享, 结果导致高速缓存行无效,并强制执行更新,进而影响系统性能。 本文介绍了检测和更正假共享的方法。

本文是“英特尔多线程应用开发指南”系列的一部分,该系列介绍了针对英特尔® 平台开发高效多线程应用的指导原则。

背景

假共享是 SMP 系统上的一种常见性能问题。在SMP系统中,每个处理器均有一个高速缓存。 当不同处理器上的线程修改驻留在同一高速缓存行中的变量时就会发生假共享,如图 1 所示。 这种现象之所以被称为假共享,是因为每个线程并非真正共享相同变量的访问权。 访问同一变量或真正共享要求编程式同步结构,以确保有序的数据访问。

在下面的代码示例中以红色显示的源代码行会引起假共享:

 

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上存在潜在的假共享。 该数组的大小取决于线程数,并且足够小,可写入单个高速缓存行。 在并行执行时,这些线程会修改不同、但相邻的 sum_local 元素(源代码行以红色显示),结果使所有处理器的高速缓存行无效。


图 1. 当不同处理器上的线程修改驻留在同一高速缓存行中的变量时就会发生假共享, 从而导致高速缓存行无效,并强制内存更新以维持高速缓存的一致性。

在图1中,线程 0 和线程 1 会用到不同变量,它们在内存中彼此相邻,并驻留在同一高速缓存行。 高速缓存行被加载到 CPU 0 和 CPU 1 的高速缓存中(灰色箭头)。 尽管这些线程修改的是不同变量(红色和蓝色箭头),高速缓存行仍会无效,并强制内存更新以维持高速缓存的一致性。

要确保多个高速缓存中的数据一致性,支持多处理器的英特尔® 处理器遵循 MESI(Modified/Exclusive/Shared/Invalid,修改/独占/共享/无效)协议。 首次加载高速缓存行时,处理器将该高速缓存行标记为独占访问。 一旦该高速缓存行被标记为独占,后续加载将可以自由使用缓存中的现有数据。 如果该处理器看到相同的高速缓存行被其它处理器加载到总线上,就会将该高速缓存行标记为“共享”访问。 如果处理器保存标记为“S(共享)”的高速缓存行,该缓存行将被标记为“已修改”,所有其它处理器会收到一条‘无效’的缓存行信息。 如果处理器看到其它处理器访问标记为“M(已修改)”的相同高速缓存,该处理器会将该高速缓存行存回内存,并将其标记为“已共享”。 访问相同高速缓存行的其它处理器发生高速缓存丢失。

当高速缓存行被标记为“无效”时,处理器之间的频繁协调要求将高速缓存行写入内存,然后再加载。 假共享增加了这种协调工作,因此会显著降低应用性能。

由于编译器可以感知假共享,所以在消除可能发生的假共享方面大有可为。 例如,当使用优化选项编译上述代码时,编译器会利用线程专有临时变量消除假共享。 上述代码中的运行时假共享只有在编译代码时禁用了优化选项才会成为问题。

建议

避免假共享的主要方式是进行代码检查。 潜在的假共享主要出现在线程访问全局或动态分配共享数据结构的例程中。 注意,在线程访问内存中碰巧相近的几个完全不同的全局变量时,也会出现假共享。 线程本地存储或本地变量不会导致假共享。

运行时检测方法是使用英特尔® VTune 性能分析器或 英特尔® 性能调优实用程序(英特尔 PTU,请见 /zh-cn/articles/intel-performance-tuning-utility/)。 此方法通过基于事件取样(可发现哪些位置存在高速缓存行共享)来揭示性能影响。 但是,这种影响不区分真共享与假共享。

针对基于英特尔 ® 酷睿™ 2 处理器的系统,配置 VTune 分析器或英特尔 PTU 以取样 MEM_LOAD_RETIRED.L2_LINE_MISSEXT_SNOOP.ALL_AGENTS.HITM 事件。 针对基于智能英特尔 ® 酷睿™ i7 处理器的系统,配置取样MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM事件。 在英特尔 ® 酷睿™ 2 处理器产品家族 CPU 上,如果您在某些代码区域看到EXT_SNOOP.ALL_AGENTS.HITM 事件频繁发生,多少与 INST_RETIRED.ANY 事件有关,或者在智能英特尔 ® 酷睿™ i7 处理器产品家族 CPU 上,如果您看到 MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM 事件频繁发生,那么您就遇到了或真或假的共享了。 检查相应系统中位于或接近加载/存储指令处MEM_LOAD_RETIRED.L2_LINE_MISSMEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM 事件集中区段的代码,以确定内存位置驻留在相同高速缓存行并引起假共享的可能性。

英特尔 PTU 自带预定义的配置文件,用于收集有助于定位假共享的事件。 这些配置文件包括“英特尔® 酷睿™ 2 处理器产品家族 — 争用”和“智能英特尔® 酷睿™ i7 处理器产品家族 — 真假共享”。 英特尔 PTU 数据访问分析通过监控被不同线程访问的同一高速缓存行的不同偏移量来识别可能的假共享。 当您在数据访问窗口 (Data Access View) 中打开分析结果时,如图2 所示,内存热点 (Memory Hotspot) 面板将按照高速缓存行的粒度给出有关假共享的提示。


图2. 英特尔 PTU 内存热点面板中显示的假共享。

在图2 中,(位于地址 0x00498180 的高速缓存行的)内存偏移量 32 和 48 在工作函数中被 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;

使用数据结构数组时,将该结构填充到高速缓存行末尾,以确保该数组元素始于高速缓存行边界。 如果不能确保该数组对齐于高速缓存行边界,可填充该数据结构至高速缓存行的两倍大小。 下面的源代码演示了如何填充数据结构到高速缓存行边界,并使用 compiler __declspec (align(n)) 语句确保数组对齐,其中 n= 64(64 字节边界)。 如果该数组是动态分配的,可以增加分配大小,并调整指示器以便与高速缓存行边界对齐。

 

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];

	

 

使用数据的线程本地拷贝也可以减少发生假共享的频率。 线程本地拷贝可频繁读取并修改,只需在完成这些操作后再将结果拷贝回数据结构即可。 下面的源代码演示了如何使用本地拷贝避免假共享。

 

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

	}

	

 

使用指南

避免假共享,但是要谨慎使用这些技术。 过度使用会影响处理器可用高速缓存的有效使用。 即便对于多处理器共享高速缓存设计,仍然建议避免假共享。 尝试最大限度提高多处理器共享高速缓存设计中的高速缓存利用率可能会带来一些好处,但一般不会超过支持不同高速缓存架构的多代码路径所需的软件维护成本。

更多资源

有关编译器优化的更完整信息,请参阅优化通知