c++ - 在 GCC/CLang 自动矢量化中强制对齐加载/存储的对齐属性

标签 c++ performance simd avx512

众所周知,GCC/CLang 使用 SIMD 指令可以很好地自动向量化循环。

此外,已知存在 alignas()标准 C++ 属性,除其他用途外还允许对齐堆栈变量,例如以下代码:

Try it online!

#include <cstdint>
#include <iostream>

int main() {
    alignas(1024) int x[3] = {1, 2, 3};
    alignas(1024) int (&y)[3] = *(&x);

    std::cout << uint64_t(&x) % 1024 << " "
        << uint64_t(&x) % 16384 << std::endl;
    std::cout << uint64_t(&y) % 1024 << " "
        << uint64_t(&y) % 16384 << std::endl;
}

输出:

0 9216
0 9216

这意味着 xy 在堆栈上按 1024 字节对齐,但不是 16384 字节。

现在让我们看另一个代码:

Try it online!

#include <cstdint>

void f(uint64_t * x, uint64_t * y) {
    for (int i = 0; i < 16; ++i)
        x[i] ^= y[i];
}

如果在 GCC 上使用 -std=c++20 -O3 -mavx512f 属性进行编译,则会生成以下 asm 代码(提供部分代码):

        vmovdqu64       zmm1, ZMMWORD PTR [rdi]
        vpxorq  zmm0, zmm1, ZMMWORD PTR [rsi]
        vmovdqu64       ZMMWORD PTR [rdi], zmm0
        vmovdqu64       zmm0, ZMMWORD PTR [rsi+64]
        vpxorq  zmm0, zmm0, ZMMWORD PTR [rdi+64]
        vmovdqu64       ZMMWORD PTR [rdi+64], zmm0

AVX-512 未对齐加载 + 异或 + 未对齐存储执行两次。所以我们可以理解,我们的 64 位数组异或操作被 GCC 自动向量化以使用 AVX-512 寄存器,并且循环也被展开。

我的问题是如何告诉 GCC 提供给函数指针 xy 都对齐到 64 字节,而不是 unaligned load (vmovdqu64) 就像上面的代码一样,我可以强制 GCC 使用 aligned load (vmovdqa64)。众所周知,对齐的加载/存储可以快得多。

我第一次尝试强制 GCC 进行对齐加载/存储是通过以下代码:

Try it online!

#include <cstdint>

void  g(uint64_t (&x_)[16],
        uint64_t const (&y_)[16]) {

    alignas(64) uint64_t (&x)[16] = x_;
    alignas(64) uint64_t const (&y)[16] = y_;

    for (int i = 0; i < 16; ++i)
        x[i] ^= y[i];
}

但此代码仍然会产生未对齐的负载 (vmovdqu64),与上面的 asm 代码(之前的代码片段)相同。因此,这个 alignas(64) 提示对于改进 GCC 汇编代码没有提供任何有用的信息。

我的问题是,除了为 _mm512_load_epi64() 等所有操作手动编写 SIMD 内在函数之外,如何强制 GCC 进行对齐自动矢量化?

如果可能的话,我需要所有 GCC/CLang/MSVC 的解决方案。

最佳答案

尽管并非所有编译器都完全可移植,__builtin_assume_aligned 会告诉 GCC 假定指针是对齐的。

我经常使用一种不同的策略,使用辅助结构更容易移植:

template<size_t Bits>
struct alignas(Bits/8) uint64_block_t
{
    static const size_t bits = Bits;
    static const size_t size = bits/64;
    
    std::array<uint64_t,size> v;
    
    uint64_block_t& operator&=(const uint64_block_t& v2) { for (size_t i = 0; i < size; ++i) v[i] &= v2.v[i]; return *this; }
    uint64_block_t& operator^=(const uint64_block_t& v2) { for (size_t i = 0; i < size; ++i) v[i] ^= v2.v[i]; return *this; }
    uint64_block_t& operator|=(const uint64_block_t& v2) { for (size_t i = 0; i < size; ++i) v[i] |= v2.v[i]; return *this; }
    uint64_block_t operator&(const uint64_block_t& v2) const { uint64_block_t tmp(*this); return tmp &= v2; }
    uint64_block_t operator^(const uint64_block_t& v2) const { uint64_block_t tmp(*this); return tmp ^= v2; }
    uint64_block_t operator|(const uint64_block_t& v2) const { uint64_block_t tmp(*this); return tmp |= v2; }
    uint64_block_t operator~() const { uint64_block_t tmp; for (size_t i = 0; i < size; ++i) tmp.v[i] = ~v[i]; return tmp; }
    bool operator==(const uint64_block_t& v2) const { for (size_t i = 0; i < size; ++i) if (v[i] != v2.v[i]) return false; return true; }
    bool operator!=(const uint64_block_t& v2) const { for (size_t i = 0; i < size; ++i) if (v[i] != v2.v[i]) return true; return false; }
    
    bool get_bit(size_t c) const   { return (v[c/64]>>(c%64))&1; }
    void set_bit(size_t c)         { v[c/64] |= uint64_t(1)<<(c%64); }
    void flip_bit(size_t c)        { v[c/64] ^= uint64_t(1)<<(c%64); }
    void clear_bit(size_t c)       { v[c/64] &= ~(uint64_t(1)<<(c%64)); }
    void set_bit(size_t c, bool b) { v[c/64] &= ~(uint64_t(1)<<(c%64)); v[c/64] |= uint64_t(b ? 1 : 0)<<(c%64); }
    size_t hammingweight() const   { size_t w = 0; for (size_t i = 0; i < size; ++i) w += mccl::hammingweight(v[i]); return w; }
    bool parity() const            { uint64_t x = 0; for (size_t i = 0; i < size; ++i) x ^= v[i]; return mccl::hammingweight(x)%2; }
};

然后使用reinterpret_cast将指向uint64_t的指针转换为指向该结构的指针。

将 uint64_t 上的循环转换为这些 block 上的循环通常可以很好地自动矢量化。

关于c++ - 在 GCC/CLang 自动矢量化中强制对齐加载/存储的对齐属性,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70045775/

相关文章:

python - 大于阈值的元素的 numpy.argmin

c - 声明与 SIMD 类型的 union 是否有害?

c++ - 无法编译 FSM 示例代码

java - 计算多条鱼的时空复杂度?

c++ - 使两种类型的相同模板兼容

performance - 为什么按位异或 (^) 在 Firefox 中比不等于 (!=) 比较快?

x86 - 显示向量寄存器的约定

gcc - 在 GCC 中编译 SSE 内在函数会出错

c++ - 如何抑制 C++ 中的个别警告?

c++ - OpenGL/GLUT 我的光照方法正确吗?