我必须编写汇编代码,该代码在循环中复制100字节在内存中。我是这样写的:
section .data
a times 100 db 1 ;reserve 100 bytes and fill with 1
b times 100 db 0 ;reserve 100 bytes and fill with 0
section _start
global _start
_start:
mov rsi, a ;get array a address
mov rdi, b ;get arrat b address
_for: ;początek pętli
cmp cx, 100 ;loop
jae _end_for ;loop
push cx ;loop
mov byte al, [rsi] ;get one byte from array a from al
mov byte [rdi], al ;put one byte from al to array b
inc rsi ;set rsi to next byte in array a
inc rdi ;set rdi to next byte in array b
pop cx ;loop
inc cx ;loop
jmp _for ;loop
_end_for:
_end:
mov rax, 60
mov rdi, 0
syscall
我不确定复制部分。我从地址读取值到寄存器,然后将其放入另一个。这对我来说看起来不错,但是我不确定要递增rsi
和rdi
。
真的够吗?
我是NASM和组装的新手,请帮助:-)
答案 0 :(得分:2)
我知道rep movsb,但是任务是逐个字节地在循环中进行,我不知道是否可以做得更好。
如果您必须一次循环1个字节,这是有效执行此操作的方法。值得一提的是,有效循环对memcpy
以外的其他情况也很有用!
首先,您知道循环主体应该至少运行一次,因此可以使用底部带有条件分支的普通循环结构。 (Why are loops always compiled into "do...while" style (tail jump)?)
第二,如果您根本不打算展开,那么应该使用索引寻址模式,以避免必须增加两个指针。 (但实际上最好将其展开)。
如果不需要,请不要使用16位寄存器。最好使用32位操作数大小(ECX);编写一个32位寄存器隐式零扩展到64位,因此可以安全地将索引用作寻址模式的一部分。
您可以使用索引加载,但可以使用非索引存储,因此您的存储地址uops仍可以在port7上运行,这在Haswell / Skylake上对超线程更加友好。并避免在Sandybridge上分层。显然,一次复制1个字节对于性能来说是完全浪费的,但是有时您确实想循环并实际上在寄存器中的每个字节执行某些操作。
您可以通过相对于dst索引src来做到这一点。
或者另一个技巧是将负索引计数到零,这样就避免了额外的cmp
。让我们先这样做:
default rel ; use RIP-relative addressing modes by default
ARR_SIZE equ 100
section .data
a: times ARR_SIZE db 1
section .bss
b: resb ARR_SIZE ;reserve n bytes of space in the BSS
;section _start ; do *not* use custom section names unless you have a good reason
; they might get linked with unexpected read/write/exec permission
section .text
global _start
_start:
lea rsi, [a+ARR_SIZE] ; pointers to one-past-the-end of the arrays
lea rdi, [b+ARR_SIZE] ; RIP-relative LEA is better than mov r64, imm64
mov rcx, -ARR_SIZE
.copy_loop: ; do {
movzx eax, byte [rsi+rcx] ; load without a false dependency on the old value of RAX
mov [rdi+rcx], al
inc rcx
jnz .copy_loop ; }while(++idx != 0);
.end:
mov eax, 60
xor edi, edi
syscall ; sys_exit(0)
在类似于静态(或其他非PIE)Linux可执行文件的位置相关代码中,mov edi, b+ARR_SIZE
是将静态地址放入寄存器的最有效方法。
请勿将_
用于所有标签名称。 _start
之所以这样命名,是因为保留以_
开头的C符号名称供实现使用。这不是您应该复制的东西;事实恰恰相反。
在函数中使用.foo
作为本地标签名称。例如如果您在.foo:
之后使用_start.foo:
,则它是_start
的简写。
相对于dst的src索引:
通常,您的输入和输出都不都是在静态存储中,因此您必须在运行时sub
地址。在这里,如果将它们像您最初所做的那样放在同一部分中,mov rcx, a-b
实际上会组装在一起。但是,如果没有,NASM拒绝。
实际上,我可以执行[rdi + (a-b)]
或简单地[rdi - ARR_SIZE]
来代替2寄存器寻址模式,因为我知道它们是连续的。
_start:
lea rdi, [b] ; RIP-relative LEA is better than mov r64, imm64
mov rcx, a-b ; distance between arrays so [rdi+rcx] = [a]
;;; for a-b to assemble, I had to move b back to the .data section.
lea rdx, [rdi+ARR_SIZE] ; end_dst pointer
.copy_loop: ; do {
movzx eax, byte [rdi + rcx] ; src = dst+(src-dst)
mov [rdi], al
inc rdi
cmp rdi, rdx
jbe .copy_loop ; }while(dst < end_dst);
数组末尾指针与使用foo.end()
在C ++中获得指向过去的指针/迭代器的方式完全一样。
这需要INC + CMP / JCC作为循环开销。在AMD CPU上,CMP / JCC可以宏熔合为1个uop,而INC / JCC则不能,因此从末尾开始的额外CMP vs.索引基本上是免费的。 (代码大小除外)。
在Intel上,这避免了建立索引存储。在这种情况下,负载是纯负载,因此无论如何它都是单个uop,而无需与ALU uop保持微融合。英特尔可以对inc/jcc
进行宏熔接,因此这确实会增加额外的循环开销。
如果您要展开,并且不需要避免为负载分配索引的寻址方式,则这种循环方式非常有用。但是,如果您将存储源用于诸如vaddps ymm0, ymm1, [rdi]
之类的ALU指令,那么是的,您应该分别增加两个指针,以便可以对装载和存储使用非索引寻址模式,因为英特尔CPU比办法。 (端口7的存储AGU仅处理未建立索引的索引,并且某些微融合的负载会以建立索引的寻址模式分层。Micro fusion and addressing modes)
答案 1 :(得分:1)
真的够吗?
是;您显示的代码足以复制数组。
对于性能/优化,您显示的代码可能更好。但是优化是一个滑坡,它绕过“ rep movsb
更适合代码大小”,经过“带循环展开的SIMD”,最后到达“可以避免复制数组”。