【发布时间】:2017-07-22 09:13:54
【问题描述】:
我在一个系统上观察到,与恒定值 1 或动态值相比,在设置恒定值 0 时,大型 std::vector<int> 上的 std::fill 明显且始终较慢:
5.8 GiB/s 与 7.5 GiB/s
但是,对于较小的数据大小,结果会有所不同,fill(0) 更快:
使用多个线程,数据大小为 4 GiB,fill(1) 显示出更高的斜率,但达到的峰值比fill(0) 低得多(51 GiB/s vs 90 GiB/s):
这就引出了第二个问题,为什么fill(1) 的峰值带宽要低这么多。
测试系统是双插槽 Intel Xeon CPU E5-2680 v3,频率为 2.5 GHz(通过/sys/cpufreq),配备 8x16 GiB DDR4-2133。我使用 GCC 6.1.0 (-O3) 和 Intel 编译器 17.0.1 (-fast) 进行了测试,都得到了相同的结果。 GOMP_CPU_AFFINITY=0,12,1,13,2,14,3,15,4,16,5,17,6,18,7,19,8,20,9,21,10,22,11,23 已设置。 Strem/add/24 线程在系统上获得 85 GiB/s。
我能够在不同的 Haswell 双路服务器系统上重现这种效果,但不能在任何其他架构上重现。例如在 Sandy Bridge EP 上,内存性能是相同的,而在缓存中 fill(0) 更快。
这里是重现的代码:
#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <omp.h>
#include <vector>
using value = int;
using vector = std::vector<value>;
constexpr size_t write_size = 8ll * 1024 * 1024 * 1024;
constexpr size_t max_data_size = 4ll * 1024 * 1024 * 1024;
void __attribute__((noinline)) fill0(vector& v) {
std::fill(v.begin(), v.end(), 0);
}
void __attribute__((noinline)) fill1(vector& v) {
std::fill(v.begin(), v.end(), 1);
}
void bench(size_t data_size, int nthreads) {
#pragma omp parallel num_threads(nthreads)
{
vector v(data_size / (sizeof(value) * nthreads));
auto repeat = write_size / data_size;
#pragma omp barrier
auto t0 = omp_get_wtime();
for (auto r = 0; r < repeat; r++)
fill0(v);
#pragma omp barrier
auto t1 = omp_get_wtime();
for (auto r = 0; r < repeat; r++)
fill1(v);
#pragma omp barrier
auto t2 = omp_get_wtime();
#pragma omp master
std::cout << data_size << ", " << nthreads << ", " << write_size / (t1 - t0) << ", "
<< write_size / (t2 - t1) << "\n";
}
}
int main(int argc, const char* argv[]) {
std::cout << "size,nthreads,fill0,fill1\n";
for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
bench(bytes, 1);
}
for (size_t bytes = 1024; bytes <= max_data_size; bytes *= 2) {
bench(bytes, omp_get_max_threads());
}
for (int nthreads = 1; nthreads <= omp_get_max_threads(); nthreads++) {
bench(max_data_size, nthreads);
}
}
使用g++ fillbench.cpp -O3 -o fillbench_gcc -fopenmp 编译的呈现结果。
【问题讨论】:
-
比较线程数时
data size是什么? -
@GavinPortwood 4 GiB,所以在内存中,而不是缓存中。
-
那么第二个情节一定有问题,即弱缩放。我无法想象用最少的中间操作来使循环的内存带宽饱和需要两个左右的线程。实际上,即使在 24 个线程时,您也没有确定带宽饱和的线程数。你能证明它在某些有限的线程数下确实会变平吗?
-
我怀疑原始实验中的异常缩放(在第二个套接字上)与非同质内存分配和由此产生的 QPI 通信有关。这可以通过英特尔的“非核心”PMU 来验证(我认为)
-
FWIW - 您在答案中发现了代码差异,我认为 Peter Cordes 的答案如下:
rep stosb正在使用非 RFO 协议,该协议将执行填充所需的事务数量减半.其余的行为大多不在此范围内。fill(1)代码还有另一个缺点:它不能使用 256 位 AVX 存储,因为您没有指定-march=haswell或其他任何内容,因此它必须回退到 128 位代码。调用memset的fill(0)获得调用平台上AVX 版本的libc调度的优势。
标签: c++ performance x86 compiler-optimization memset