Igo*_*gor 2 assembly gcc x86-64 calling-convention function-parameter
我正在阅读《计算机系统:程序员的观点》,3 / E(CS:APP3e)Randal E. Bryant和David R. O'Hallaron,作者说:“观察到第6行的movl指令从内存中读取了4个字节;以下addb指令仅使用低位字节”
第6行,为什么使用movl?他们为什么不移动8(%rsp),%dl?
void proc(a1, a1p, a2, a2p, a3, a3p, a4, a4p)
Arguments passed as follows:
a1 in %rdi (64 bits)
a1p in %rsi (64 bits)
a2 in %edx (32 bits)
a2p in %rcx (64 bits)
a3 in %r8w (16 bits)
a3p in %r9 (64 bits)
a4 at %rsp+8 ( 8 bits)
a4p at %rsp+16 (64 bits)
1 proc:
2 movq 16(%rsp), %rax Fetch a4p (64 bits)
3 addq %rdi, (%rsi) *a1p += a1 (64 bits)
4 addl %edx, (%rcx) *a2p += a2 (32 bits)
5 addw %r8w, (%r9) *a3p += a3 (16 bits)
6 movl 8(%rsp), %edx Fetch a4 (8 bits)
7 addb %dl, (%rax) *a4p += a4 (8 bits)
8 ret Return
Run Code Online (Sandbox Code Playgroud)
TL:DR:可以,GCC可以选择不保存,而是节省1字节的代码大小,而不是普通的movzbl字节加载,并且避免了movbload + merge 对部分寄存器的不利影响。但是出于晦涩的原因,这在加载函数arg时不会导致存储转发停顿。
(此代码是正是我们从GCC4.8得到后来随着gcc -O1与宽度的C语句和整数类型。看到它,铛上Godbolt编译探险 GCC -O3调度movl一个指令早。)
这样做没有正确性的原因,只有可能的性能。您是正确的,字节加载也可以正常工作。(我省略了冗余操作数大小的后缀,因为它们由寄存器操作数隐含)。
mov 8(%rsp), %dl # byte load, merging into RDX
add %dl, (%rax)
Run Code Online (Sandbox Code Playgroud)
您可能会从C编译器获得的是零扩展的字节加载。(例如,GCC4.7和更早版本执行此操作)
movzbl 8(%rsp), %edx # byte load zero-extended into RDX
add %dl, (%rax)
Run Code Online (Sandbox Code Playgroud)
movzbl(即Intel语法中的MOVZX)是加载字节/字而不是movb或的入门指南movw。它始终是安全的,并且在现代CPU上,MOVZX加载的速度实际上与dword mov加载一样快,没有额外的延迟或额外的操作。在负载执行单元中正确处理。(Intel从Core 2或更早版本开始,AMD从Ryzen至少开始。https: //agner.org/optimize/ )。唯一的代价就是增加1个字节的代码大小(较大的操作码)。 movsbl或movsbq(aka MOVSX)符号扩展在较新的CPU上同样有效,但是在某些AMD(例如Bulldozer系列)上,其延迟比MOVZX负载高1个周期。因此,如果您关心的只是在加载字节时避免部分寄存器的欺骗,那么最好选择MOVZX。
如果您特别想合并到现有64位寄存器的低字节或字中,通常仅使用movb或movw(具有寄存器目标)。 字节/字存储在x86上非常好,我只说的是mov mem-to-reg或reg-to-reg。该规则有例外。有时,如果您小心翼翼并了解所关心的微体系结构有效地运行代码,则可以安全地使用字节操作数大小,而不会出现问题。并且要注意,通过写入字节reg然后读取较大的reg有意进行合并会导致某些CPU上的部分寄存器合并停顿。
写入%dl会对在某些CPU上(包括当前的Intel和所有AMD)编写EDX的指令(在您的调用方中)具有错误的依赖性。(为什么GCC不使用部分寄存器?)。无论如何,Clang和ICC都不在乎并执行此操作,以您期望的方式实现该功能。
movl写入完整的64位寄存器(在写入32位寄存器时通过隐式零扩展)可以避免该问题。
但是8(%rsp),如果调用方仅使用字节存储,则从中读取双字可能会导致存储转发停顿。 如果调用方使用来写了该内存,那push很好。但是,如果呼叫者仅movb $123, (%rsp)在call到已经保留的堆栈空间中,现在您的函数正在从最后一个存储为字节的位置读取dword。除非存在其他某种停顿(例如,调用函数后在代码提取中),否则在执行加载uop时,字节可能位于存储缓冲区中,但是加载需要从缓存中获取3个字节。或来自仍在存储缓冲区中的某些较早的存储,因此在合并来自存储缓冲区的字节与来自缓存的其他字节之前,它还必须扫描存储缓冲区以查找所有可能的匹配项。仅当您要加载的所有数据均来自一个商店时,商店转发的快速路径才有效。(现代x86实现是否可以从多个以前的存储中转发存储?)
clang / gcc符号arg或零扩展窄args到32位,即使编写的System V ABI不需要(还?)。lang生成的代码也取决于它。这显然包括在内存中传递的args,正如我们从查看Godbolt上的调用方可以看到的那样。(我曾经使用过,__attribute__((noinline))所以我可以在启用优化的情况下进行编译,但仍然不能内联调用并进行优化。否则,我可以只注释掉主体并查看只能看到原型的调用者。
这不是 C的用于调用非原型函数的“默认参数提升”的一部分。窄args的C类型仍然是short或char。这只是一个调用约定功能,使被调用方可以对C对象的对象表示之外的寄存器(或内存)中的位进行假设。但是,如果要求高32位为零,则将更为有用,因为您仍然不能将它们用作64位寻址模式的数组索引。但是您可以int_arg += char_arg先没有MOVSX。因此,当您使用狭窄的args且intC规则将它们隐式提升为二进制运算符(如)时,它可以使代码更高效+。
通过使用gcc -O3 -maccumulate-outgoing-args(或-O0或-O1)编译调用方,我得到了GCC来保留堆栈空间,sub然后movl $4, (%rsp)在call proc调用您的函数之前使用。使用gcc可能会更高效(代码大小更小)movb,但它选择使用movl32位立即数的。我认为这是因为它是在调用约定中实现该不成文的规则,而不是其他原因。
在加载之前-maccumulate-outgoing-args,调用者通常会(或没有)使用q push $4或push %rdi进行qword存储,这也可以有效地将存储转发到dword(或字节)加载。因此,无论哪种方式,arg都将至少写入一个dword存储区,从而使dword重载对于性能而言是安全的。
dword mov负载的代码大小比movzbl负载小1字节,并且避免了MOVSX或MOVZX的可能额外成本(在旧的AMD CPU和极其旧的Intel CPU(P5)上)。所以我认为这是最佳的。
GCC4.7和更早版本的确movzbl对char a4arg 使用(MOVZX)加载,就像我推荐的一般安全选项一样,但GCC4.8和更高版本使用movl。
| 归档时间: |
|
| 查看次数: |
64 次 |
| 最近记录: |