65.9K
CodeProject 正在变化。 阅读更多。
Home

避免和识别线程之间的虚假共享

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2010年6月2日

CPOL

7分钟阅读

viewsIcon

26169

在对称多处理器 (SMP) 系统中,每个处理器都有一个本地缓存。内存系统必须保证缓存一致性。当不同处理器上的线程修改位于同一缓存行的变量时,就会发生伪共享。学习检测和纠正伪共享的方法。

摘要

在对称多处理器 (SMP) 系统中,每个处理器都有一个本地缓存。内存系统必须保证缓存一致性。当不同处理器上的线程修改位于同一缓存行的变量时,就会发生伪共享。这会使缓存行失效并强制更新,从而损害性能。本文介绍检测和纠正伪共享的方法。

本文是大型系列“Intel 多线程应用程序开发指南”的一部分,该系列为 Intel® 平台上的高效多线程应用程序开发提供了指导方针。

背景

伪共享是 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 的不同但相邻的元素(红色所示的源行),这会使所有处理器的缓存行失效。

image001.gif

图 1. 当不同处理器上的线程修改位于同一缓存行上的变量时,就会发生伪共享。这会使缓存行失效并强制进行内存更新以保持缓存一致性。

在图 1 中,线程 0 和线程 1 需要在内存中相邻并位于同一缓存行上的变量。缓存行被加载到 CPU 0 和 CPU 1 的缓存中(灰色箭头)。即使线程修改不同的变量(红色和蓝色箭头),缓存行也会失效,从而强制进行内存更新以保持缓存一致性。

为了确保多个缓存之间的数据一致性,支持多处理器的英特尔® 处理器遵循 MESI(修改/独占/共享/无效)协议。首次加载缓存行时,处理器会将缓存行标记为“独占”访问。只要缓存行标记为独占,后续加载就可以自由使用缓存中的现有数据。如果处理器在总线上看到另一个处理器加载了相同的缓存行,它会将缓存行标记为“共享”访问。如果处理器存储标记为“S”的缓存行,缓存行将被标记为“修改”,并且所有其他处理器都会收到“无效”缓存行消息。如果处理器看到现在标记为“M”的相同缓存行被另一个处理器访问,处理器会将缓存行存储回内存,并将其缓存行标记为“共享”。访问相同缓存行的另一个处理器会发生缓存未命中。

当缓存行标记为“无效”时,处理器之间需要频繁协调,这需要将缓存行写入内存并随后加载。伪共享会增加这种协调,并会显著降低应用程序性能。

由于编译器知道伪共享,因此它们在消除可能发生伪共享的情况方面做得很好。例如,当上述代码使用优化选项编译时,编译器会使用线程私有临时变量消除伪共享。只有在禁用优化的情况下编译代码,上述代码的运行时伪共享才是一个问题。

通知

避免伪共享的主要方法是通过代码检查。线程访问全局或动态分配的共享数据结构的情况是伪共享的潜在来源。请注意,伪共享可能会被以下事实所掩盖:线程可能正在访问完全不同的全局变量,这些变量在内存中恰好相对靠近。线程局部存储或局部变量可以排除为伪共享的来源。

运行时检测方法是使用 Intel® VTune™ 性能分析器或 Intel® 性能调优实用程序 (Intel PTU,可在 http://software.intel.com/en-us/articles/intel-performance-tuning-utility/ 获得)。此方法依赖于基于事件的采样,该采样发现缓存行共享暴露出性能可见效应的地方。但是,此类效应无法区分真共享和伪共享。

对于基于英特尔® 酷睿™ 2 处理器的系统,配置 VTune 分析器或 Intel 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 事件的集中代码,以确定内存位置位于同一缓存行并导致伪共享的可能性。

Intel PTU 附带预定义的配置文件,用于收集有助于定位伪共享的事件。这些配置是“英特尔® 酷睿™ 2 处理器家族 – 竞争使用”和“英特尔® 酷睿™ i7 处理器家族 – 伪真共享”。Intel PTU 数据访问分析通过监控不同线程访问的相同缓存行的不同偏移量来识别伪共享候选项。当您在数据访问视图中打开分析结果时,内存热点窗格将包含缓存行粒度伪共享的提示,如图 2 所示。

image002.jpg

图 2. Intel PTU 内存热点窗格中显示的伪共享。

在图 2 中,内存偏移量 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
{
  // 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
}

使用指南

避免伪共享,但要谨慎使用这些技术。过度使用会阻碍处理器有效利用可用缓存。即使采用多处理器共享缓存设计,也建议避免伪共享。在多处理器共享缓存设计上尝试最大化缓存利用率的微小潜在收益通常不会超过支持不同缓存架构的多个代码路径所需的软件维护成本。

额外资源

避免和识别线程间的伪共享 - CodeProject - 代码之家
© . All rights reserved.