什么是“伪共享”?如何重现/避免它?

18

今天我在并行编程课上和我的教授就“伪共享”问题有了不同的理解。我的教授的说法让我感到困惑,所以我立即指出了这一点。她认为,“伪共享”会导致程序结果出错。

我说,“伪共享”发生在将不同内存地址分配给同一高速缓存行时,向其中一个写入数据将导致另一个被移出缓存。如果处理器在这两个伪共享地址之间交替写入,那么它们都不能保留在缓存中,因此所有操作都会导致访问DRAM。

这是我目前的看法。实际上,我也不确定我所说的是否正确...如果我有误解,请指出。

因此有一些问题,假设缓存为64字节对齐,4路组相联。

  1. 两个地址之间的距离超过64字节,是否可能发生“伪共享”?
  2. 单线程程序是否可能遇到“伪共享”问题?
  3. 最好的代码示例来复现“伪共享”是什么?
  4. 一般来说,程序员应该注意什么以避免“伪共享”?
1. 如果两个地址之间的距离超过64字节,则不可能发生“伪共享”。
2. 单线程程序不可能遇到“伪共享”问题。
3. 最好的代码示例是创建一个数组,将其分成多个小块,并在各个线程之间交替写入每个小块。这样可以模拟多个线程同时访问同一缓存行的情况。
4. 程序员应该将相关数据放在单独的缓存行中,以避免“伪共享”。此外,可以通过增加填充来确保数据不共享相同的缓存行。

这是一个关于伪共享的视频,希望能有所帮助。我因为声望值不足50而无法添加评论,真的很尴尬。 - pingsoli
1个回答

10
我将分享我的观点回答你的问题。
  1. Two addresses that are separated by more bytes than block's size, won't reside on the exact same cache line. Thus, if a core has the first address in its cache, and another core requests the second address, the first won't be removed from cache because of that request. So a false sharing miss won't occur.

  2. I can't imagine how false sharing would occur when there's no concurrency at all, as there won't be anyone else but the single thread to compete for the cache line.

  3. Taken from here, using OpenMP, a simple example to reproduce false sharing would be:

    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];
    }
    
  4. Some general notes that I can think of to avoid false sharing would be:

    a. Use private data as much as possible.

    b. Sometimes you can use padding in order to align data, to make sure that no other variables will reside in the same cache that shared data reside.

欢迎进行任何更正或添加。


启用优化后,任何像样的编译器都会将sum_local[me]保存在寄存器中,除非它无法证明x[i]y[i]不能与其别名。 (某些编译器会发出代码以在运行时检查重叠,并在这种情况下使用自动向量化循环。否则,潜在的别名可能会使循环变得糟糕,即使没有虚假共享;仅在循环传递依赖链中增加额外的5个周期的存储转发延迟也非常糟糕)。但是,是的,任何稍微复杂一点的东西都可能在循环内产生虚假共享,而不仅仅是在末尾。 - Peter Cordes
@PeterCordes 如果NUM_THREADS不是一个常量会怎么样?我认为编译器在这种情况下无法分配寄存器。 - tuket
@tuket:这并不重要。它只需要证明foo[bar]在循环中每次都是相同的地址,并且写入它不会影响我们从x[i]y[i]读取的内容。(例如,sum_local[]不与x[]y[]重叠)。由于me是循环不变量,而sum_local是一个数组,编译器可以在循环内部将和存储在寄存器中,并使用me的运行时变量值在最后一次存储。每个线程都有自己的me,并知道C99 VLA double sum_local[NUM_THREADS];的基地址。 - Peter Cordes
@tuket:无论如何,理想情况下,每个线程都应该只使用自己的私有标量变量,但这个例子的重点是演示错误共享,所以它被故意写得“糟糕”。我仍然认为你需要禁用优化编译,才能阻止编译器将其优化为寄存器。或者使用volatile double sum_local[NUM_THREADS]; - Peter Cordes
你对问题1的回答是误导性的。因为在许多架构中,一个硬件预取器会拉取多个缓存行,所以即使在缓存线大小对齐时仍会发生(通常最小程度的)错误共享。例如,英特尔的Streamer和Spatial预取器会拉取两个缓存行,并且它们提到了内存的128字节对齐,出于这个原因。 - Emma Jane Bonestell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接