与 Cortex-A53 相比,NEON 汇编代码在 Cortex-A72 上需要更多周期

NEON assembly code requires more cycles on Cortex-A72 vs Cortex-A53

我正在 AArch32 模式下的两个 ARMv8 处理器上对 ARMv7 NEON 代码进行基准测试:Cortex-A53 和 Cortex-A72。我正在使用 Raspberry Pi 3B 和 Raspberry Pi 4B 板与 32 位 Raspbian Buster。

我的基准测试方法如下:

uint32_t x[4];
uint32_t t0 = ccnt_read();
for(int i = 0; i < 1000; i++)
    armv7_neon(x);
uint32_t t1 = ccnt_read();
printf("%u\n",(t1-t0)/1000);

其中 armv7_neon 函数由以下指令定义:

.global armv7_neon
.func armv7_neon, armv7_neon
.type armv7_neon, %function
armv7_neon:
    vld1.32 {q0}, [r0]
    vmvn.i32 q0, q0
    vmov.i32 q8, #0x11111111
    vshr.u32 q1, q0, #2
    vshr.u32 q2, q0, #3
    vmov.i32 q9, #0x20202020
    vand q1, q1, q2
    vmov.i32 q10, #0x40404040
    vand q1, q1, q8
    vmov.i32 q11, #0x80808080
    veor q0, q0, q1
    vmov.i32 q12, #0x02020202
    vshl.u32 q1, q0, #5
    vshl.u32 q2, q0, #1
    vmov.i32 q13, #0x04040404
    vand q1, q1, q2
    vmov.i32 q14, #0x08080808
    vand q3, q1, q9
    vshl.u32 q1, q0, #5
    vshl.u32 q2, q0, #4
    veor q0, q0, q3
    vand q1, q1, q2
    vmov.i32 q15, #0x32323232
    vand q1, q1, q10
    vmov.i32 q8, #0x01010101
    veor q0, q0, q1
    vshl.u32 q1, q0, #2
    vshl.u32 q2, q0, #1
    vand q1, q1, q2
    vand q3, q1, q11
    vshr.u32 q1, q0, #2
    vshl.u32 q2, q0, #1
    veor q0, q0, q3
    vand q1, q1, q2
    vand q1, q1, q12
    veor q0, q0, q1
    vshr.u32 q1, q0, #5
    vshl.u32 q2, q0, #1
    vand q1, q1, q2
    vand q3, q1, q13
    vshr.u32 q1, q0, #1
    vshr.u32 q2, q0, #2
    veor q0, q0, q3
    vand q1, q1, q2
    vand q1, q1, q14
    veor q0, q0, q1
    vmvn.i32 q0, q0
    vand q1,  q0, q14
    vand q2,  q0, q15
    vand q3,  q0, q8
    vand q8,  q0, q11
    vand q9,  q0, q10
    vand q10, q0, q13
    vshl.u32 q1,  q1,  #1
    vshl.u32 q2,  q2,  #2
    vshl.u32 q3,  q3,  #5
    vshr.u32 q8,  q8,  #6
    vshr.u32 q9,  q9,  #4
    vshr.u32 q10, q10, #2
    vorr q0, q1, q2
    vorr q1, q3, q8
    vorr q2, q9, q10
    vorr q3, q0, q1
    vorr q0, q3, q2
    vst1.32 {q0}, [r0]
    bx lr
.endfunc

代码使用以下选项简单编译:

gcc -O3 -mfpu=neon-fp-armv8 -mcpu=cortex-a53
gcc -O3 -mfpu=neon-fp-armv8 -mcpu=cortex-a72

我在 Cortex-A53 和 Cortex-A72 上分别获得了 74 和 99 个周期。 我遇到 this blogpost 讨论 Cortex-A72 上的 tbl 指令的一些性能问题,但我 运行 的代码不包含任何。

这个差距从何而来?

我比较了A72和A55的指令周期时序(A53上没有):

vshlvshr:

A72: 吞吐量(IPC)1,延迟 3,仅在 F1 管道上执行
A55: 吞吐量(IPC)2,延迟 2,在两个管道上执行(虽然受限)

这很准确,因为您的代码中有很多。

您的汇编代码也有一些缺点:

  1. vaddvshl 限制更少,throughput/latency 更好。您应该将所有 vshl 替换为立即数 1 和 vadd。桶形移位器比 SIMD 上的算术成本更高。
  2. 您不应不必要地重复相同的指令(<<5)
  3. 第二个vmvn是不必要的。您可以将以下所有 vand 替换为 vbic
  4. 只要不涉及排列,编译器就会生成可接受的机器代码。因此,在这种情况下,我会用 neon 内在函数编写代码。

#include <arm_neon.h>

void armv7_neon(uint32_t * pData) {
    const uint32x4_t cx11 = vdupq_n_u32(0x11111111);
    const uint32x4_t cx20 = vdupq_n_u32(0x20202020);
    const uint32x4_t cx40 = vdupq_n_u32(0x40404040);
    const uint32x4_t cx80 = vdupq_n_u32(0x80808080);
    const uint32x4_t cx02 = vdupq_n_u32(0x02020202);
    const uint32x4_t cx04 = vdupq_n_u32(0x04040404);
    const uint32x4_t cx08 = vdupq_n_u32(0x08080808);
    const uint32x4_t cx32 = vdupq_n_u32(0x32323232);
    const uint32x4_t cx01 = vdupq_n_u32(0x01010101);

    uint32x4_t temp1, temp2, temp3, temp4, temp5, temp6;
    uint32x4_t in = vld1q_u32(pData);

    in = vmvnq_u32(in);

    temp1 = (in >> 2) & (in >> 3);
    temp1 &= cx11;
    in ^= temp1;

    temp1 = (in << 5) & (in + in);
    temp1 &= cx20;
    temp2 = (in << 5) & (in << 4);
    temp2 &= cx40;
    in ^= temp1;
    in ^= temp2;

    temp1 = (in << 2) & (in + in);
    temp1 &= cx80;
    temp2 = (in >> 2) & (in >> 1);
    temp2 &= cx02;
    in ^= temp1;
    in ^= temp2;

    temp1 = (in >> 5) & (in + in);
    temp1 &= cx04;
    temp2 = (in >> 1) & (in >> 2);
    temp2 &= cx08;
    in ^= temp1;
    in ^= temp2;

    temp1 = vbicq_u32(cx08, in);
    temp2 = vbicq_u32(cx32, in);
    temp3 = vbicq_u32(cx01, in);
    temp4 = vbicq_u32(cx80, in);
    temp5 = vbicq_u32(cx40, in);
    temp6 = vbicq_u32(cx04, in);

    temp1 += temp1;
    temp2 <<= 2;
    temp3 <<= 5;
    temp4 >>= 6;
    temp5 >>= 4;
    temp6 >>= 2;

    temp1 |= temp2 | temp3 | temp4 | temp5 | temp6;

    vst1q_u32(pData, temp1);
}

godbolt link

你可以看到 -mcpu 选项在这里有明显的区别。

但 GCC 从不让人失望:它拒绝使用 vbic,即使我明确命令它使用(Clang 也是如此。我讨厌它们)

我会进行拆卸,删除第二个 vmvn,并将所有 vand 替换为 vbic 以获得最佳性能。

请记住,用汇编编写不会自动使代码 运行 更快,而且较新的体系结构不一定具有更有利的 ICT:在 ICT 方面,A72 在很大程度上不如 A53 .

PS:使用 -mcpu=cortex-a53 选项生成的代码与 a55 的相同。我们可以假设 A55 只是 armv8.2 ISA 对 A53 的扩展。