在NASM中循环一个数组

我想在汇编中学习编程来编写快速高效的代码。 我怎么会偶然发现一个我无法解决的问题。

我想循环一个双字的数组,并添加如下的组件:

%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 

总和由一个简单的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 

当总结所有数组元素后,打印结果时,它会显示“result:14”,而不是15.我尝试了几种元素的组合,看来我的循环总是跳过数组的第一个元素。 有人可以告诉我为什么th循环跳过第一个元素?

编辑

我忘了提及我正在使用x86_64 Linux系统

我不知道为什么你的代码打印错误的数字。 大概是一个你应该用调试器追踪的地方。 gdb与layout asmlayout reg应该有所帮助。 实际上,我想你会走到阵子的尽头。 那里可能有一个-1,你将它添加到你的累加器。

如果您的最终目标是编写快速高效的代码,您应该查看一下我最近添加到https://stackoverflow.com/tags/x86/info的一些链接。 ESP。 Agner Fog的优化指南非常适合帮助您理解今天机器上的高效运行,而不是什么。 例如, leave比较短,但是比起mov rsp, rbp / pop rbp取2,需要3个uops。或者只是省略帧指针。 (gcc现在默认为amd64的-fomit-frame-pointer )。乱搞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来寻找它在不同寄存器中的参数。 (SysV调用约定比Win64更好:在寄存器中有更多的参数,所有的向量寄存器都可以在不保存的情况下使用。)确保你有一个新的gcc,它有https:// gcc的修正.gnu.org / bugzilla / show_bug.cgi?id = 66275 ,但。

另一种方法是使用一些汇编宏来%define一些寄存器名称,以便为Windows或SysV ABI组装相同的源代码。 或者有一个Windows入口点,它使用一些MOV指令将函数的其余部分放在寄存器中。 但是这显然效率不高。


知道函数调用在asm中看起来很有用,但是自己写这些函数通常是浪费时间。 你完成的例程将返回一个结果(在寄存器或内存中),而不是打印它。 你的print_int等例程是非常低效的。 (推送/弹出每个被保存的寄存器,即使你没有使用它们,也不需要使用以\n结尾的单个格式字符串来多次调用printf)。我知道你并没有声称这个代码是有效的,你只是在学习。 你可能已经有一些想法,这不是很紧密的代码。 :P

我的观点是,编译器在大多数情况下是非常擅长的。 花你的时间只写你的代码的热部分:通常只是一个循环,有时包括周围的设置/清理代码。


所以, 在你的循环

 lp: mov rax, [rbx] add rdx, rax add rbx, 4 loop lp 

切勿使用loop指令 。 它解码为7个uops,1个用于宏融合的比较分支。 loop每5个周期的最大吞吐量为1(Intel Sandybridge / Haswell及更高版本)。 通过比较, dec ecx / jnz lp或者cmp rbx, array_end / jb lp会让你的循环在每个周期的一次迭代中运行。

由于您使用的是单寄存器寻址模式,使用add rdx, [rbx]也比单独的mov -load更高效。 (这是一个更复杂的索引寻址模式的折衷, 因为它们只能在解码器/ uop缓存中微熔,而不是在Intel SnB系列的其余部分,在这种情况下, add rdx, [rbx+rsi]或其他东西将保持在Haswell和稍后微融合)。

当用手写asm时,如果方便的话,通过在rsi中保留源指针和在rdi中的dest指针来帮助自己。 movs insn以这种方式隐含地使用它们,这就是为什么它们被命名为sidi 。 不过,不要仅仅因为寄存器名称而使用额外的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 asmlayout reg )是不错的,并在每个Linux发行版中可用。

如果你的数组总是非常短的编译时间常量,只需要完全展开循环即可。 否则,像这样有两个累加器的方法可以让两个并行的加法运行。 (Intel和AMD CPU有两个加载端口,所以每个时钟都可以支持两次内存加载,Haswell有4个执行端口,可以处理标量整型操作,所以它可以在每个周期迭代1次执行这个循环。每个周期4个uops,但执行端口会跟上它们,展开以减少循环开销会有帮助。)

所有这些技术(尤其是多个累加器)同样适用于矢量指令。

 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 

再次,这个代码可能有错误,并不处理对齐和长度的一般情况。 它被展开,所以每次迭代都会执行两个* 4个输入数据= 32字节的输入数据。

它应该在Haswell的每个周期的一次迭代中运行,否则在SnB / IvB上每1.333个周期迭代一次。 前端可以在一个周期内发布所有4个uops,但是如果没有Haswell的第四个ALU端口来处理add和宏融合的cmp/jb ,那么执行单元将无法跟上。 每次迭代展开到4个paddd都会为Sandybridge带来诀窍,也可能帮助Haswell。

使用AVX2 vpadd ymm1, [32+rsi] ,吞吐量会增加一倍(如果数据在缓存中,否则仍然是内存瓶颈)。 要做一个256b矢量的水平和,从vextracti128 xmm1, ymm0, 1 / vpaddd xmm0, xmm0,xmm1 ,然后和SSE情况一样。 请参阅此答案以获取有关水平操作的高效洗牌的更多详细信息 。