编译器优化消除了错误共享的影响.怎么样?

Sla*_*tan 3 c++ loops openmp compiler-optimization false-sharing

我正在尝试使用OpenMP复制虚假共享的效果,正如Tim Mattson在OpenMP简介中所解释的那样.

我的程序执行简单的数值集成(参见数学细节的链接),我已经实现了两个版本,第一个应该是缓存友好的,每个线程保持一个局部变量来累积它的索引空间部分,

const auto num_slices = 100000000; 
const auto num_threads = 4;  // Swept from 1 to 9 threads
const auto slice_thickness = 1.0 / num_slices;
const auto slices_per_thread = num_slices / num_threads;

std::vector<double> partial_sums(num_threads);

#pragma omp parallel num_threads(num_threads)
{
  double local_buffer = 0;
  const auto thread_num = omp_get_thread_num();
  for(auto slice = slices_per_thread * thread_num; slice < slices_per_thread * (thread_num + 1); ++slice)
    local_buffer += func(slice * slice_thickness); // <-- Updates thread-exclusive buffer
  partial_sums[thread_num] = local_buffer; 
}
// Sum up partial_sums to receive final result
// ...
Run Code Online (Sandbox Code Playgroud)

而第二个版本让每个线程更新共享中的一个元素std::vector<double>,导致每个写入使所有其他线程上的缓存行无效

// ... as above
#pragma omp parallel num_threads(num_threads)
{
  const auto thread_num = omp_get_thread_num();
  for(auto slice = slices_per_thread * thread_num; slice < slices_per_thread * (thread_num + 1); ++slice)
    partial_sums[thread_num] += func(slice * slice_thickness); // <-- Invalidates caches
}
// Sum up partial_sums to receive final result
// ...
Run Code Online (Sandbox Code Playgroud)

问题是除非我关闭优化,否则我无法看到任何虚假共享的影响.

在此输入图像描述

使用没有优化的GCC 8.1(-O0)编译我的代码(必须考虑比上面的代码片段更多的细节)产生我天真期望的结果,而使用完全优化(-O3)消除了在性能方面的任何差异.两个版本,如图中所示.

对此有何解释?编译器是否真的消除了错误共享?如果没有,为什么在运行优化代码时效果如此之小?

我在使用Fedora的Core-i7机器上.该图显示平均值,其样本标准偏差不会向此问题添加任何信息.

ein*_*ica 10

tl; dr:编译器将第二个版本优化为第一个版本.

考虑第二个实现的循环中的代码 - 暂时忽略它的OMP /多线程方面.

你有一个值的增量std::vector- 它必须位于堆上(好吧,直到并包括在C++ 17中).编译器看到你在循环中添加了堆上的值; 这是优化的典型候选者:它将堆访问从循环中取出,并使用寄存器作为缓冲区.它甚至不需要从堆读取,因为它们只是添加 - 所以它基本上到达了您的第一个解决方案.

在GodBolt上看到这种情况(有一个简化的例子) - 注意代码的代码bar1()和bar2()几乎是相同的,寄存器中发生了累积.

现在,涉及多线程和OMP的事实并没有改变上述情况.如果您要使用,std::atomic<double>而不是double,那么它可能已经改变(如果编译器足够智能,可能甚至不会).


笔记:

  • 感谢@Evg注意到此答案的先前版本的代码中的一个明显错误.
  • 编译器必须能够知道这func()会不会也改变您的载体的价值-或者决定,对于另外的目的,应该不是真正的问题.
  • 这种优化可以看作是一种强度降低 - 从堆上的操作到寄存器上的操作 - 但我不确定这个术语是否适用于这种情况.