GCC 向量扩展和 ARM NEON 的内存对齐问题

Posted

技术标签:

【中文标题】GCC 向量扩展和 ARM NEON 的内存对齐问题【英文标题】:Memory alignment issues with GCC Vector Extension and ARM NEON 【发布时间】:2020-10-09 21:33:50 【问题描述】:

问题描述

我正在尝试使用 GCC 矢量扩展编写 NEON 优化代码。 因此我定义了一个联合结构,如

#include <arm_neon.h>

typedef int32_t    v4si __attribute__ ((vector_size (16)));
typedef float32_t  v4sf __attribute__ ((vector_size (16)));

union v128

    int32x4_t   m128i;
    float32x4_t m128f;
    v4si        si;
    v4sf        sf;
;

v128 x,y;

编写像x.sf *= y.sf 这样的代码经常会由于总线错误而导致崩溃。 使用 gdb 进行检查总是会发现,在所有这些崩溃情况下,至少有一个变量仅与 8 个字节对齐,而不是与 16 个字节对齐。 但是,当我使用优化选项“-O2”进行编译时,这些崩溃情况的发生率要低得多。

是否有任何 gcc/g++ 编译器选项始终保证 GCC 向量的 16 位对齐? 由于“-O2”支持一整套优化,有谁知道哪个特定优化导致总线错误频率低得多?

我正在树莓派 3 上编译和测试我的代码。在那里我还使用了 g++ 参数:

-march=armv8-a+crc -mtune=cortex-a53 -mfloat-abi=hard -mfpu=neon-fp-armv8 -funsafe-math-optimizations

最小代码示例

simd_numeric_test.cpp:

#include <random>
#include <limits>
#include <cfloat>
#include <type_traits>
#include <cassert>
#include <arm_neon.h>


typedef int32_t    v4si __attribute__ ((vector_size (16), aligned(16)));
typedef float32_t  v4sf __attribute__ ((vector_size (16), aligned(16)));


typedef int32x4_t   m128i_t; // __attribute__ ((aligned(16)));
typedef float32x4_t m128f_t; // __attribute__ ((aligned(16)));

union v128

    m128i_t m128i;
    m128f_t m128f;
    v4si    si;
    v4sf    sf;
;
static_assert( sizeof(v128) == 16 );


struct vf32_t

    v128 val;

    static constexpr size_t num_items()  return (sizeof(val) / sizeof(float32_t)); 

    inline
    const vf32_t& operator+=( const vf32_t& other )  val.sf += other.val.sf; return *this; 

    inline
    const float32_t* cbegin() const  return &(val.sf[0]); 

    inline
    const float32_t* cend() const  return &(val.sf[num_items()]); 
;
static_assert( sizeof(vf32_t) == 16 );


class CSimdNumericTest

protected:

    const size_t m_numElemInSimd     = vf32_t::num_items();
    
    const int m_randomSeed_u         = 69;
    const int m_repeats_u            = 10000;

    const float32_t m_maxFloatVal_f32;// = 43.f;

    std::default_random_engine                m_rand;
    std::uniform_real_distribution<float32_t> m_floatSampler;

    void test_binary_assign_vv_operation( const vf32_t a_v32, const vf32_t b_v32 ) const;

public:

    void float32_base_op_test();

    CSimdNumericTest()
        : m_maxFloatVal_f32( std::ceil( std::pow( std::numeric_limits<float32_t>::max(),
                                                  1.f / static_cast<float32_t>( m_numElemInSimd  ) ) ) )
        , m_rand( m_randomSeed_u )
        , m_floatSampler( -m_maxFloatVal_f32, m_maxFloatVal_f32 )
    
;

void CSimdNumericTest::test_binary_assign_vv_operation( const vf32_t a_v32, const vf32_t b_v32 ) const

    vf32_t x = a_v32;

    x += b_v32;

    auto aIter = a_v32.cbegin();
    auto bIter = b_v32.cbegin();
    for ( auto xIter = x.cbegin(); xIter != x.cend();
           ++xIter, ++aIter, ++bIter ) 
        float32_t rx = *aIter;
        rx += *bIter;
        assert( rx == *xIter );
    


void CSimdNumericTest::float32_base_op_test()

    vf32_t a_v32, b_v32;

    const float32_t l_minFloat_f32 = 1. / m_maxFloatVal_f32;

    for ( int n = 0; n < m_repeats_u; ++n )
    
        for ( size_t i = 0; i < vf32_t::num_items(); ++i )
        
            a_v32.val.sf[i] = m_floatSampler( m_rand );
            b_v32.val.sf[i] = m_floatSampler( m_rand );
        
        test_binary_assign_vv_operation( a_v32, b_v32 );
    


int main(int argc, char **argv) 
  
    CSimdNumericTest test;
    test.float32_base_op_test();
    return 0;

我编译了所有东西

