c++ 为什么自动向量化器找不到“可向量化类型信息”?

qvtsj1bj  于 2023-11-19  发布在  其他
关注(0)|答案(1)|浏览(95)

我试图对我的一些代码进行向量化,但我总是遇到info C5002: loop not vectorized due to reason '1305'。根据this page
//当编译器无法识别此循环的正确可向量化类型信息时,将发出代码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

ujv3wf0j

ujv3wf0j1#

如果你看一下asm(在Godbolt上),我们可以看到MSVC将两个循环合并在一起,所以没有单独的init循环。它只是在运行中计算v0[i],添加到未初始化的v2[i]中(从它分配但从未写入的空间中进行向量加载和存储)。
它报告第一个循环被向量化,第二个没有,但实际上它将它们融合到一个asm循环中。这些循环中的工作都被向量化,所以这可以说是它报告中的一个bug。(除了优化掉没有任何东西读取的未使用的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分配了空间,不是v0sub rsp, 536刚好超过128 * sizeof(int)= 512)。两个编译器都没有为未使用的v1分配空间,IDK为什么这会让你的例子变得混乱。
Clang优化了一切(包括返回值,因为阅读未初始化的v2[]在C++中是UB,或者至少是不确定的,所以它可以在EAX中留下任何它想要的垃圾作为返回值)。使用alignas(16) int v2[128] = {};,clang仍然优化了数组,只是返回-1https://godbolt.org/z/E9v1evE94- clang需要标准的alignas(128) int v0[];语法,不允许alignas在声明之后。GCC和MSVC允许这样做。
对于v2的init,MSVC确实为此调用了memset,但随后仍然进行了相同的单个循环,即动态实现v0[i]以添加到v2[i]中。

相关问题