c++ - 使用 openmp + SIMD 没有加速

标签 c++ multithreading performance openmp simd

我是 Openmp 的新手,现在尝试使用 Openmp + SIMD 内在函数来加速我的程序,但结果远非预期。

为了在不丢失太多基本信息的情况下简化案例,我写了一个更简单的玩具示例:

#include <omp.h>
#include <stdlib.h>
#include <iostream>
#include <vector>
#include <sys/time.h>

#include "immintrin.h" // for SIMD intrinsics

int main() {
    int64_t size = 160000000;
    std::vector<int> src(size);

    // generating random src data
    for (int i = 0; i < size; ++i)
        src[i] = (rand() / (float)RAND_MAX) * size;

    // to store the final results, so size is the same as src
    std::vector<int> dst(size);

    // get pointers for vector load and store
    int * src_ptr = src.data();
    int * dst_ptr = dst.data();

    __m256i vec_src;
    __m256i vec_op = _mm256_set1_epi32(2);
    __m256i vec_dst;

    omp_set_num_threads(4); // you can change thread count here

    // only measure the parallel part
    struct timeval one, two;
    double get_time;
    gettimeofday (&one, NULL);

    #pragma omp parallel for private(vec_src, vec_op, vec_dst)
    for (int64_t i = 0; i < size; i += 8) {
        // load needed data
        vec_src = _mm256_loadu_si256((__m256i const *)(src_ptr + i));

        // computation part
        vec_dst = _mm256_add_epi32(vec_src, vec_op);
        vec_dst = _mm256_mullo_epi32(vec_dst, vec_src);
        vec_dst = _mm256_slli_epi32(vec_dst, 1);
        vec_dst = _mm256_add_epi32(vec_dst, vec_src);
        vec_dst = _mm256_sub_epi32(vec_dst, vec_src);

        // store results
        _mm256_storeu_si256((__m256i *)(dst_ptr + i), vec_dst);
    }

    gettimeofday(&two, NULL);
    double oneD = one.tv_sec + (double)one.tv_usec * .000001;
    double twoD = two.tv_sec + (double)two.tv_usec * .000001;
    get_time = 1000 * (twoD - oneD);
    std::cout << "took time: " << get_time << std::endl;

    // output something in case the computation is optimized out
    int64_t i = (int)((rand() / (float)RAND_MAX) * size);
    for (int64_t i = 0; i < size; ++i)
        std::cout << i << ": " << dst[i] << std::endl;

    return 0;
}

它是使用 icpc -g -std=c++11 -march=core-avx2 -O3 -qopenmp test.cpp -o test 编译的并测量平行部分的耗时。结果如下(中间值是从 5 次运行中挑选出来的):
1 thread: 92.5192 threads: 89.0454 threads: 90.361
计算似乎令人尴尬地并行,因为不同的线程可以在给定不同的索引的情况下同时加载所需的数据,并且写入结果的情况类似,但为什么没有加速?

