【发布时间】:2013-10-29 22:06:29
【问题描述】:
我正在寻找一些关于如何使用 SSE 进行并行前缀求和的建议。我有兴趣在整数、浮点数或双精度数组上执行此操作。
我想出了两个解决方案。一个特例和一个一般情况。在这两种情况下,解决方案都在与 OpenMP 并行的两遍中在阵列上运行。对于特殊情况,我在两次通行证上都使用 SSE。对于一般情况,我只在第二遍使用它。
我的主要问题是在一般情况下如何在第一次通过时使用 SSE? 以下链接 simd-prefix-sum-on-intel-cpu 显示了字节的改进,但 32 位数据类型没有。
特殊情况被称为特殊的原因是它要求数组采用特殊格式。例如,假设数组aof 浮点数只有 16 个元素。那么如果数组是这样重新排列的(结构数组到数组结构):
a[0] a[1] ...a[15] -> a[0] a[4] a[8] a[12] a[1] a[5] a[9] a[13]...a[3] a[7] a[11] a[15]
SSE 垂直总和可用于两个通道。但是,这只有在数组已经采用特殊格式并且输出可以以特殊格式使用时才有效。否则必须对输入和输出进行昂贵的重新排列,这将使其比一般情况慢得多。
也许我应该为前缀和考虑一种不同的算法(例如二叉树)?
一般情况的代码:
void prefix_sum_omp_sse(double a[], double s[], int n) {
double *suma;
#pragma omp parallel
{
const int ithread = omp_get_thread_num();
const int nthreads = omp_get_num_threads();
#pragma omp single
{
suma = new double[nthreads + 1];
suma[0] = 0;
}
double sum = 0;
#pragma omp for schedule(static) nowait //first parallel pass
for (int i = 0; i<n; i++) {
sum += a[i];
s[i] = sum;
}
suma[ithread + 1] = sum;
#pragma omp barrier
#pragma omp single
{
double tmp = 0;
for (int i = 0; i<(nthreads + 1); i++) {
tmp += suma[i];
suma[i] = tmp;
}
}
__m128d offset = _mm_set1_pd(suma[ithread]);
#pragma omp for schedule(static) //second parallel pass with SSE as well
for (int i = 0; i<n/4; i++) {
__m128d tmp1 = _mm_load_pd(&s[4*i]);
tmp1 = _mm_add_pd(tmp1, offset);
__m128d tmp2 = _mm_load_pd(&s[4*i+2]);
tmp2 = _mm_add_pd(tmp2, offset);
_mm_store_pd(&s[4*i], tmp1);
_mm_store_pd(&s[4*i+2], tmp2);
}
}
delete[] suma;
}
【问题讨论】:
-
我虽然像 gcc/icc 这样的编译器可以为第二部分做自动矢量化,所以你不需要使用 SIMD 内在函数。与带有一些编译器选项(如
-msse2)的纯 c 代码相比,您是否获得了性能提升 -
他们可能会。我在 MSVC2013 上运行了这个。它不会自动矢量化第二遍。我对 MSVC 的经验是,当您使用 OpenMP 时,您必须自己进行矢量化。我认为他们中的任何一个都不会为您使用 SSE 代码展开循环,但无论如何它在这种情况下无济于事。
-
针对性能问题,我发布的一般代码比在我的 4 核常春藤桥系统上启用 AVX 的发布模式下的顺序代码快 3 倍以上。时间成本应该是
n/ncores*(1+1/SIMD_width)。所以对于 4 核和 SIMD_width=2(双),应该是 3n/8。这大约是 2.7 倍的加速。超线程有一点帮助,所以我猜这会超过 3 个(我使用 8 个线程。当我尝试 4 个线程时,性能会下降一点)。 -
您可能要提到由于使用
_mm_load_ps,输入和输出数组需要16字节对齐,但float *在一般情况下只有4字节对齐。