NEON 代码在 armeabi-v7a 上比标准 C 代码快,但在 arm64-v8a 上慢 armeabi-v7a 设备 - 四核,32 位arm64-v8a 设备 - 八核,64 位

问题描述

我正在试用 android/ndk-samples 提供的 hello neon 示例,并在两台设备上测试了 fir 过滤器演示,一台支持 armeabi-v7a,另一台支持 arm64-v8a ABI。

认情况下,JNI code 对于 arm64-v8a 失败,但可以通过一些调整来解决。现在,当我最终在两个设备上运行比较代码(具有差异规格)时,我得到以下结果

armeabi-v7a 设备 - 四核,32 位

C Version:      182.47 ms
Neon Version:   69.782 ms (2.62x faster)

arm64-v8a 设备 - 八核,64 位

C Version:      10.189 ms
Neon Version:   19.4836 ms (0.52295x faster)

问题

为什么这个霓虹灯版本的 arm64-v8a 会变慢?

(我对 NEON 和 SIMD 还很陌生)

链接到内部代码 - cpp/helloneon-intrinsics.c

解决方法

为什么这个霓虹版本对于 arm64-v8a 会变慢?

因为您链接的代码是在 2016 年为 ARMv7 编写的。

在 ARMv7 中,8 字节 NEON 寄存器可单独寻址。

ARM64 SIMD 一直是 16 字节,8 字节向量具有非平凡的性能损失。

试试这个版本:

void fir_filter_neon_arm64( short *output,const short* input,const short* kernel,size_t width,size_t kernelSize )
{
    const ptrdiff_t offset = -(ptrdiff_t)kernelSize / 2;
    const size_t kernelSizeAligned = ( kernelSize / 8 ) * 8;
    const bool extraVector = ( kernelSize % 8 ) >= 4;

    for( size_t outer = 0; outer < width; outer++ )
    {
        const short* const inputPtr = input + outer + offset;
        // Handle stuff 16 bytes at a time.
        // Using 2 independent accumulators to improve data dependency situation on these accumulators.
        int32x4_t acc1 = vdupq_n_s32( 0 );
        int32x4_t acc2 = vdupq_n_s32( 0 );

        size_t ii = 0;
        for( ; ii < kernelSizeAligned; ii += 8 )
        {
            int16x8_t kernel_vec = vld1q_s16( kernel + ii );
            int16x8_t input_vec = vld1q_s16( inputPtr + ii );
            acc1 = vmlal_s16( acc1,vget_low_s16( kernel_vec ),vget_low_s16( input_vec ) );
            acc2 = vmlal_high_s16( acc2,kernel_vec,input_vec );
        }
        if( extraVector )
        {
            // The remainder was longer than 4,use SIMD for the first 4 of the remaining elements
            int16x4_t kernel_vec = vld1_s16( kernel + ii );
            int16x4_t input_vec = vld1_s16( inputPtr + ii );
            acc1 = vmlal_s16( acc1,input_vec );
            ii += 4;
        }

        // Add these two accumulators together
        acc1 = vaddq_s32( acc1,acc2 );
        // Horizontal sum into the scalar,ARM64 has an instruction for that
        const int sumVector = vaddvq_s32( acc1 );

        // Handle the final 0-3 elements
        int sumRemainder = 0;
        for( ; ii < kernelSize; ii++ )
            sumRemainder += (int)( kernel[ ii ] ) * (int)( inputPtr[ ii ] );

        // Store the final result
        const int sum = sumVector + sumRemainder;
        output[ outer ] = (short)( ( sum + 0x8000 ) >> 16 );
    }
}

如果使用 GCC 构建,请确保 -O3 -fno-tree-vectorize 否则编译器会自动将最后一个循环向量化为剩余部分,无缘无故地膨胀代码。使用该命令行开关,代码 looks reasonable.