GCC向量扩展和ARM NEON的内存对齐问题 问题描述最小代码示例进一步的调查结果

问题描述

问题描述

我正在尝试使用 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;

由于{strong>总线错误,编写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

编译结果:

崩溃出现在赋值语句中:

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中)都使用传递值

是否有任何g ++参数帽还可以对矢量化函数参数进行正确的内存对齐? 这可能是g ++错误吗?

非常感谢

解决方法

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

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

它可以用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;
}

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

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

在ARM上:

f2:
        push    {r7,lr}
        sub     sp,sp,#8
        add     r7,#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,16]
        add     x0,16
        bl      f3
        nop
        ldp     x29,[sp],32
        ret

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

似乎clang正确处理了对齐方式,因此这可能是您的另一选择。

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

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...