SSE/AVX + OpenMP:数组的快速求和
Posted
技术标签:
【中文标题】SSE/AVX + OpenMP:数组的快速求和【英文标题】:SSE/AVX + OpenMP: fast sum of arrays 【发布时间】:2013-03-07 12:54:25 【问题描述】:我将两个数组相加并输出第三个数组(不是减少)。像这样:
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 四个,四个内核四个)。我已经用 SSE(和 AVX)实现了代码。 Visual Studio 2012 具有自动矢量化功能,但我通过“展开循环”获得了更好的结果。我为具有四种大小的数组(32 字节对齐)运行我的代码:小于 32KB、小于 256KB、小于 8MB 和大于 8MB 的内核对应于 L1、L2、L3 缓存和主内存。对于 L1,使用展开的 SSE 代码(使用 AVX 的 5-6),我看到了大约 4 倍的加速。这和我预期的一样多。之后每个缓存级别的效率都会下降。然后我使用 OpenMP 在每个内核上运行。我在数组的主循环之前放置了“#pragma omp parallel for”。但是,我得到的最佳加速是 SSE + OpenMP 的 5-6 倍。有谁知道为什么我没有看到 16 倍的加速?也许是由于数组从系统内存到缓存的一些“上传”时间?我意识到我应该分析代码,但这本身就是我必须学习的另一种冒险。
#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);
【问题讨论】:
【参考方案1】:免责声明:和你一样,我没有分析代码,所以我不能绝对肯定地回答。
您的问题很可能与内存带宽或并行化开销有关。
您的循环计算量非常轻,因为它会为 3 次内存操作添加 1 次,因此您自然会受到内存带宽的限制(考虑到 ALU 吞吐量比现代架构中的内存带宽要好得多)。因此,您的大部分时间都花在传输数据上。
如果数据足够小以适合缓存,您可以(理论上)将 openmp 线程绑定到特定内核并确保向量的正确部分位于特定内核的 L1/L2 缓存中,但这不会真的很有帮助,除非您可以并行化初始化(传输数据时并不重要,如果您必须这样做)。因此,将数据从一个核心缓存传输到另一个核心缓存会受到打击。
如果数据不适合处理器缓存,您最终会受到主内存带宽的限制。由于预取一个内核可能几乎能够为这种简单的访问模式最大限度地利用带宽,给您的增长空间很小。
要记住的第二点是,omp parallel
构造的创建和循环的分发有一定的开销。对于小型数据集(适合 L1/L2/L3 的数据集可能符合条件),此开销很容易与计算时间本身一样高,几乎没有加速。
【讨论】:
如何将 OpenMP 线程绑定到特定内核?这更多地用于实验(确定可以做什么的上限),因此数据大小就是我所做的。我为我的主循环添加了代码。我在数组上循环了几次,所以我认为我消除了 OpenMP 开销。 @raxman:取决于您的 OpenMP 运行时。对于 GOpenMP,它将是 GOMP_CPU_AFFINITY 环境变量,对其他人一无所知。但是看看你的主循环,我很惊讶你实际上得到了加速:你并行执行add_vector
,同时执行所有add_vector
操作在相同数据上。这是 a) 竞争条件(来自同一地址的多个线程的不受保护的写入)和 b) 永久地使内核在彼此之间交换它们的缓存线
@raxman 我怀疑线程绑定会做很多事情。对于适合缓存的大小,加载/存储吞吐量将成为瓶颈。
@Grizzly 我认为你是对的。我不认为我有比赛条件,但我明白你的意思。我会检查并回复你。对于我的代码计时,您有比多次循环数组并使用计时器更好的建议吗?
@raxman:嗯,你可以为每个线程使用不同的结果数组,这将解决竞争条件和缓存乒乓(当然增加了进程中的工作集,所以你的 L3 大小将关闭)。真正的问题是您实际上要测量的内容。毕竟多次处理相同的数据有点浪费,所以这可能不是你在现实中实际得到的场景。那么您如何看待现实中的并行化?以上是关于SSE/AVX + OpenMP:数组的快速求和的主要内容,如果未能解决你的问题,请参考以下文章
MSVC /arch:[指令集] - SSE3、AVX、AVX2