C++ 编译器优化消除了错误共享的影响。怎么用?

C++ 编译器优化消除了错误共享的影响。怎么用?,c++,loops,openmp,compiler-optimization,false-sharing,C++,Loops,Openmp,Compiler Optimization,False Sharing,我正在尝试使用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; co

我正在尝试使用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
// ...
const auto num\u slices=100000000;
const auto num_threads=4;//从1个线程扫描到9个线程
常数自动切片厚度=1.0/num切片;
const auto slices_per_thread=num_slices/num_threads;
std::向量部分和(num_线程);
#pragma omp并行num_线程(num_线程)
{
双本地缓冲区=0;
const auto thread_num=omp_get_thread_num();
用于(自动切片=每线程切片数*线程数;切片<每线程切片数*(线程数+1);+切片)
local_buffer+=func(slice*slice_thickness);/tl;dr:编译器将第二个版本优化为第一个版本。
考虑第二个实现的循环中的代码——暂时忽略它的OMP/多线程方面

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

(通过一个简化的示例)-注意
bar1()
bar2()
的代码几乎相同,在寄存器中发生累积

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


注:

  • 感谢@Evg注意到这个答案的前一个版本的代码中有一个明显的错误
  • 编译器必须能够知道
    func()
    也不会改变向量的值,或者为了加法的目的,决定它其实并不重要
  • 这种优化可以被看作是一种优化——从堆上的操作到寄存器上的操作——但我不确定这个术语是否适用于这种情况

nslice
nthread\u obt\u local
的值是什么?否则无法编译。您可以检查生成的代码是什么,似乎gcc优化了很多小片段。@MatthieuBrucher我的坏名字已更正。
num\u slices
在清理
num\u线程时是固定的,如图所示。注意在GodBolt的示例中,您正在修改指针
结果
本身,而不是它指向的值
*结果
,以及
foo
返回的值(在
eax
中)甚至没有使用!如果您更正代码,您将看到生成的程序集根本不相同,编译器不会执行您描述的优化。答案很好。我在几个月前得出相同结论的答案中看到了这一点(heap put in register)但我仍然对此感到有点惊讶。我最近给出了一个答案,明确避免了错误共享,而没有使用
threadprivate
,但这可能不是必要的。我想知道这种优化有多可靠?过早优化是万恶之源,但我想这是一种必要的明确优化,但也许我现在还为时过早(可能已经很久了)。@Evg:你是对的;但是纠正代码还包括确保编译器能够实现
int foo()
不会影响
*结果。请参阅新链接一些观察结果:1)
\uuu限制\uuuu
似乎对代码没有影响,2)如果添加
\uu属性__((noinline))
foo
为了防止内联,汇编代码将更容易掌握。
// ... 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
// ...