与 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上没有):
vshl
和 vshr
:
A72:
吞吐量(IPC)1,延迟 3,仅在 F1 管道上执行
A55:
吞吐量(IPC)2,延迟 2,在两个管道上执行(虽然受限)
这很准确,因为您的代码中有很多。
您的汇编代码也有一些缺点:
vadd
比 vshl
限制更少,throughput/latency 更好。您应该将所有 vshl
替换为立即数 1 和 vadd
。桶形移位器比 SIMD 上的算术成本更高。
- 您不应不必要地重复相同的指令(
<<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);
}
你可以看到 -mcpu
选项在这里有明显的区别。
但 GCC 从不让人失望:它拒绝使用 vbic
,即使我明确命令它使用(Clang 也是如此。我讨厌它们)
我会进行拆卸,删除第二个 vmvn
,并将所有 vand
替换为 vbic
以获得最佳性能。
请记住,用汇编编写不会自动使代码 运行 更快,而且较新的体系结构不一定具有更有利的 ICT:在 ICT 方面,A72 在很大程度上不如 A53 .
PS:使用 -mcpu=cortex-a53
选项生成的代码与 a55 的相同。我们可以假设 A55 只是 armv8.2
ISA 对 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上没有):
vshl
和 vshr
:
A72:
吞吐量(IPC)1,延迟 3,仅在 F1 管道上执行
A55:
吞吐量(IPC)2,延迟 2,在两个管道上执行(虽然受限)
这很准确,因为您的代码中有很多。
您的汇编代码也有一些缺点:
vadd
比vshl
限制更少,throughput/latency 更好。您应该将所有vshl
替换为立即数 1 和vadd
。桶形移位器比 SIMD 上的算术成本更高。- 您不应不必要地重复相同的指令(
<<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);
}
你可以看到 -mcpu
选项在这里有明显的区别。
但 GCC 从不让人失望:它拒绝使用 vbic
,即使我明确命令它使用(Clang 也是如此。我讨厌它们)
我会进行拆卸,删除第二个 vmvn
,并将所有 vand
替换为 vbic
以获得最佳性能。
请记住,用汇编编写不会自动使代码 运行 更快,而且较新的体系结构不一定具有更有利的 ICT:在 ICT 方面,A72 在很大程度上不如 A53 .
PS:使用 -mcpu=cortex-a53
选项生成的代码与 a55 的相同。我们可以假设 A55 只是 armv8.2
ISA 对 A53 的扩展。