编译器优化消除了错误共享的影响。如何?
Compiler optimization eliminates effects of false sharing. How?
我正在尝试使用 OpenMP 复制错误共享的效果,如 OpenMP introduction by Tim Mattson 中所述。
我的程序执行了一个简单的数值积分(请参阅 link 了解数学细节)并且我已经实现了两个版本,第一个应该是缓存友好的,每个线程都保留一个本地变量来累积它在索引中的部分 space,
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
// ...
而第二个版本让每个线程更新共享 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
// ...
问题是除非我关闭优化,否则我无法看到虚假共享的任何影响。
使用没有优化 (-O0) 的 GCC 8.1 编译我的代码(它必须考虑比上面的代码片段更多的细节)产生我天真的预期的结果,而使用完全优化 (-O3) 消除了任何差异两个版本之间的性能方面,如图所示。
这是怎么解释的?编译器真的消除了虚假共享吗?如果不是,怎么运行优化后的代码效果这么小?
我在使用 Fedora 的 Core-i7 机器上。该图显示平均值,其样本标准偏差不会为该问题添加任何信息。
tl;dr:编译器将您的第二个版本优化为第一个。
考虑第二个实现循环中的代码 - 暂时忽略它的 OMP/multithreaded 方面。
您在 std::vector
中有一个值的增量 - 它必须位于堆上(好吧,直到并包括在 C++17 中)。编译器看到你在一个循环中添加到堆上的一个值;这是一个典型的优化候选者:它将堆访问从循环中取出,并使用寄存器作为缓冲区。它甚至不需要从堆中 读取 ,因为它们只是添加 - 所以它基本上到达了您的第一个解决方案。
See this happening on GodBolt(用一个简化的例子)- 注意 bar1()
和 bar2()
的代码几乎是一样的,累加发生在寄存器中。
现在,涉及多线程和 OMP 的事实并没有改变上述情况。例如,如果您使用 std::atomic<double>
而不是 double
,那么它 可能 已更改(如果编译器足够聪明,甚至可能不会更改) .
备注:
- 感谢@Evg 注意到此答案先前版本的代码中存在明显错误。
- 编译器必须能够知道
func()
不会同时更改向量的值 - 或者为了加法的目的而决定,这真的不重要。
- 这个优化可以看作是一个 Strength Reduction - 从堆上的操作到寄存器上的操作 - 但我不确定这个术语是否用于这种情况。
我正在尝试使用 OpenMP 复制错误共享的效果,如 OpenMP introduction by Tim Mattson 中所述。
我的程序执行了一个简单的数值积分(请参阅 link 了解数学细节)并且我已经实现了两个版本,第一个应该是缓存友好的,每个线程都保留一个本地变量来累积它在索引中的部分 space,
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
// ...
而第二个版本让每个线程更新共享 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
// ...
问题是除非我关闭优化,否则我无法看到虚假共享的任何影响。
使用没有优化 (-O0) 的 GCC 8.1 编译我的代码(它必须考虑比上面的代码片段更多的细节)产生我天真的预期的结果,而使用完全优化 (-O3) 消除了任何差异两个版本之间的性能方面,如图所示。
这是怎么解释的?编译器真的消除了虚假共享吗?如果不是,怎么运行优化后的代码效果这么小?
我在使用 Fedora 的 Core-i7 机器上。该图显示平均值,其样本标准偏差不会为该问题添加任何信息。
tl;dr:编译器将您的第二个版本优化为第一个。
考虑第二个实现循环中的代码 - 暂时忽略它的 OMP/multithreaded 方面。
您在 std::vector
中有一个值的增量 - 它必须位于堆上(好吧,直到并包括在 C++17 中)。编译器看到你在一个循环中添加到堆上的一个值;这是一个典型的优化候选者:它将堆访问从循环中取出,并使用寄存器作为缓冲区。它甚至不需要从堆中 读取 ,因为它们只是添加 - 所以它基本上到达了您的第一个解决方案。
See this happening on GodBolt(用一个简化的例子)- 注意 bar1()
和 bar2()
的代码几乎是一样的,累加发生在寄存器中。
现在,涉及多线程和 OMP 的事实并没有改变上述情况。例如,如果您使用 std::atomic<double>
而不是 double
,那么它 可能 已更改(如果编译器足够聪明,甚至可能不会更改) .
备注:
- 感谢@Evg 注意到此答案先前版本的代码中存在明显错误。
- 编译器必须能够知道
func()
不会同时更改向量的值 - 或者为了加法的目的而决定,这真的不重要。 - 这个优化可以看作是一个 Strength Reduction - 从堆上的操作到寄存器上的操作 - 但我不确定这个术语是否用于这种情况。