我正在为yasm
处理器架构使用x86_64
汇编程序。
假设我已经在.data
部分中定义了三个数字:
section .data
;CONSTANTS:
SYSTEM_EXIT equ 60
SUCCESS_EXIT equ 0
;VARIABLES:
dVar1 dd 40400
wVar2 dw -234
bVar3 db -23
dRes dd 0 ;quotient
dRem dd 0 ;reminder
我想要做的是将签名双字dVar1
乘以签名字dVar2
,然后除以已签名字节bVar3
。
下面我介绍我的解决方案"引用this一书,说明我为什么要做每一步。问题在文本末尾。
我没有看到任何明确的规则,即乘法仅适用于相同大小的数字。但看到一些隐含的。这就是我为wVar2
使用转换的原因:
movsx eax, word [wVar2] ;[wVar2] now in eax
现在"他们"具有相同的大小,所以我只是将它们相乘:
imul dword [dVar1] ;edx:eax = eax * [dVar1]
...例如,将 ax (16位)乘以字操作数(也是16位)的结果提供双字(32位)结果。但是,结果不会放在 eax 中(这可能更容易),它被放置在两个寄存器中, dx 用于高阶结果(16位)和< strong> ax 用于低阶结果(16位),通常写为 dx:ax (按惯例)。
据我所知,结果现在在edx:eax
。
...红利需要 D 寄存器(对于高位部分)和 A (对于低位部分)...如果是先前的乘法执行后, D 和 A 寄存器可能已经设置正确(这是我的情况[OP注意事项])。
和
...此外, A ,可能还有 D 寄存器,必须结合使用。
- 字节除以: ax 表示16位
- 单词划分: dx:ax 表示32位
- 双字划分: edx:eax 64位(这是我的情况[OP&#39; s])
- Quadword Divide: rdx:rax for 128-bit
所以最初我将bVar3
转换为双字,然后将其除以:
movsx ebx, byte [bVar3] ;ebx = [bVar3]
idiv ebx, ;eax = edx:eax / [bVar3]
然后整个代码
section .data
;CONSTANTS:
SYSTEM_EXIT equ 60
SUCCESS_EXIT equ 0
;VARIABLES:
dVar1 dd 40400
wVar2 dw -234
bVar3 db -23
dRes dd 0 ;quotient
dRem dd 0 ;reminder
section .text
global _start
_start:
movsx ebx, byte [bVar3] ;conversion to double-word
movsx eax, word [wVar2] ;conversion to double-word
imul dword [dVar1] ;edx:eax = eax * [dVar1]
idiv ebx ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]
mov dword [dRes], eax
mov dword [dRem], edx
last:
mov rax, SYSTEM_EXIT
mov rdi, SUCCESS_EXIT
syscall
我使用调试器并看到正确的答案:
(gdb) x/dw &dRes
0x600159: 411026
(gdb) x/dw &dRem
0x60015d: -2
但我不确定以下事项。
P.S。也许这个问题更可能是CodeReview SE的问题。如果你也这么想,请告诉我。
答案 0 :(得分:1)
它是&#34;尽可能少的行数&#34;溶液
您的代码看起来很好,并且没有任何浪费的指令或明显的效率(除非在系统调用中,mov
到64位寄存器是浪费代码大小)。< / p>
但是在其他两次加载之后再做第二次movsx
。乱序执行不会分析依赖关系链并首先在关键路径上执行加载。在movsx
结果准备就绪之前,不需要第二次imul
加载,因此请将其放在imul
之后,以便前两次加载(movsx
和{{{} 1}}内存操作数)可以尽早执行,让imul
开始。
针对最少数量的指令(源代码行)优化asm通常没有用/重要。要么是代码大小(最少的机器代码字节),要么是性能(最小的uops,最低的延迟等,请参阅Agner Fog's optimization guide,以及the x86 tag wiki中的其他链接)。例如,imul
为microcoded on Intel CPUs,并且在所有CPU上都比您使用的任何其他指令慢得多。
在具有固定宽度指令的体系结构上,指令数量是代码大小的代理,但在具有可变长度指令的x86上就是这种情况。
无论如何,除非除数是编译时常量:Why does GCC use multiplication by a strange number in implementing integer division?和32位操作数大小(64位),否则没有好办法避免idiv
。 bit dividend)是您可以使用的最小/最快版本。 (与大多数指令不同,idiv
使用更窄的操作数会更快。)
对于代码大小,您可能希望使用一个RIP相对div
,然后访问其他变量,如lea rdi, [rel dVar1]
,这需要2个字节(modr / m + disp8)而不是5个字节bytes(modr / m + rel32)。即每个内存操作数增加1个字节(与寄存器源相比)。
在单词和字节位置之前分配您的双字结果位置是有意义的,因此所有双字都是自然对齐的,您不必担心它们被分割在缓存行中的性能损失。 (或者在[rdi + 4]
之后,在标签和align 4
之前使用db
。
这里的一个危险是64/32 =&gt;如果dd
的商不适合32位寄存器,则32位除法可以溢出和出错(使用#DE
,leading to SIGFPE on Linux)。您可以通过使用64位乘法和除法来避免这种情况,编译器的方式,如果这是一个问题。但请注意,64位(dVar1*wVar2) / bVar3
比Haswell / Skylake上的32位idiv
慢约3倍。 (http://agner.org/optimize/)
idiv
这显然是更大的代码大小(更多的指令,更多的指令使用64位操作数大小的REX前缀),但不太明显它的多慢,因为64正如我之前所说,比特; fully safe version for full range of all inputs (other than divide by 0)
movsx rcx, byte [bVar3]
movsxd rax, dword [dVar1] ; new mnemonic for x86-64 dword -> qword sign extension
imul rax, rcx ; rax *= rcx; rdx untouched.
cqo ; sign extend rax into rdx:rax
movsx rcx, word [wVar2]
idiv rcx
mov qword [qRes], rax ; quotient could be up to 32+16 bits
mov dword [dRem], edx ; we know the remainder is small, because the divisor was a sign-extended byte
很慢。
在具有2个显式操作数的64位idiv
之前使用movsxd
在大多数CPU上更好,但在64位imul
缓慢的几个CPU上(AMD Bulldozer-family)或者Intel Atom),你可以使用
imul
在现代主流CPU上,2操作数movsx eax, byte [bVar3]
imul dword [dVar1] ; result in edx:eax
shl rdx, 32
or rax, rdx ; result in rax
更快,因为它只需写一个寄存器。
您将代码放在imul
部分!在.data
之前使用section .text
,或者将数据放在最后。 (与C不同,您可以在源代码中引用符号,而不是它们声明的符号,包括标签和_start:
常量。仅按顺序应用汇编程序宏(equ
)。
此外,您的源数据可能会进入%define foo bar
,您的输出可能会进入BSS。 (或者将它们留在寄存器中,除非你的作业需要记忆;没有任何东西在使用它们。)
使用RIP相对寻址而不是32位绝对值: section .rodata
指令不是默认值,但RIP相对编码的时间比{{短1个字节1}}。 (32位绝对值适用于64位可执行文件,但not in 64-bit position-independent executables)。
如果商数不适合32位(如现有代码),default rel
可以解决问题,这个版本有我建议的所有修复:
[abs dVar1]
样式:在标签后使用div
,即使它是可选的。 default rel ; RIP-relative addressing is more efficient, but not the default
;; section .text ; already the default section
global _start
_start:
movsx eax, word [wVar2]
imul dword [dVar1] ;edx:eax = eax * [dVar1]
movsx ecx, byte [bVar3]
idiv ecx ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]
; leaving the result in registers is as good as memory, IMO
; but presumably your assignment said to store to memory.
mov dword [dRes], eax
mov dword [dRem], edx
.last: ; local label, or don't use a label at all
mov eax, SYS_exit
xor edi, edi ; rdi = SUCCESS_EXIT. don't use mov reg,0
syscall ; sys_exit(0), 64-bit ABI.
section .bss
dRes: resd 1
dRem: resd 1
section .rodata
dVar1: dd 40400
wVar2: dw -234
bVar3: db -23
; doesn't matter what part of the file you put these in.
; use the same names as asm/unistd.h, SYS_xxx
SYS_exit equ 60
SUCCESS_EXIT equ 0
代替:
。如果您不小心使用与指令助记符匹配的标签名称,这是一个好习惯。与dVar1: dd 40400
一样可能会出现令人困惑的错误消息,但dVar1 dd 40400
只会有效。
请勿使用enter dd 40400
将寄存器设置为零,use xor same,same
because it's smaller and faster。当你知道你的常数很小时,不要enter: dd 40400
到64位寄存器,让隐式零扩展完成它的工作。 (YASM并没有为您优化mov
到mov
,尽管NASM确实如此)。 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?
我没有看到任何明确的规则,即乘法仅适用于相同大小的数字。但是看一些隐含的。
(差不多)所有x86指令对所有操作数都需要相同的大小。例外情况包括mov rax,60
/ mov eax,60
,movzx
(和其他变量计数移位/旋转),以及movsx
之类的内容,可以在不同的寄存器集之间复制数据。还有imm8控制操作数,如shr reg, cl
。
通常与相同尺寸的输入/输出一起使用的说明不具备可以动态扩展其中一个输入的版本。
您可以看到movd xmm0, eax
明确记录了Intel's instruction-set reference manual entry for it 中适用的尺寸。 32x32 =&gt;的唯一形式64位结果为pshufd xmm0, xmm1, 0xFF
,即显式操作数必须是32位寄存器或内存,以与隐式imul
源操作数一起使用。
所以是的,来自内存的IMUL r/m32
加载是迄今为止实现此目的的最佳方式。 (但不是唯一的; cbw
/ cwde
also work to sign extend within EAX,并且始终eax
/ movsx
的签名从16延伸到32.这些比shl eax, 16
更糟糕 - 负载。)