arm-linux-gnueabihf-g++ -c -o simd_numeric_test_neon.o simd_numeric_test.cpp -pipe -fsigned-char -pthread -ftree-vectorize -Wall -Wextra -Wdate-time -Wformat -Werror=format-security -ggdb3 -O0 -march=armv8-a+crc -mtune=cortex-a53 -mfloat-abi=hard -mfpu=neon-fp-armv8 -funsafe-math-optimizations -Wno-psabi 
arm-linux-gnueabihf-g++ -pthread -lpthread -lstdc++ -o simd_test_neon simd_numeric_test_neon.o

编译结果:

simd_numeric_test_neon.o目标文件 simd_test_neon 可执行文件

在赋值语句处出现崩溃:

x += b_v32;

Godbolt link

进一步调查结果

现在我注意到所有的崩溃都是在使用传值函数参数时发生的。虽然原始向量变量仍然正确对齐,但复制的函数参数不再存在。因此,当我将 pass-by-value 替换为 pass-by-reference 时,可执行文件可以正常工作:

void test_binary_assign_vv_operation( const vf32_t a_v32, const vf32_t b_v32 )

void test_binary_assign_vv_operation( const vf32_t& a_v32, const vf32_t& b_v32 )

我在所有的总线错误崩溃案例中都观察到了这种模式。

但是,这种观察并没有真正带来解决方案。有很多函数(例如在 C++STL 中)使用 pass-by-value

是否有任何 g++ 参数也可以为矢量化函数参数实现正确的内存对齐? 这可能是一个 g++ 错误吗?

在此先感谢

【问题讨论】:

评论不用于扩展讨论;这个对话是moved to chat。 【参考方案1】:

我同意你的看法,这是 ARM / AArch64 和其他几个目标(但不是 x86)上 gcc 中的一个错误。

当您有一个需要额外对齐但可以在寄存器中传递的类型时,问题似乎出现了。如果您将这样的对象作为函数参数传递,并且被调用的函数获取其地址,则该对象将溢出到堆栈但没有必要的对齐。然后,未对齐的对象可能会通过引用传递给另一个函数,从而导致崩溃。

它可以在 C 中复制,无需向量。这是一个测试用例;使用-O0 编译以避免内联。 (但即使开启优化,函数本身仍然编译错误。)

#include <stdio.h>

typedef int V __attribute__((aligned(64)));

void f3(V *p) 
  printf("%p\n", (void *)p);


void f2(V x) 
    //volatile int blah = 17;
    f3(&x);


int main(void) 
  f2(-43);
  return 0;

使用 gcc 到 10.2,在 arm-linux-gnueabihfaarch64-linux-gnu 上,这会打印非 64 字节对齐的地址。 (您可能必须取消注释 volatile int 声明,以防堆栈因巧合而正确对齐。)

检查生成的程序集显示 gcc 将x 溢出到堆栈并且没有尝试对齐它。我相信 ABI 堆栈对齐对于 ARM 来说只有 8 个字节,对于 AArch64 来说只有 16 个字节,所以需要手动对齐。

在 ARM 上:

f2:
        push    r7, lr
        sub     sp, sp, #8
        add     r7, sp, #0
        str     r0, [r7]
        mov     r3, r7
        mov     r0, r3
        bl      f3(PLT)
        nop
        adds    r7, r7, #8
        mov     sp, r7
        pop     r7, pc

在 AArch64 上:

f2:
        stp     x29, x30, [sp, -32]!
        mov     x29, sp
        str     w0, [sp, 16]
        add     x0, sp, 16
        bl      f3
        nop
        ldp     x29, x30, [sp], 32
        ret

您可以通过将函数参数分配给临时变量并将其传递来解决您自己函数中的错误,但当然,正如您所说,这对从标准库模板生成的函数没有帮助。

看起来clang正确地处理了对齐,所以这可能是你的另一个选择。

更新: 自 20201010 起,该错误存在于 gcc 主干中,我还能够在 alpha、sparc64 和 mips 目标上重现它(在仿真中)。但是,x86-64 会生成正确的对齐代码。我已将此报告为gcc bug 97473。

【讨论】:

可能相关:MinGW GCC 有(有?)一个使 AVX 基本上无法使用的错误:尽管通常为 alignas(32) 进行了正确的堆栈对齐,但无法对齐堆栈以溢出__m256i 类型对象。很好的侦探工作。 @Cordes:我也在 mingw 下编译了我的代码。在那里我使用了 gcc-7.4。你是对的!仅使用 SSE 而没有 AVX 时,一切正常。在我的 ARM+NEON 可执行文件也崩溃的某些地方,使用 AVX 会导致崩溃。不同的是,在 windows 下的错误信息是“Segmentation Fault”。 @PeterCordes:那可能是Bug 54412。

以上是关于GCC 向量扩展和 ARM NEON 的内存对齐问题的主要内容,如果未能解决你的问题,请参考以下文章

ARM NEON 没有 xor gcc 内在函数

在 ARM NEON 中的数组边界上加载向量

为 ARM NEON 编译时出现未知的 GCC 错误(严重)

使用 NEON 在 ARM 汇编中对四字向量中的所有元素求和

在 ARM Cortex A8 上的汇编中 XOR NEON 向量/寄存器的所有元素/通道(成对?)

ARM NEON Intrinsics:将向量的值限制为 0-255