矢量化的标准模板似乎是这样的:
#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 Advisor
和number_in_index_set
都会相应地更改。
我有两个问题:
(1)问题循环甚至被矢量化意味着什么?数组索引不是连续的,那么编译器将如何对循环进行矢量化处理?
(2)如indexset[]
所暗示的那样,假设可以进行向量化,那么如何安全地对所讨论的循环进行向量化?因此,Intel Advisor
的建议是:
Intel Advisor
具体来说,可以保证上述哪些建议可以确保安全的矢量化?
答案 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],也不能同时存储结果,但至少可以同时执行两个加法运算。