我正在尝试对我的一些代码进行矢量化,但我不断遇到
info C5002: loop not vectorized due to reason '1305'
。根据本页:
// 当编译器无法识别此循环的正确可向量化类型信息时,将发出代码 1305。
(我正在使用 Visual Studio Community 2022)
我决定尝试一些非功能代码,以更好地理解为什么会发生这种情况,但这个错误似乎出现在应该明显键入且易于矢量化的代码中。这是我的代码:
int vecTest() {
int v0[128] alignas(16);
int v1[128] alignas(16);
int v2[128] alignas(16);
int sum = 0;
for (int i = 0; i < 128; i++) {
v0[i] = i-1;
v1[i] = i*2;
}
for (int i = 0; i < 128; i++) {
v2[i] = v0[i] + v2[i];
}
#ifdef CASE_TWO
int* pv0 = &v0[0];
int* pv1 = &v1[0];
int* pv2 = &v2[0];
for (int i = 0; i < 128; i++) {
pv2[i] = pv0[i] + pv2[i];
}
#endif
sum += v2[0];
return sum;
}
int main(int argc, char* argv[])
{
int sum = vecTest();
sum = sum + 1;
}
如果 CASE_TWO 不存在,第一个(初始化)循环将进行矢量化,但第二个循环将返回代码 1305。但是,添加 CASE_TWO 的内容会导致所有三个循环正确矢量化!此外,包含 CASE_TWO 代码并排除第二个循环会导致 CASE_TWO 返回 1305。
在我看来,这些循环都不应该在矢量化方面遇到困难,并且它们不应该相互影响。我错过了什么?
代码 1305 和“正确的可向量化类型信息”的实际含义是什么?编译器实际上是否按照文档建议的方式运行?
我使用默认编译器设置,除了
/O2
和 /Qvec-report:2
。
如果您查看 asm(在 Godbolt 上),我们可以看到 MSVC 将两个循环折叠在一起,因此没有单独的 init 循环。它只是动态计算
v0[i]
,添加到未初始化的 v2[i]
(向量从它分配但从未写入的空间加载和存储)。
它报告第一个循环已矢量化,第二个循环未矢量化,但实际上它将它们融合到一个 asm 循环中。 这些循环中的工作全部被矢量化,因此这可以说是其报告中的一个错误。 (除了优化掉那些从未读取过的未使用的
v1[i] = i*2;
。)
// x64 MSVC 19.37 -O2
v2$ = 0
int vecTest(void) PROC ; vecTest, COMDAT
... function prologue
movdqa xmm2, XMMWORD PTR __xmm@00000003000000020000000100000000 ; _mm_setr_epi32(0,1,2,3)
xor eax, eax ; i = 0
movdqa xmm3, XMMWORD PTR __xmm@00000001000000010000000100000001 ; _mm_set1_epi32(1)
mov ecx, eax ; byte_offset = 0, could have just used a scaled-index addr mode
npad 3
$LL4@vecTest:
movdqu xmm0, XMMWORD PTR v2$[rsp+rcx] ; load uninitialized v2[i]
lea rcx, QWORD PTR [rcx+16] ; byte_offset += 16
movd xmm1, eax
add eax, 4
pshufd xmm1, xmm1, 0 ; _mm_set1_epi32(i) = movd+pshufd
paddd xmm1, xmm2 ; add [3,2,1,0] to get [i+3, i+2, i+1, i+0]
psubd xmm1, xmm3 ; v0[i] = i - 1 for i+0..3
paddd xmm1, xmm0 ; v2[i] += v0[i]
movdqu XMMWORD PTR v2$[rsp+rcx-16], xmm1
cmp eax, 128 ; 00000080H
jl SHORT $LL4@vecTest
mov eax, DWORD PTR v2$[rsp] # retval = v2[0]
... epilogue
相比之下,GCC 并不是那么聪明,它确实为 v2 和 v1 分配了空间(
sub rsp, 928
,加上 128 字节的红色区域,刚刚超过 1024 = 2x 128 * sizeof(int)
)。 MSVC 为 v2
分配了空间,而不是 v0
(sub rsp, 536
刚刚超过 128 * sizeof(int) = 512)。两个编译器都没有为未使用的 v1
分配空间,我不知道为什么这会让你的示例变得混乱。
Clang 优化掉了所有内容(包括返回值,因为读取未初始化的
v2[]
在 C++ 中是 UB,或者至少是不确定的,因此它可以在 EAX 中留下任何它想要的垃圾作为返回值)。使用 alignas(16) int v2[128] = {};
,clang 仍然优化掉数组,只是返回 -1
。 https://godbolt.org/z/E9v1evE94 - clang 需要标准 alignas(128) int v0[];
语法,不允许 alignas
位于声明之后。 GCC 和 MSVC 允许这样做。
随着
v2
的 init,MSVC 确实会为此调用 memset
,但仍然会进行相同的单个循环,即时实现 v0[i]
以添加到 v2[i]
中。