通过索引集对非连续元素进行矢量化

时间:2018-11-11 05:31:53

标签: c++ compiler-optimization

矢量化的标准模板似乎是这样的:

#define N 100
double arr[N];
double func(int i);

for(int i = 0; i <N; i++)
    arr[i] = func(i);

连续访问所有索引的地方。

但是,我遇到的情况是并非所有N的{​​{1}}个元素都需要更新。我拥有的模板如下:

arr

在这种情况下,#define N 100 double arr[N]; double func(int i); int indexset[N];//this indexset has the indices of arr[] that get updated int number_in_index_set; //E.g., if I only need to update arr[4] and arr[10], number_in_index_set = 2 //indexset[0] = 4 and indexset[1] = 10 for(int i = 0; i <number_in_index_set; i++) arr[indexset[i]] = func(indexset[i]); 报告此循环未向量化,因为在执行循环之前无法计算循环迭代计数。在我的应用程序中,此循环是针对索引的不同子集执行的,对于每个这样的子集,Intel Advisornumber_in_index_set都会相应地更改。

我有两个问题:

(1)问题循环甚至被矢量化意味着什么?数组索引不是连续的,那么编译器将如何对循环进行矢量化处理?

(2)如indexset[]所暗示的那样,假设可以进行向量化,那么如何安全地对所讨论的循环进行向量化?因此,Intel Advisor的建议是:

Intel Advisor

具体来说,可以保证上述哪些建议可以确保安全的矢量化?

1 个答案:

答案 0 :(得分:2)

基于其他注释,假设您要优化的功能如下:

error

因为您的目标是奔腾,所以只有SSE2 SIMD指令可用。您可以尝试以下优化功能(也可以使用here):

void euclidean(double x0, double y0, const double *x, const double* y,
               const size_t *index, size_t index_size, double *result)
{
    for (size_t i = 0; i < index_size; ++i) {
        double dx = x0 - x[index[i]];
        double dy = y0 - y[index[i]];
        result[index[i]] = sqrt(dx*dx + dy * dy);
    }
}

此处,加载和存储操作未向量化,也就是说,我们正在逐一加载void euclidean_sse2(double x0, double y0, const double *x, const double* y, const size_t *index, size_t index_size, double *result) { __m128d vx0 = _mm_set1_pd(x0); __m128d vy0 = _mm_set1_pd(y0); for (size_t i = 0; i + 1 < index_size; i += 2) { // process 2 at a time size_t i0 = index[i]; size_t i1 = index[i+1]; __m128d vx = _mm_set_pd(x[i1], x[i0]); // load 2 scattered elements __m128d vy = _mm_set_pd(y[i1], y[i0]); // load 2 scattered elements __m128d vdx = _mm_sub_pd(vx, vx0); __m128d vdy = _mm_sub_pd(vy, vy0); __m128d vr = _mm_sqrt_pd(_mm_add_pd(_mm_mul_pd(vdx, vdx), _mm_mul_pd(vdy, vdy))); _mm_store_sd(result + i0, vr); _mm_storeh_pd(result + i1, vr); } if (index_size & 1) { // if index_size is odd, there is one more element to process size_t i0 = index[index_size-1]; double dx = x0 - x[i0]; double dy = y0 - y[i0]; result[i0] = sqrt(dx*dx + dy * dy); } } x,然后一一存储到y中。所有其他操作均已向量化。

使用gcc时,主循环主体的汇编器输出如下。

result

您还可以进行一些循环展开(这可能是编译器矢量化器将执行的操作)。但是在这种情况下,循环体非常庞大,并且可能受内存I / O限制,因此我认为这不会有太大帮助。

如果这些功能的含义不清楚,您可以阅读this

关于矢量化的全面讨论是here。简而言之,现代cpu除了常规寄存器外还提供矢量寄存器。根据机器架构,我们有128位寄存器(SSE指令),256位寄存器(AVX指令),512位寄存器(AVX512指令)。向量化意味着充分利用这些寄存器。例如,如果我们有一个 // scalar load operations mov r10, QWORD PTR [rax] mov r9, QWORD PTR [rax+8] add rax, 16 movsd xmm3, QWORD PTR [rdi+r10*8] movsd xmm2, QWORD PTR [rsi+r10*8] movhpd xmm3, QWORD PTR [rdi+r9*8] movhpd xmm2, QWORD PTR [rsi+r9*8] // vectorial operations subpd xmm3, xmm4 subpd xmm2, xmm5 mulpd xmm3, xmm3 mulpd xmm2, xmm2 addpd xmm2, xmm3 sqrtpd xmm2, xmm2 // scalar store operations movlpd QWORD PTR [r8+r10*8], xmm2 movhpd QWORD PTR [r8+r9*8], xmm2 double的向量,并且想要将其添加到常量{x0, x1, x2, …. xn}中,获得k,则在标量模式下,该操作在读取{ {1}},添加{x0+k, x1+k, x2+k, …. xn+k},存储结果。在矢量模式下,使用SSE,看起来像同时读取x(i)k,同时添加x[i],同时存储2个结果。 如果元素在内存中不连续,则不能同时加载x [i]和x [i + 1],也不能同时存储结果,但至少可以同时执行两个加法运算。