如何使用AARCH64汇编语言编写将给定数量的字节从给定源复制到给定目标的函数?这基本上就像memcpy,但有一些额外的假设。
到目前为止,我发现的最佳资源是this source code for memcpy,但我不明白哪些部分与我给出的具体参数相关。
编辑:到目前为止,我已经尝试将C中的以下功能转换为汇编。
void memcpy80(unsigned char* dest, unsigned char* sourc, unsigned long count)
{
unsigned char* end = sourc + count;
while (sourc < end)
*(dest++) = *(sourc++)
}
然后我尝试将其归结为符合规范的版本:
memcpy80:
add x3, x1, x2
loopstart:
cmp x3, x1
//how do I branch to loopend if x1 > x3?
add x0, x0, 1
add x1, x1, 1
ldr x4, [x1]
str x4, [x0]
b loopstart
loopend:
ret
用于分支到循环结束的指令是什么?那么我还需要改变什么才能符合规范?
这将在64位ARMv8-a架构上进行优化。规范中没有任何内容可以说更小或更大的尺寸更常见。代码大小小于80B没有任何好处,因此为了提高效率而直接达到极限将是理想的。
答案 0 :(得分:3)
你肯定不希望一次只复制1个字节。这是荒谬的。您知道您的问题大小是16字节的倍数,因此您至少应该使用64位long
,如果不是128位类型。
你的asm实现使用8字节加载/存储(好),但只将指针递增1,所以你重复复制相同的数据8次,重叠和错位。
如果您还不知道({3}},在底部使用条件分支,您应该用C语言编写并调整C +编译器选项,直到得到您想要的结果。这不是一个学习asm的坏方法;你可以看到编译器生成的代码如何实现你的简单循环设置+循环本身。
这将在64位ARMv8-a架构上进行优化。
该架构有多种不同的实现,每种实现都有不同的 micro 架构和不同的性能特征。对有序核心进行优化与无序有很大不同。在有序CPU中,您必须自己隐藏所有延迟,以便一次保持多个操作在飞行中。如果商店的数据尚未就绪,则执行会停止。但是,无序CPU允许硬件在循环中保持循环的多次迭代,从下一次迭代开始加载,而此次迭代的存储仍在等待数据。 (内存和缓存是流水线的,所以这很重要。)
例如,Cortex-A53有一个双重问题,即有序流水线,而Cortex-A57有一个3路超标量,深度无序流水线。 (how to structure a loop efficiently in asm)。
我玩了一些wikipedia,AArch64 gcc 5.4和6.3。
void memcpy80_simple_optimizations(unsigned long *restrict dst, unsigned long* restrict src, unsigned long len){
dst = __builtin_assume_aligned(dst, 16);
src = __builtin_assume_aligned(src, 16);
unsigned long* end = src + len; // len is in 8byte units here, but implement however you want
while (src < end) {
unsigned long tmp1 = *src; // do both loads ahead of both stores for better pipelining on in-order chips
unsigned long tmp2 = src[1]; // gcc seems to do a bad job
dst[0] = tmp1; // unroll by 2 because we know len is a multiple of 16 bytes
dst[1] = tmp2;
src+=2;
dst+=2;
}
}
这会使用gcc6.3 -O3 -fno-builtin -fno-tree-vectorize
进行编译(因此gcc无法识别该模式并将其编译为对memcpy
的调用!)
add x2, x1, x2, lsl 3 # leave out the lsl 3 for count in bytes
cmp x1, x2
bcs .L8 # skip first iteration for small size
.L12:
ldr x3, [x1, 8] # x3 = memory[x1+8]
add x0, x0, 16
ldr x4, [x1], 16 # x4 = memory[x1], x1+=16 (post-increment)
cmp x2, x1
stp x4, x3, [x0, -16] # store pair
bhi .L12
.L8:
ret
这可能是循环结构的一个很好的起点;只有10条指令(因而是40字节)。它也可以更高效,使用ldp
加载2个寄存器可能是最好的,就像它使用stp
将x4和x3存储为一条指令一样。如果启动代码根据count & 16
计算跳转到循环的位置(是否复制奇数或偶数个16字节块),则可能有80个字节的空间展开32。但这可能会干扰两个商店之前的两个负载。
您可以通过更聪明的寻址模式选择来减少循环开销。
我认为做两个加载然后两个商店都是更优化的。 *dst++ = *src++
两次,我正在加载/存储,加载/存储。我不知道gcc是否忽略了指针上的restrict
限定符(它告诉它们不重叠),或者它是否以某种方式更好来替代AArch64上的加载和存储做多个负载然后多个商店。有一些on the Godbolt compiler explorer关于编译器应如何为小型固定大小的副本内联memcpy
,并且有一些建议ldr/str / ldp/stp
可能更好,但可能只与其中一个相比较stp
存储来自ldr和ldp
的前半部分的数据。我不确定他们在暗示什么。
discussion on an LLVM patch proposal将此内循环用于大型副本。 dst
,src
和count
(以及A_l
等)是x
个寄存器的预处理器宏。
L(loop64):
stp A_l, A_h, [dst, 16]
ldp A_l, A_h, [src, 16]
stp B_l, B_h, [dst, 32]
ldp B_l, B_h, [src, 32]
stp C_l, C_h, [dst, 48]
ldp C_l, C_h, [src, 48]
stp D_l, D_h, [dst, 64]!
ldp D_l, D_h, [src, 64]!
subs count, count, 64
b.hi L(loop64)
请注意,每个存储指令都存储在前一次迭代中加载的数据,因此从每个加载到每个存储的距离一直是循环,减去1指令。这就是为什么它从商店开始,以负载结束。循环之前的代码加载4对x
寄存器,然后代码存储循环留在寄存器中的代码。
您可以将此技术调整为2对。
但无论如何,很明显,编写/调整glibc实现的人认为AArch64通常受益于glibc's AArch64 memcpy
,与存储 加载的数据相反。
通过对Cortex-A53和A57 software pipelining的测试进行备份,报告速度超过之前的版本。