gpt4 book ai didi

arrays - 遍历NASM中的阵列

转载 作者:太空狗 更新时间:2023-10-29 12:02:05 26 4
gpt4 key购买 nike

我想学习汇编编程,以编写快速有效的代码。
我如何偶然遇到无法解决的问题。

我想遍历双字数组并添加其组件,如下所示:

%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系统

最佳答案

我不确定您的代码为什么打印了错误的数字。您应该通过调试器进行跟踪的某个地方可能是一个一个的地方。具有layout asmlayout reg的gdb应该会有所帮助。实际上,我认为您要在数组末尾走一遍。那里可能是-1,然后将其添加到累加器中。
如果您的最终目标是编写快速高效的代码,则应该看看我最近添加到https://stackoverflow.com/tags/x86/info的一些链接。 Esp。 Agner Fog的优化指南非常适合帮助您了解当今机器上有效运行的内容,而哪些行之有效。例如与leave占用2相比,mov rsp, rbp / pop rbp较短,但需要3 oups。或者只是省略帧指针。 (这些天,gcc默认将amd64的默认值设置为-fomit-frame-pointer。)乱七八糟的做法只会浪费指令,而且会浪费您的注册费用,尤其是在注册时。值得在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,并且所有 vector 寄存器都可以使用而无需保存它们。)确保您有一个足够新的gcc,它可以修复 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66275
一种替代方法是使用一些汇编程序宏对某些寄存器名称进行 %define编码,以便为Windows或SysV ABI汇编相同的源。或者在常规入口之前有一个Windows入口点,该入口点使用一些MOV指令将args放在函数其余部分期望的寄存器中。但这显然效率较低。

知道什么函数在asm中看起来是有用的,但是通常自己编写它们是浪费时间。您完成的例程将仅返回结果(在寄存器或存储器中),而不打印结果。您的 print_int等例程效率极低。 (即使没有使用任何一个,也按/ pop 每个保存有被调用者的寄存器,并且多次调用printf而不是使用以 \n结尾的单个格式的字符串。)我知道您并不认为此代码是有效的,并且你只是在学习。您可能已经知道这不是很严格的代码。 :P
我的观点是,大多数情况下,编译器确实非常擅长于其工作。花时间只为代码的热门部分编写asm:通常只是一个循环,有时还包括围绕它的设置/清除代码。

因此, 进入循环:
lp:
mov rax, [rbx]
add rdx, rax
add rbx, 4
loop lp
Never use the loop instruction。与宏融合的比较和分支的1相比,它解码为7 oups。 loop的最大吞吐量为每5个周期之一(Intel Sandybridge/Haswell及更高版本)。相比之下, dec ecx / jnz lpcmp rbx, array_end / jb lp将使您的循环每个循环运行一次。
由于您使用的是单寄存器寻址模式,因此使用 add rdx, [rbx]也比单独的 mov -load更有效。 (这是使用索引寻址模式 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时,如果方便,可以通过将源指针保留在rsi中并将dest指针保留在rdi中来帮助自己。 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次迭代中执行此循环。以前的Intel CPU可以发出每个周期4 uops,但是执行端口将跟不上它们。展开以最小化循环开销将有所帮助。)
所有这些技术(尤其是多个累加器)同样适用于 vector 指令。
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个周期运行1次迭代。前端可以在一个周期内发出所有4个指令,但是如果没有Haswell的第4个ALU端口来执行 add和宏融合的 cmp/jb,执行单元就无法跟上。每次迭代展开到4个 paddd将为Sandybridge带来成功,并且可能也对Haswell有所帮助。
使用AVX2 vpadd ymm1, [32+rsi],您可以获得两倍的吞吐量(如果数据在高速缓存中,否则仍然会成为内存瓶颈)。要对256b vector 进行水平求和,请从 vextracti128 xmm1, ymm0, 1/ vpaddd xmm0, xmm0,xmm1开始,然后与SSE情况相同。参见 this answer for more details about efficient shuffles for horizontal ops

关于arrays - 遍历NASM中的阵列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31405072/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com