为什么所有内核上的 sin(Vector) 可以和一个内核上的 sin(V) 一样快?

Why can sin(Vector) on all cores be as fast as sin(V) on one core?

我有一个简单的 C++ 代码,它 运行 是跨值向量的默认 sin 函数。

static void BM_sin() {
        int data_size = 100000000;
        double lower_bound = 0;
        double upper_bound = 1;
        random_device device;
        mt19937 engine(device());
        uniform_real_distribution<double> distribution(lower_bound, upper_bound);
        auto generator = bind(distribution, engine);
        vector<double> data(data_size);
        generate(begin(data), end(data), generator);
        #pragma omp parallel for
        for(int i = 0; i < data_size; ++i) {
                data[i] = sin(data[i]);
        }
        cout << accumulate(data.begin(), data.end(), 0) << endl;
}

当我 运行 将此函数设置为 18 具有 8 个内核时,我得到了相同的时间。注释行 #pragma omp parallel for 也无济于事。所以我想知道为什么从所有线程应用于向量的正弦曲线与从一个线程应用于向量一样快?

(我在 gcc-4.8 上用 -Ofast -fopenmp 编译)

简单的回答很简单:

  • 并非所有事情都能很好地扩展。我不知道 fast_sin,但可能主要是内存带宽受限。在那种情况下,将工作负载分配给多个核心将一事无成。
  • 另外,我怀疑你的测量方法。如果您的生成器是 mt19337,它比您的正弦波复杂得多,因此并行化您的正弦波并没有太大作用,因为大部分时间都花在了生成随机数上。

你测量错了。发生器环路很慢,但还没有慢到完全盖过正弦环路的地步。以下是在两种不同的 Intel 架构上测量几个代码部分的执行速度的结果:

 Code part             | WM (x64) | WM (x86) | SB (x64) | SB (x86)
-----------------------+----------+----------+----------+----------
 generate()            |  1,45 s  |  2,44 s  |  1,28 s  |  2,18 s
 sine loop (serial)    |  2,17 s  |  2,88 s  |  1,80 s  |  2,91 s
 sine loop (6 threads) |  0,37 s  |  0,51 s  |  0,31 s  |  0,52 s
 accumulate()          |  0,31 s  |  0,70 s  |  0,33 s  |  0,67 s
-----------------------+----------+----------+----------+----------
 speed-up: overall     |  1,85x   |  1,65x   |  1,78x   |  1,71x
 speed-up: sine loop   |  5,86x   |  5,65x   |  5,81x   |  5,60x
 speed-up: Amdahl      |  2,23x   |  1,92x   |  2,12x   |  2,02x

在上面table中,WM代表Intel X5675,一个WestmereCPU,而SB代表Intel E5-2650,一个Sandy BridgeCPU。 x64 代表 64 位模式,x86 代表 32 位模式。 GCC 4.8.5 与 -Ofast -fopenmp -mtune=native 一起使用(-m32 用于 32 位模式)。两个系统都是 运行 CentOS 7.2。执行时间只是近似值,因为我没有通过取多次执行的平均值来正确计时。使用 portable omp_get_wtime() 计时器程序完成计时。

如您所见,6 个线程的整体加速范围从 1.65 倍到 1.85 倍,而单独正弦循环的加速范围从 5.60 倍到 5.86 倍。生成器循环和累加器循环都是串行执行的,这限制了并行加速(参见Amdahl's law)。

这里有两点需要注意。第一个,如果向量的内存是预先故障的,生成器循环可能会更快一点。它基本上意味着扫过向量并触及支持它的每个内存页面。 运行 生成器循环两次并且仅对第二次调用计时也可以解决问题。在我的系统上,这并没有带来明显的优势(节省与测量误差在同一数量级),很可能是因为 CentOS 的内核默认打开了透明大页面。

第二件事是accumulate()的最后一个参数是一个整数0,因此算法每次都被迫执行整数转换,这大大减慢了速度并在最后给出了错误的结果( 0). accumulate(data.begin(), data.end(), 0.0) 执行速度快了十倍,而且还产生了正确的结果。