我不确定为什么你的代码打印出了错误的数字。可能是某个地方出现了偏差,你应该使用调试器进行跟踪。gdb与
layout asm
和
layout reg
应该有所帮助。实际上,我认为你超出了数组的末尾。那里可能有一个-1,而你正在将其添加到累加器中。
如果你的最终目标是编写快速高效的代码,你应该查看一些我最近添加到
https://stackoverflow.com/tags/x86/info的链接。特别是Agner Fog的优化指南非常适合帮助你了解在今天的机器上运行得高效和不高效的内容。例如,
leave
更短,但需要3个uop,而
mov rsp,rbp / pop rbp
只需要2个。或者只需省略框架指针。(gcc默认在amd64上使用
-fomit-frame-pointer
这几天)。在函数中搞乱rbp只会浪费指令并且让你损失一个寄存器,特别是在值得用汇编语言编写的函数中(即通常所有内容都在寄存器中,并且你不会调用其他函数)。
“正常”的做法是用汇编语言编写函数,从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编译,也不必更改汇编语言以查找其参数在不同寄存器中的位置。(SysV调用约定比Win64更好:在寄存器中有更多的参数,并且所有矢量寄存器都允许在不保存它们的情况下使用。)确保您拥有足够新的gcc,其中包含
https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66275的修复程序。
另一种选择是使用一些汇编宏来%define
一些寄存器名称,以便您可以将相同的源代码汇编为Windows或SysV ABIs。 或者在常规入口点之前具有Windows入口点,该入口点使用一些MOV指令将参数放置在函数其余部分期望的寄存器中。 但这显然效率较低。
了解函数调用在汇编中的样子是有用的,但自己写函数通常是浪费时间的。你的完成例程只会返回结果(在寄存器或内存中),而不是打印它。你的print_int等例程效率非常低下。(每次调用保存和恢复所有被调用者保存的寄存器,即使你没有使用它们,并且多次调用printf而不是使用以
\n
结尾的单个格式字符串。)我知道你没有声称这段代码很高效,而且你只是在学习。你可能已经意识到这不是非常紧凑的代码。:P
我的观点是,编译器通常非常擅长它们的工作。把时间花在为代码的热点部分编写汇编代码上:通常只是一个循环,有时包括它周围的设置/清理代码。
所以,继续你的循环:
lp:
mov rax, [rbx]
add rdx, rax
add rbx, 4
loop lp
永远不要使用loop
指令。它解码为7个uops,而宏融合的比较和分支只有1个uop。 loop
的最大吞吐量为每5个周期一次(Intel Sandybridge / Haswell及更高版本)。相比之下,dec ecx / jnz lp
或cmp rbx,array_end / jb lp
会让您的循环以每个周期一次的速度运行。
由于您正在使用单寄存器寻址模式,因此使用add rdx,[rbx]
也比单独的mov
-load更有效率。(对于索引寻址模式,由于它们只能在解码器/ uop-cache中微融合,在Intel SnB系列的其余部分中不能进行融合,因此这是一个更复杂的权衡。在这种情况下,add rdx,[rbx + rsi]
或其他内容将在Haswell及更高版本上保持微融合)。
当手写汇编时,如果方便的话,请将源指针保存在rsi中,将目标指针保存在rdi中。 movs
指令隐式地使用它们,这就是为什么它们被命名为si
和di
的原因。但是,不要仅仅因为寄存器名称而使用额外的mov
指令。如果想要更好的可读性,请使用带有良好编译器的C语言。
mov rsi, array
lea rdx, [rsi + array_length*4]
mov eax, [rsi]
mov ebx, [rsi+4]
add rsi, 8
ALIGN 16
.loop:
add eax, [ rsi]
add ebx, [4 + rsi]
add rsi, 8
cmp rsi, rdx
jb .loop
add eax, ebx
ret
不要在没有调试的情况下使用这段代码。gdb(使用
layout asm
和
layout reg
)并不差,而且在每个Linux发行版中都可以使用。
如果您的数组始终是非常短的编译时常量长度,只需完全展开循环即可。否则,像这样使用两个累加器的方法可以让两个加法并行进行。(Intel和AMD CPU具有两个加载端口,因此它们可以每个时钟周期从内存中支持两个加法。Haswell具有4个执行端口,可处理标量整数操作,因此它可以以1个迭代/时钟周期执行此循环。以前的Intel CPU可以每个时钟周期发出4个uop,但执行端口会落后于它们的速度。最小化循环开销的展开将有所帮助。)
所有这些技术(尤其是多个累加器)同样适用于向量指令。
segment .rodata
ALIGN 16
array: times 64 dd 1, 2, 3, 4, 5
array_bytes equ $-array
string1 db "result: ",0
segment .text
lea rsi, [array + 32]
lea rdx, [rsi - 32 + array_bytes]
movdqu xmm0, [rsi - 32]
movdqu xmm1, [rsi - 16]
ALIGN 16
.loop:
paddd xmm0, [ rsi]
paddd xmm1, [16 + rsi]
add rsi, 32
cmp rsi, rdx
jb .loop
paddd xmm0, xmm1
phaddd xmm0, xmm0
phaddd xmm0, xmm0
movd eax, xmm0
ret
再次强调,这段代码可能存在错误,并且不能处理对齐和长度的一般情况。它是展开的,因此每个迭代都会处理两个*四个打包的整数= 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情况相同。有关水平操作的有效洗牌的更多细节,请参见此答案。
eax
。 - Frank Kotlerrbx
和rdx
改为ebx
和edx
吗? - muXXmit2Xrbx
- 那是一个地址,需要 64 位。将rdx
更改为edx
应该没问题。 - Frank Kotler