我想学习汇编编程以编写快速高效的代码。 我怎么能偶然发现一个我无法解决的问题。
我想循环遍历双字数组,并添加如下组件:
%include "asm_io.inc"
%macro prologue 0
push rbp
mov rbp,rsp
push rbx
push r12
push r13
push r14
push r15
%endmacro
%macro epilogue 0
pop r15
pop r14
pop r13
pop r12
pop rbx
leave
ret
%endmacro
segment .data
string1 db "result: ",0
array dd 1, 2, 3, 4, 5
segment .bss
segment .text
global sum
sum:
prologue
mov rdi, string1
call print_string
mov rbx, array
mov rdx, 0
mov ecx, 5
lp:
mov rax, [rbx]
add rdx, rax
add rbx, 4
loop lp
mov rdi, rdx
call print_int
call print_nl
epilogue
Sum由一个简单的C驱动程序调用。 print_string,print_int和print_nl函数如下所示:
section .rodata
int_format db "%i",0
string_format db "%s",0
section .text
global print_string, print_nl, print_int, read_int
extern printf, scanf, putchar
print_string:
prologue
; string address has to be passed in rdi
mov rsi,rdi
mov rdi,dword string_format
xor rax,rax
call printf
epilogue
print_nl:
prologue
mov rdi,0xA
xor rax,rax
call putchar
epilogue
print_int:
prologue
;integer arg is in rdi
mov rsi, rdi
mov rdi, dword int_format
xor rax,rax
call printf
epilogue
在对所有数组元素求和后打印结果时,它表示"结果:14"而不是15.我尝试了几个元素组合,似乎我的循环总是跳过数组的第一个元素。 有人可以告诉我为什么循环会跳过第一个元素吗?
我忘了提到我使用的是x86_64 Linux系统
答案 0 :(得分:3)
我不确定您的代码打印错误号码的原因。可能是一个你应该使用调试器跟踪的某个地方。带有layout asm
和layout reg
的gdb应该会有所帮助。实际上,我认为你已经超越阵列的末尾了。那里可能是-1,你将它添加到你的累加器。
如果你的最终目标是快速写作&有效的代码,你应该看看我最近添加到https://stackoverflow.com/tags/x86/info的一些链接。 ESP。 Agner Fog的优化指南非常适合帮助您了解当今机器的高效运行情况,以及无法解决的问题。例如leave
较短,但与mov rsp, rbp / pop rbp
取2相比需要3 uops。或者只省略帧指针。 (这些天gd默认为-fomit-frame-pointer
为amd64。)乱用rbp只是浪费指令并花费你注册,特别是。在ASM中值得写作的函数中(即通常所有东西都存在于寄存器中,而你不会调用其他函数)。
"正常"这样做的方法是在asm中编写你的函数,从C调用它来获得结果,然后用C打印输出。如果你想让你的代码可以移植到Windows,你可以使用像
这样的东西#define SYSV_ABI __attribute__((sysv_abi))
int SYSV_ABI myfunc(void* dst, const void* src, size_t size, const uint32_t* LH);
然后,即使您为Windows编译,也不必更改ASM以在不同的寄存器中查找其args。 (SysV调用约定比Win64更好:寄存器中的args更多,允许使用所有向量寄存器而不保存它们。)确保你有一个足够新的gcc,它具有https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66275的修复但是。
另一种方法是使用一些汇编程序宏来%define
某些寄存器名称,这样就可以为Windows或SysV ABI组装相同的源代码。或者在常规版本之前有一个Windows入口点,它使用一些MOV指令将args放在函数的其余部分所期望的寄存器中。但这显然效率较低。
了解asm中的函数调用是否有用,但通常自己编写它们是浪费时间。完成的例程将返回结果(在寄存器或内存中),而不是打印它。你的print_int
等例程非常低效。 (按下/弹出每个被调用者保存的寄存器,即使你不使用它们,也可以多次调用printf而不是使用以\n
结尾的单个格式字符串。)我知道你没有声明< em>这个代码很有效,而且你只是在学习。您可能已经知道这不是非常紧密的代码。 :P
我的观点是,编译器在大多数情况下都非常擅长工作。花时间编写asm仅用于代码的热门部分:通常只是一个循环,有时包括它周围的设置/清理代码。
所以,关于你的循环:
lp:
mov rax, [rbx]
add rdx, rax
add rbx, 4
loop lp
Never use the loop
instruction。它解码为7 uops,而宏观融合的比较和分支则为1。 loop
的最大吞吐量为每5个周期一个(英特尔Sandybridge / Haswell及更高版本)。相比之下,dec ecx / jnz lp
或cmp rbx, array_end / jb lp
会让您的循环在每个循环中以一次迭代运行。
由于您使用的是单寄存器寻址模式,因此使用add rdx, [rbx]
也会比单独的mov
- 加载效率更高。 (使用索引寻址模式since they can only micro-fuse in the decoders / uop-cache, not in the rest of the pipeline, on Intel SnB-family是一个更复杂的权衡。在这种情况下,add rdx, [rbx+rsi]
或其他东西会在Haswell及其后的东西上保持微融合。
当手动编写asm时,如果方便的话,可以通过在rdi中保留rsi和dest指针中的源指针来帮助自己。 movs
insn以这种方式隐式使用它们,这就是他们将si
和di
命名的原因。但是,不要因为寄存器名称而使用额外的mov
指令。如果您想要更高的可读性,请使用带有良好编译器的C语言。
;;; This loop probably has lots of off-by-one errors
;;; and doesn't handle array-length being odd
mov rsi, array
lea rdx, [rsi + array_length*4] ; if len is really a compile-time constant, get your assembler to generate it for you.
mov eax, [rsi] ; load first element
mov ebx, [rsi+4] ; load 2nd element
add rsi, 8 ; eliminate this insn by loading array+8 in the first place earlier
; TODO: handle length < 4
ALIGN 16
.loop:
add eax, [ rsi]
add ebx, [4 + rsi]
add rsi, 8
cmp rsi, rdx
jb .loop ; loop while rsi is Below one-past-the-end
; TODO: handle odd-length
add eax, ebx
ret
不使用此代码而不进行调试。 gdb(layout asm
和layout reg
)也不错,并且可以在每个Linux发行版中使用。
如果你的数组总是非常短的编译时常量长度,那么只需完全展开循环。否则,这样的方法,有两个累加器,可以并行发生两次加法。 (Intel和AMD CPU有两个加载端口,因此它们可以在每个时钟内存中增加两个.Haswell有4个执行端口,可以处理标量整数运算,因此它可以在每个周期1次迭代执行此循环。以前的Intel CPU可以发出每个周期4个uop,但执行端口将落后于跟上它们。展开以最小化循环开销会有所帮助。)
所有这些技术(尤其是多个累加器)同样适用于向量指令。
segment .rodata ; read-only data
ALIGN 16
array: times 64 dd 1, 2, 3, 4, 5
array_bytes equ $-array
string1 db "result: ",0
segment .text
; TODO: scalar loop until rsi is aligned
; TODO: handle length < 64 bytes
lea rsi, [array + 32]
lea rdx, [rsi - 32 + array_bytes] ; array_length could be a register (or 4*a register, if it's a count).
; lea rdx, [array + array_bytes] ; This way would be lower latency, but more insn bytes, when "array" is a symbol, not a register. We don't need rdx until later.
movdqu xmm0, [rsi - 32] ; load first element
movdqu xmm1, [rsi - 16] ; load 2nd element
; note the more-efficient loop setup that doesn't need an add rsi, 32.
ALIGN 16
.loop:
paddd xmm0, [ rsi] ; add packed dwords
paddd xmm1, [16 + rsi]
add rsi, 32
cmp rsi, rdx
jb .loop ; loop: 4 fused-domain uops
paddd xmm0, xmm1
phaddd xmm0, xmm0 ; horizontal add: SSSE3 phaddd is simple but not optimal. Better to pshufd/paddd
phaddd xmm0, xmm0
movd eax, xmm0
; TODO: scalar cleanup loop
ret
同样,此代码可能存在错误,并且不处理对齐和长度的一般情况。它已展开,因此每次迭代都会执行两次*四次打包输入= 32字节的输入数据。
它应该在Haswell上每个周期运行一次,否则在SnB / IvB上每1.333个周期进行1次迭代。前端可以在一个周期内发出所有4个uop,但执行单元无法在没有Haswell的第4个ALU端口的情况下处理add
和宏融合cmp/jb
。每次迭代展开4 paddd
可以为Sandybridge提供帮助,也可能对Haswell有所帮助。
使用AVX2 vpadd ymm1, [32+rsi]
,您可以获得两倍的吞吐量(如果数据在缓存中,否则您仍然会遇到内存瓶颈)。要对256b向量执行水平求和,请从vextracti128 xmm1, ymm0, 1
/ vpaddd xmm0, xmm0,xmm1
开始,然后它与SSE大小写相同。请参阅this answer for more details about efficient shuffles for horizontal ops。