更多信息:
  • 我使用 icpc -g -std=c++11 -march=core-avx2 -O3 -qopenmp -S test.cpp 检查了汇编代码并生成找到的向量化指令;
  • 为了检查它是否受内存限制,我在循环中注释了计算部分,并且测量的时间减少到了60 左右。 ,但如果我从 1 -> 2 -> 4 更改线程数,它不会有太大变化.

  • 欢迎任何建议或线索。

    编辑 1:

    感谢@JerryCoffin 指出可能的原因,所以我使用 Vtune 进行了内存访问分析。结果如下:
    1-thread: Memory Bound: 6.5%, L1 Bound: 0.134, L3 Latency: 0.0392-threads: Memory Bound: 18.0%, L1 Bound: 0.115, L3 Latency: 0.0154-threads: Memory Bound: 21.6%, L1 Bound: 0.213, L3 Latency: 0.003
    它是 Intel 4770 处理器,最大速度为 25.6GB/s(Vtune 测量为 23GB/s)。带宽。内存限制确实增加了,但我仍然不确定这是否是原因。有什么建议吗?

    EDIT-2(只是想提供详尽的信息,所以附加的内容可能很长但不乏味):

    感谢@PaulR 和@bazza 的建议。我尝试了 3 种方法进行比较。需要注意的一件事是处理器有4核心和 8硬件线程。结果如下:

    (1) 只需初始化 dst作为提前全零:1 thread: 91.922; 2 threads: 93.170; 4 threads: 93.868 --- 似乎没有效果;

    (2)不带(1),将并行部分放在外循环100次迭代,测100次迭代的时间:1 thread: 9109.49; 2 threads: 4951.20; 4 threads: 2511.01; 8 threads: 2861.75 --- 非常有效,除了 8 个线程;

    (3) 在(2)的基础上,在100次迭代前多放1次迭代,测量100次迭代的时间:1 thread: 9078.02; 2 threads: 4956.66; 4 threads: 2516.93; 8 threads: 2088.88 --- 与 (2) 类似,但对 8 个线程更有效。

    似乎更多的迭代可以暴露openmp + SIMD的优势,但是无论循环次数如何,计算/内存访问比率都没有变化,而且locality似乎也不是原因,因为srcdst太大而无法留在任何缓存中,因此连续迭代之间不存在关系。

    有什么建议吗?

    编辑 3:

    万一误导,需要澄清一件事:在(2)和(3)中,openmp指令在添加的外循环之外
    #pragma omp parallel for private(vec_src, vec_op, vec_dst)
    for (int k = 0; k < 100; ++k) {
        for (int64_t i = 0; i < size; i += 8) {
            ......
        }
    }
    

    即外循环使用多线程并行化,内循环仍然串行处理。因此(2)和(3)中的有效加速可能通过增强线程之间的局部性来实现。

    我做了另一个实验,将 openmp 指令放在外循环中:
    for (int k = 0; k < 100; ++k) {
        #pragma omp parallel for private(vec_src, vec_op, vec_dst)
        for (int64_t i = 0; i < size; i += 8) {
            ......
        }
    }
    

    而且加速还是不好:1 thread: 9074.18; 2 threads: 8809.36; 4 threads: 8936.89.93; 8 threads: 9098.83 .

    问题依然存在。 :(

    编辑 4:

    如果我用这样的标量操作替换矢量化部分(相同的计算,但以标量方式):
    #pragma omp parallel for
    for (int64_t i = 0; i < size; i++) { // not i += 8
        int query = src[i];
        int res = src[i] + 2;
        res = res * query;
        res = res << 1;
        res = res + query;
        res = res - query;
        dst[i] = res;
    }
    

    加速是1 thread: 92.065; 2 threads: 89.432; 4 threads: 88.864 .我可以得出结论,看似令人尴尬的并行实际上是内存限制(瓶颈是加载/存储操作)?如果是这样,为什么不能很好地并行加载/存储操作?

    最佳答案

    May I come to the conclusion that the seemingly embarassing parallel is actually memory bound (the bottleneck is load / store operations)? If so, why can't load / store operations well parallelized?



    是的,这个问题是令人尴尬的并行,因为缺乏依赖关系很容易并行化。这并不意味着它会完美地扩展。您仍然可能有一个糟糕的初始化开销与工作比率或共享资源限制您的加速。

    在您的情况下,您确实受到内存带宽的限制。首先一个实际的考虑:当用 icpc(16.0.3 或 17.0.1)编译时,“标量”版本在 size 时产生更好的代码。制成 constexpr .这不是因为它优化了这两条冗余线:
    res = res + query;
    res = res - query;
    

    确实如此,但这没什么区别。主要是编译器使用与您对内在函数执行的指令完全相同的指令,但存储除外。在商店前,它使用 vmovntdq而不是 vmovdqu ,利用有关程序、内存和体系结构的复杂知识。不仅vmovntdq需要对齐的内存,因此效率更高。它给 CPU 一个非时间性的提示,防止在写入内存期间缓存这些数据。这提高了性能,因为将其写入缓存需要从内存加载缓存行的其余部分。因此,虽然您的初始 SIMD 版本确实需要三个内存操作:读取源、读取目标缓存行、写入目标,但具有非临时存储的编译器版本只需要两个。事实上,在我的 i7-4770 系统上,编译器生成的版本将 2 个线程的运行时间从 ~85.8 毫秒减少到 58.0 毫秒,并且几乎完美的 1.5 倍加速。这里的教训是信任你的编译器,除非你非常了解架构和指令集。

    考虑到这里的峰值性能,传输 2*160000000*4 字节的 58 ms 对应于 22.07 GB/s(汇总读写),这与您的 VTune 结果大致相同。 (很有趣,考虑到 85.8 ms 与两次读取、一次写入的带宽大致相同)。没有更直接的改进空间。

    为了进一步提高性能,您必须对代码的操作/字节比率进行一些处理。请记住,您的处理器可以执行 217.6 GFLOP/s(我猜 int ops 可能是相同的或两倍),但只能读写 3.2 G int/s。这让您了解需要执行多少操作才能不受内存限制。因此,如果可以,请在块中处理数据,以便您可以重用缓存中的数据。

    我无法重现您对 (2) 和 (3) 的结果。当我环绕内部循环时,缩放的行为相同。结果看起来很可疑,特别是考虑到结果与其他方面的峰值性能如此一致。一般来说,我建议在平行区域内进行测量并利用omp_get_wtime像这样:
      double one, two;
    #pragma omp parallel 
      {
        __m256i vec_src;
        __m256i vec_op = _mm256_set1_epi32(2);   
        __m256i vec_dst;
    
    #pragma omp master
        one = omp_get_wtime();
    #pragma omp barrier
        for (int kk = 0; kk < 100; kk++)
    #pragma omp for
        for (int64_t i = 0; i < size; i += 8) {
            ...
        }
    #pragma omp master
        {
          two = omp_get_wtime();
          std::cout << "took time: " << (two-one) * 1000 << std::endl;
        }
      }
    

    最后一句话:台式机处理器和服务器处理器在内存性能方面有着非常不同的特性。在现代服务器处理器上,您需要更多事件线程来使内存带宽饱和,而在台式机处理器上,一个内核通常几乎可以使内存带宽饱和。

    编辑:关于 VTune 没有将其归类为内存限制的另一个想法。这可能是由于与初始化相比计算时间短。尝试查看 VTune 对循环中的代码有何看法。

    关于c++ - 使用 openmp + SIMD 没有加速,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42895066/

    相关文章:

    c++ - 使用 STL std::sort 的方式作为 qsort_r

    c++ - 将 C++11 枚举类作为模板传递,同时自动推导其类型

    c++ - 如何将指针函数转换为类函数? C++

    python - 如何使用 threading 模块暂停和恢复线程?

    python - 线程池类似于多处理池?

    c++ - 在 C++ 中使用位运算符还是使用 if 语句更快?

    .net - 查找 .NET 多线程瓶颈

    javascript - CSS 动画性能基准

    c++ - 为什么 '[[fallthrough]]' 需要方括号 '[[]]' ?

    java - 多线程异常处理释放资源