【发布时间】:2017-08-11 05:27:24
【问题描述】:
我是 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.519
2 threads: 89.045
4 threads: 90.361
计算似乎令人尴尬地并行,因为不同的线程可以在给定不同索引的情况下同时加载所需的数据,写入结果的情况类似,但为什么没有加速?
更多信息:
我使用
icpc -g -std=c++11 -march=core-avx2 -O3 -qopenmp -S test.cpp检查了汇编代码,发现生成了向量化指令;
1234563 @。
欢迎任何建议或线索。
EDIT-1:
感谢@JerryCoffin 指出可能的原因,所以我使用 Vtune 进行了内存访问分析。结果如下:
1-thread: Memory Bound: 6.5%, L1 Bound: 0.134, L3 Latency: 0.039
2-threads: Memory Bound: 18.0%, L1 Bound: 0.115, L3 Latency: 0.015
4-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的优势,但是计算/内存访问比率无论循环次数都没有变化,而且局部性似乎也不是原因,因为src或dst太大了留在任何缓存中,因此连续迭代之间不存在任何关系。
有什么建议吗?
编辑 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.
问题依然存在。 :(
EDIT-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。我可以得出这样的结论,看似尴尬的并行实际上是内存绑定的(瓶颈是加载/存储操作)?如果是这样,为什么不能很好地并行加载/存储操作?
【问题讨论】:
-
我将首先查看您用于主内存的带宽。至少乍一看,这看起来很有可能是内存绑定的,即使只有一个内核——在这种情况下,添加更多内核并不会带来太多好处。
-
@JerryCoffin 感谢您提供可能的线索。你的意思是大部分时间都花在加载/存储而不是计算上?最初我虽然内存访问局部性很好,因为它们是每个线程的连续读/写。我将使用 vtune 分析内存访问并稍后更新。如果这是问题所在,您对解决它有什么建议吗?
-
您的时间可能会因
dst上的页面错误而出现偏差(由于延迟分配)- 要么显式初始化dst,和/或在外部循环中运行你的基准测试超过 2 次或更多次迭代和忽略第一次迭代的时间。 -
您的整个数据集,src 和 dst 组合为 1.2GByte。您对 src 中的每个项目进行的处理是非常轻量级的,因此感觉应该是受内存限制的。但是,将整个 1.2GBytes 读/写到 SDRAM 应该不超过 51 毫秒,因此这里还有一些其他开销。非常值得尝试@PaulR 的建议
-
另一件要注意的事情:您的循环目前只需要大约 90 毫秒 - 因此任何类型的线程/OMP/任何并行处理的粒度可能太小而无法产生任何显着的好处 - 尝试增加
size一个数量级(或添加外循环)?
标签: c++ multithreading performance openmp simd