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

Posted

技术标签:

【中文标题】NEON 汇编代码在 Cortex-A72 与 Cortex-A53 上需要更多周期【英文标题】:NEON assembly code requires more cycles on Cortex-A72 vs Cortex-A53 【发布时间】:2021-12-11 14:36:03 【问题描述】:

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

我的基准测试方法如下:

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 指令的一些性能问题,但我正在运行的代码不包含任何内容。

这个差距从何而来?

【问题讨论】:

您是否尝试过在两台机器上测试相同的二进制文件,而不是为每台机器使用不同的调整选项进行编译?我不期望调用循环会很重要,但不同的代码对齐可能会做一些事情。我在您的 .S 文件中没有看到 .p2align 4 来对齐函数入口点,因此这可能很重要。 IDK 大约 74 对 99 个周期,但仍然值得在两台机器上尝试这两个二进制文件。 @PeterCordes 我刚刚尝试过它并没有什么不同,除了添加.p2align 4 似乎可以在 A72 上节省 3 个周期(因此总体上是 96 而不是 99)。其他二进制文件在 A53 上仍然给出 74 个周期,而在 A72 上给出 99 个周期。 您不应该期望更现代的架构具有更高的指令吞吐量。由于较高时钟设计的性质,通常情况正好相反。 ARM 从未发布过 Cortex-A53 的指令周期时序,但它可用于 A72,这看起来并不惊人。 @Jake'Alquimista'LEE 嗯,它实际上是手写的组装......是什么让你认为它不是? @Raoul722 所有这些都是vmov.i32 而不是vmov.i8 【参考方案1】:

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

vshlvshr

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

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

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

    vaddvshl 具有更少的限制和更好的吞吐量/延迟。您应该将所有vshl 立即替换为vadd。桶式移位器比 SIMD 上的算术成本更高。 您不应不必要地重复相同的说明 (&lt;&lt;5) 第二个vmvn 是不必要的。您可以将以下所有vand 替换为vbic。 只要不涉及排列,编译器就会生成可接受的机器代码。因此,在这种情况下,我会在 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,并用vbic 替换所有vand 以获得最佳性能。

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

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

【讨论】:

以上是关于NEON 汇编代码在 Cortex-A72 与 Cortex-A53 上需要更多周期的主要内容,如果未能解决你的问题,请参考以下文章

初识RK3399以及相关资料汇总

带有 NEON 的 ARM 汇编中的高级数学函数

ARM Neon 汇编器 + C 如何传递和使用指针数组

Neon Intrinsic 版本的汇编代码

RK3399开发板介绍

NEON 汇编代码,如何将 BYTE 转换为浮点数?