如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?
Posted
技术标签:
【中文标题】如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?【英文标题】:How to reverse an array in Assembly language ARM with Neon Extension efficiently? 【发布时间】:2017-10-29 10:13:32 【问题描述】:我正在使用一些图像处理功能,我需要以最快的方式反转字节数组。如果我试图解释我的实际功能,那将是不合适的。这就是我要简化问题标准的原因。
Input Array:
37 3B 29 32 C5 E3 F3 5E 04 E2 CA B8 A1 1F 64 1D
E5 6F 7B 2C EA 6A FD 1F A5 6B 8F FA FB 7A F4 2A
DC 08 6D DB B8 D4 77 5D A2 44 E6 8A 59 9C 7D C2
8E FB C6 2A F8 EC 96 ED DC F8 00 2D 63 4C A4 F9
Length: 64
Output Array:
F9 A4 4C 63 2D 00 F8 DC ED 96 EC F8 2A C6 FB 8E
C2 7D 9C 59 8A E6 44 A2 5D 77 D4 B8 DB 6D 08 DC
2A F4 7A FB FA 8F 6B A5 1F FD 6A EA 2C 7B 6F E5
1D 64 1F A1 B8 CA E2 04 5E F3 E3 C5 32 29 3B 37
用 C++ 等高级语言来完成这项工作真的很容易。在 C++ 中,此实现可能如下所示:
void Reverse_array(unsigned char* pInData, int iLen, unsigned char* pOutData)
int indx = 0;
for(int i=iLen-1; i>=0; i--)
pOutData[indx++] = pInData[i];
但我确实需要找到最有效和优化的解决方案来完成这项工作。由于此任务将在移动设备中执行,我决定在 ARM 中使用带有 Neon Extension 的原始汇编语言来实现。现在,我将分享我为实现此任务所做的努力(仍然不完整)。
NEON_ASM_FUNC_BEGIN Reverse_array_arm_neon
push r2-r8, lr
#r0 First parameter, This is the address of <pInData>
#r1 Second Parameter, This is the iLen
#r2 Third Parameter, This is the address of <pOutData>
add r2, r2, r1
ands r3, r1, #7
add r2, r2, #8
loop_Reverse:
vld1.u8 d0, [r0]!
vrev64.u8 d1, d0
sub r2, r2, #16
vst1.u8 d1, [r2]!
subs r1, #8
bne loop_Reverse
pop r2-r8, pc
NEON_ASM_FUNC_END
我还检查了 *** 的 How to reverse an array in assembly language ARM? 和 Reversing an array in assembly 解决方案,但我仍然需要更多有关此实现的知识。
-
有没有可能用arm汇编语言写出比c++更快的函数来解决这个问题?
使用 NEON 扩展功能实现此任务的正确方法是什么? (我的函数是不完整的,因为它不能以不能被 8 整除的不同长度工作)。
任何想法或信息都会对我有所帮助。谢谢你。
【问题讨论】:
对我来说,这听起来像是过早的优化——先尝试基准测试/分析std::reverse
,看看这是否真的是一个很好的优化候选者,然后再浪费大量时间和精力编写不可移植和不太健壮的代码哪一个可能会变得很少或没有好处?
In
和 out
是否重叠,或者它们甚至相同?
当然,如果 std:reverse 没有针对 NEON 进行优化,NEON 版本会快很多,尤其是因为提升了对齐和较低的带宽要求。
@RajibTheKing:您可能会发现 this article 很有用 - 它更多地关注 x86/SSE/AVX,但许多技术可以有效地应用于其他 SIMD 架构,例如 NEON。
【参考方案1】:
如果 In 和 Out 不重叠,并且 iLen
大于 64,下面的代码应该可以工作:
// Written by Jake 'Alquimista' LEE
.syntax unified
.arch armv7-a
.fpu neon
.text
.global Reverse_array_arm_neon
// void Reverse_array_arm_neon(unsigned char* pInData, int iLen, unsigned char* pOutData);
pSrc .req r0
iLen .req r1
pDst .req r2
postInc .req r3
.balign 32
.func
Reverse_array_arm_neon:
add pDst, pDst, iLen
mov postInc, #-32
sub pDst, pDst, #32
sub iLen, iLen, #64 // "withholding tax"
.balign 32
1:
vld1.8 d16, d17, d18, d19, [pSrc]!
vld1.8 d20, d21, d22, d23, [pSrc]!
subs iLen, iLen, #64
pld [pSrc, #64]
vrev64.8 q8, q8
vrev64.8 q9, q9
vrev64.8 q10, q10
vrev64.8 q11, q11
vswp d19, d16
vswp d18, d17
vswp d23, d20
vswp d22, d21
vst1.8 d16, d17, d18, d19, [pDst], postInc
vst1.8 d20, d21, d22, d23, [pDst], postInc
bpl 1b
add pSrc, pSrc, iLen
cmp iLen, #-64
sub pDst, pDst, iLen
bxle lr // return
b 1b
.endfunc
.end
您必须保留的唯一寄存器是:r4-r11,lr
和 q4-q7
,并且仅当您确实必须使用它们时。
如果您没有保留任何东西并损坏了lr
,您可以返回bx lr
您可以按照我的方式最有效地处理“剩余”(“预扣税”、bpl
和 add/sub
,循环后的负循环计数器)。
您应该将主循环对齐到 32 字节以提高 I-Cache 效率。
【讨论】:
非常感谢,@Jake 'Alquimista' Lee 但我真正想要的是,这个函数应该适用于所有可能的数组长度。因为图像可以是各种分辨率,例如 352*288、52*54 和 78*56,所以数组长度将为 101376、2808 和 4368。其中一些不能被 64 整除。 @RajibTheKing 它适用于所有大于 64 的长度。它不必是 64 的倍数。仔细看看我是如何处理循环内外的循环计数器的。 我试图执行此代码...但它给了我以下错误.. TestCamera/Neon_Assembly.s:449:1: error: unknown directive .func ^ TestCamera/Neon_Assembly.s:481: 1:错误:未知指令 .endfunc ^ 请您帮我解决这个问题。 @RajibTheKing 我正在使用 GCC,看来您可能正在使用 Clang。您可以简单地删除.func
和 .endfunc
,因为它们仅用于分析目的。或者您可以尝试分别用.fnstart
和.fnend
替换它们。
Jake 'Alquimista' LEE,非常感谢您的精彩贡献。我已经在苹果 Ipod 第 5 代设备中成功执行了这个代码部分。这个功能运行得很好,而且比我预期的要高效得多。我已经用更大的图像帧(其中数组长度为 1280x1280x3 = 4915200)以两种方式测试了这个函数...... C++ std::reverse() 和 ARM NEON Assembly(这个函数)......结果是,另一方面,std::reverse() 花费了 47 毫秒.... ARM NEON 解决方案仅花费了 28 毫秒。这真的是非常棒的优化,它将节省我的大量时间。【参考方案2】:
首先,我要尊重 Jake 'Alquimista' LEE,因为我从他的回答中学到了很多东西。在这里,我将分享解决此问题的替代解决方案。
.macro NEON_ASM_FUNC_BEGIN
.syntax unified
.text
.extern printf
.align 2
.arm
.globl _$0
_$0:
.endm
.macro NEON_ASM_FUNC_END
mov pc, lr
.endm
NEON_ASM_FUNC_BEGIN Reverse_array_arm_neon
push r3-r8, lr
pInData .req r0
iLen .req r1
pOutData .req r2
iOverlap .req r3
iTemp .req r4
add pOutData, pOutData, iLen
add pOutData, pOutData, #8
ands iOverlap, iLen, #7
beq loop_Reverse //if(iLen % 8 == 0) goto loop_Reverse
sub pOutData, pOutData, #16
vld1.u8 d0, [pInData]!
vrev64.u8 d1, d0
vst1.u8 d1, [pOutData]!
subs iLen, iLen, iOverlap
beq Reverse_array_arm_neon_completed //if(iLen == 0) go to end
mov iTemp, #8
sub iTemp, iTemp, iOverlap
sub pInData, pInData, iTemp
add pOutData, pOutData, iTemp
loop_Reverse:
vld1.u8 d0, [pInData]!
vrev64.u8 d1, d0
sub pOutData, pOutData, #16
vst1.u8 d1, [pOutData]!
subs iLen, #8
bne loop_Reverse
Reverse_array_arm_neon_completed:
pop r3-r8, pc
NEON_ASM_FUNC_END
我已在 Apple Ipod touch 第 5 代(32bit/Cortex A9/ARMv7-A 平台)设备中测试过此功能。此函数适用于所有可能的数组长度。希望能帮助到你。
【讨论】:
这在有序 ARM 上可能要慢很多,因为它不会隐藏加载延迟或展开时的 ALU 操作。对于大数组,将这样的小输入处理代码放在 Jake 的 64 字节循环前面。 我已经用相同的更大数组分析了这两个函数。这两个函数花费的时间完全相同。 我是说他们在 other CPU 上可能需要不同的时间。 Cortex A9 is an out-of-order CPU core,所以乱序执行隐藏了每次循环迭代中加载/ALU 的延迟,所以存储执行时没有准备好也没关系。下一次迭代中的 load + ALU 开始,而上一次迭代的存储仍在无序 CPU 上等待数据。 @PeterCordes NEON 绕过 L1 缓存,并且在 A15 等一些高级内核上,有一个循环缓冲区可以处理多达 15 条指令。每次迭代将其展开更深至 128 字节将使函数失去循环缓冲区的资格,从而导致功耗增加并可能降低性能。大多数时候,在 ARM 上降低功耗比稍微提高性能更重要。 @Jake'Alquimista'LEE:啊有趣。 Apparently having NEON bypass L1 is optional. 你的意思是这是一个典型的配置? 128 位存储在典型 ARM uarch 上的吞吐量不比 64 位更好吗?即数据路径真的只有64位宽吗?隐藏有序核心的延迟我错了吗?我以为我看到其他人说这是 ARM 上的东西,就像在 P5 Pentium 或 Atom 上一样。或者只是在这种情况下,由于硬件预取,它仍然运行得几乎一样快?以上是关于如何使用 Neon Extension 有效地反转汇编语言 ARM 中的数组?的主要内容,如果未能解决你的问题,请参考以下文章
在 arm neon 中有效地重新洗牌和组合 16 个 3 位数字