gcc中的数组与指针自动矢量化

Posted

技术标签:

【中文标题】gcc中的数组与指针自动矢量化【英文标题】:Array vs pointer auto-vectorization in gcc 【发布时间】:2016-09-16 07:07:51 【问题描述】:

我正在尝试在 g++ 5.4 (-ftree-vectorize) 中使用自动矢量化。我注意到下面代码中的数组版本导致编译器错过了内部循环中的向量化机会,导致与指针版本相比存在显着的性能差异。在这种情况下,有什么可以帮助编译器的吗?

void floydwarshall(float* mat, size_t n) 
#if USE_POINTER
    for (int k = 0; k < n; ++k) 
        for (int i = 0; i < n; ++i) 
            auto v = mat[i*n + k];
            for (int j = 0; j < n; ++j) 
                auto val = v + mat[k*n+j];
                if (mat[i*n + j] > val) 
                    mat[i*n + j] = val;
                
            
        
    
#else // USE_ARRAY
    typedef float (*array)[n];
    array m = reinterpret_cast<array>(mat);
    for (int k = 0; k < n; ++k) 
        for (int i = 0; i < n; ++i) 
            auto v = m[i][k];
            for (int j = 0; j < n; ++j) 
                auto val = v + m[k][j];
                if (m[i][j] > val) 
                    m[i][j] = val;
                
            
        
    
#endif

【问题讨论】:

最好避免分支,mat[i*n+j]=(mat[i*n+j]&gt;val)?val:mat[i*n + j];(无条件写入)更容易向量化。 我将您的代码分成两个单独的函数(同时查看)和put them on the Godbolt compiler explorer。由于 Marc 指出的原因,它们都使用 gcc5.4 -O3 -march=haswell 自动矢量化,在内循环中使用 vcmpltps/vmaskmovps。指针版本在内循环内部有两个从内存加载的整数,而数组版本在内存中绑定了内循环(cmp r11, QWORD PTR [rbp-72]),所以它们都有问题。您使用了哪些目标选项?只是基线 SSE2? 【参考方案1】:

两个版本矢量化,使用 g++5.4 -O3 -march=haswell,由于 Marc 指出的原因,在内循环中使用 vcmpltps/vmaskmovps。

如果您不让编译器使用 AVX 指令,那就更难了。但是如果我只使用-O3,我根本看不到任何一个版本的矢量化(所以只有 SSE2 可用,因为它是 x86-64 的基线)。所以你原来的问题是基于我无法重现的结果。

将 if() 更改为三元运算符(因此代码始终存储到数组中)让编译器加载/MINPS/无条件存储。如果您的矩阵不适合缓存,这会占用大量内存;也许你可以用不同的方式安排你的循环?也可能不需要,因为需要m[i][k],而且我认为事情发生的顺序很重要。

如果更新非常不频繁并且脏数据的回写导致内存瓶颈,那么如果没有修改任何向量元素,甚至可能值得分支以避免存储。


这是一个矢量化效果很好的数组版本,即使只有 SSE2。我添加了代码来告诉编译器输入是对齐的,并且大小是 8 的倍数(每个 AVX 向量的浮点数)。如果你的真实代码不能做出这些假设,那么就把那部分去掉。它使矢量化部分更容易找到,因为它没有隐藏在标量介绍/清理代码中。 (使用-O2 -ftree-vectorize 不会以这种方式完全展开清理代码,但-O3 会。)

我注意到,如果没有 AVX,gcc 仍然使用未对齐的加载,但使用对齐的存储。也许它没有意识到如果m[i][j] 对齐,大小为8 的倍数应该使m[k][j] 对齐?这可能是指针版本和数组版本的区别。

code on the Godbolt compiler explorer

void floydwarshall_array_unconditional(float* mat, size_t n) 

    // try to tell gcc that it doesn't need scalar intro/outro code
    // The vectorized inner loop isn't particularly different without these, but it means less wading through scalar cleanup code (and less bloat if you can use this in your real code).

    // works with gcc6, doesn't work with gcc5.4
    mat = (float*)__builtin_assume_aligned(mat, 32);
    n /= 8;
    n *= 8;         // code is simpler if matrix size is always a multiple of 8 (floats per AVX vector)

    typedef float (*array)[n];
    array m = reinterpret_cast<array>(mat);

    for (size_t k = 0; k < n; ++k) 
        for (size_t i = 0; i < n; ++i) 
            auto v = m[i][k];
            for (size_t j = 0; j < n; ++j) 
                auto val = v + m[k][j];
                m[i][j] = (m[i][j]>val) ? val : m[i][j];   // Marc's suggested change: enables vectorization with unconditional stores.
            
        
    

gcc5.4 无法避免矢量化部分周围的标量介绍/清理代码,但 gcc6.2 可以。两个编译器版本的矢量化部分基本相同。

## The inner-most loop (with gcc6.2 -march=haswell -O3)
.L5:
    vaddps  ymm0, ymm1, YMMWORD PTR [rsi+rax]
    vminps  ymm0, ymm0, YMMWORD PTR [rdx+rax]     #### Note use of minps and unconditional store, enabled by using the ternary operator instead of if().
    add     r14, 1
    vmovaps YMMWORD PTR [rdx+rax], ymm0
    add     rax, 32
    cmp     r14, r13
    jb      .L5

外面的下一个循环执行一些整数计数器检查(使用一些 setcc 东西),并执行vmovss xmm1, DWORD PTR [rax+r10*4] 和一个单独的vbroadcastss ymm1, xmm1。据推测,它跳转到的标量清理不需要广播,并且 gcc 不知道即使在不需要广播部分的情况下,使用 VBROADCASTSS 作为负载也会更便宜。

【讨论】:

以上是关于gcc中的数组与指针自动矢量化的主要内容,如果未能解决你的问题,请参考以下文章

在 GCC 的函数中禁用特定循环的自动矢量化

在 C 和 C++ 中对齐堆数组以简化编译器 (GCC) 向量化

使用 GCC 强制自动矢量化

自动矢量化的实际使用?

cilk 加数组表示法未使用 gcc 4.9.0 进行矢量化

gcc 自动矢量化(未处理的数据参考)