我正在求和两个数组并输出第三个数组(不是缩减)。像这样:
void add_scalar(float* result, const float* a, const float* b, const int N) {
for(int i = 0; i<N; i++) {
result[i] = a[i] + b[i];
}
}
我想以最大吞吐量来做这件事。使用SSE和四个内核,我天真地期望加速16倍(SSE为4,四个内核为4)。我用SSE(和AVX)实现了代码。 Visual Studio 2012具有自动矢量化功能,但是通过&#34;展开循环&#34;我可以获得更好的结果。我运行我的代码用于阵列(具有32字节对齐),具有四种大小:小于32KB,小于256KB,小于8MB,以及对L1,L2,L3高速缓存和主存储器的应对大于8 MB。对于L1,我使用我的展开的SSE代码(使用AVX的5-6)看到大约4倍的加速。这和我期望的一样多。之后,每个缓存级别的效率都会下降。然后我使用OpenMP在每个核心上运行。我把&#34; #pragma omp并行用于&#34;在我的主循环数组之前。但是,我获得的最佳加速比是SSE + OpenMP的5-6倍。有没有人知道为什么我没有看到16倍的加速?也许是因为某些&#34;上传&#34;数组从系统内存到缓存的时间?我意识到我应该对代码进行分析,但这本身就是我必须学习的另一种冒险。
#define ROUND_DOWN(x, s) ((x) & ~((s)-1))
void add_vector(float* result, const float* a, const float* b, const int N) {
__m128 a4;
__m128 b4;
__m128 sum;
int i = 0;
for(; i < ROUND_DOWN(N, 8); i+=8) {
a4 = _mm_load_ps(a + i);
b4 = _mm_load_ps(b + i);
sum = _mm_add_ps(a4, b4);
_mm_store_ps(result + i, sum);
a4 = _mm_load_ps(a + i + 4);
b4 = _mm_load_ps(b + i + 4);
sum = _mm_add_ps(a4, b4);
_mm_store_ps(result + i + 4, sum);
}
for(; i < N; i++) {
result[i] = a[i] + b[i];
}
return 0;
}
我的错误主循环有一个类似的竞争条件:
float *a = (float*)_aligned_malloc(N*sizeof(float), 32);
float *b = (float*)_aligned_malloc(N*sizeof(float), 32);
float *c = (float*)_aligned_malloc(N*sizeof(float), 32);
#pragma omp parallel for
for(int n=0; n<M; n++) { //M is an integer of the number of times to run over the array
add_vector(c, a, b, N);
}
根据Grizzly的建议纠正了主循环:
for(int i=0; i<4; i++) {
results[i] = (float*)_aligned_malloc(N*sizeof(float), 32);
}
#pragma omp parallel for num_threads(4)
for(int t=0; t<4; t++) {
for(int n=0; n<M/4; n++) { //M is an integer of the number of times to run over the array
add_vector(results[t], a, b, N);
}
}
答案 0 :(得分:6)
免责声明:就像你一样,我没有对代码进行描述,所以我无法绝对肯定地回答。
您的问题很可能与内存带宽或并行化开销有关。
你的循环计算很轻,因为它为3个内存操作添加1个,这使你自然受到内存带宽的限制(考虑到ALU吞吐量比现代架构中的内存带宽要好得多)。因此,大部分时间都花在转移数据上。
如果数据足够小以适应缓存,您可以(理论上)将openmp线程绑定到特定核心,并确保向量的正确部分位于特定核心的L1 / L2缓存中,但是赢得了#39 ; t真的有帮助,除非你可以并行化初始化(当你传输数据时它并不重要,如果你必须这样做的话)。因此,您可以将数据从一个核心缓存传输到另一个核心缓存。
如果数据不适合处理器缓存,则最终会受到主内存带宽的限制。由于预取,一个核心可能几乎可以为这种简单的访问模式带来最大的带宽,使您几乎没有成长空间。
要记住的第二点是,创建omp parallel
构造并分发循环会产生一定的开销。对于小型数据集(适合L1 / L2 / L3的数据集可能符合条件),这种开销很容易与计算时间本身一样高,几乎没有加速。