避免并识别线程间伪共享

分类:

摘要

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

本文是《英特尔® 多线程应用开发指南》系列的一部分,后者用于指导开发人员针对英特尔®平台开发高效的多线程应用。

背景

错误共享是 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(已修改)”相同的高速缓存被其它处理器访问,该处理器将该高速缓存行存回内存,并将其标记为“S”。访问相同高速缓存行的其它处理器发生高速缓存丢失。

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

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

建议

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

运行时检测方法是使用 Intel VTune Performance Analyzer 或 Intel_ Performance Tuning Utility(Intel PTU,请见http://software.intel.com/en-us/articles/intel-performance-tun= ing-utility/)。此方法通过基于事件取样(可发现哪些位置存在高速缓存行共享)来揭示性能影响。但是,这种影响不区分真正共享与错误共享。

针对基于英特尔 ® 酷睿™ 2处理器的系统,配置 VTune 分析器或 Intel PTU 以取样 MEM_LOAD_RETIRED.L2_LINE_MISS 和 EXT_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_MISS 和 MEM_UNCORE_RETIRED.OTHER_CORE_L2_HITM 事件集中区段的代码,以确定内存地址驻留在相同高速缓存行并引起错误共享的可能性。

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


图2.Intel PTU Memory Hotspots 面板中显示的错误共享。

在图2 中,(位于地址 0x00498180 的高速缓存行的)内存偏移量 32 和 48 在工作函数中被 ID=3D59 线程和 ID=3D62 线程访问。由于 ID=3D59 线程执行数组初始化,其中也存在部分真正共享。

粉色用于提示高速缓存行中的错误共享。请注意与高速缓存行及其对应的偏移量相关的 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
{
  //对于以下4 个变量:4*4 = 16 字节
  unsigned long thread_id;
  unsigned long v; //频繁读/写访问变量
  unsigned long start;
  unsigned long end;

  //扩展到64 字节以避免错误共享
  //(4 个无符号的长变量+ 12 个填充值)*4 = 64
  int padding[12];
};

__declspec (align(64)) struct ThreadParams Array[10];

 

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

 

struct ThreadParams
{
  //对于以下4 个变量:4*4 = 16 字节
  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;  //只需更新一次共享数据结构
}

 

使用准则

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

其它资源

如需更全面地了解编译器优化,请参阅优化注意事项.