TL:DR:你可以,GCC 只是选择不,节省 1 个字节的代码大小与正常的 movzbl 字节负载相比,并避免来自 movb 的任何部分寄存器惩罚加载+合并。但由于不明原因,这不会在加载函数 arg 时导致存储转发停止。
(此代码正是我们从 GCC4.8 及更高版本中得到的,带有那些 C 语句和这些宽度的整数类型的 gcc -O1。看到它并叮当on the Godbolt compiler explorer GCC -O3提前安排movl 一条指令。)
这样做没有正确性理由,只有可能的性能。您是正确的,字节加载也可以正常工作。 (我省略了多余的操作数大小后缀,因为它们是由寄存器操作数隐含的)。
mov 8(%rsp), %dl # byte load, merging into RDX
add %dl, (%rax)
您可能从 C 编译器获得的是零扩展的字节加载。 (例如 GCC4.7 及更早版本会这样做)
movzbl 8(%rsp), %edx # byte load zero-extended into RDX
add %dl, (%rax)
movzbl(又名MOVZX in Intel syntax)是加载字节/字的首选指令,而不是movb 或movw。它总是安全的,并且在现代 CPU 上,MOVZX 加载速度与 dword mov 加载一样快,没有额外的延迟或额外的微指令;在加载执行单元中处理。 (英特尔从 Core 2 或更早版本开始,AMD 至少从 Ryzen 开始。https://agner.org/optimize/)。
唯一的成本是 1 个额外字节的代码大小(更大的操作码)。 movsbl 或 movsbq(又名 MOVSX)符号扩展在更新的 CPU 上同样有效,但在某些 AMD(如一些 Bulldozer 系列)上,它们的延迟比 MOVZX 负载高 1 个周期。因此,如果您只关心在加载字节时避免部分寄存器恶作剧,那么更喜欢 MOVZX。
如果您特别想要合并到现有 64 位寄存器的低字节或字中,通常只使用 movb 或 movw(带有寄存器目标)。 strong> Byte / word stores are perfectly fine on x86,我只说 mov mem-to-reg 或 reg-to-reg。这条规则有例外;有时,如果您小心并了解您关心代码在其上有效运行的微架构,有时您可以安全地使用字节操作数大小而不会出现问题。请注意,通过写入字节 reg 然后读取更大的 reg 来故意进行合并可能会导致某些 CPU 上的部分寄存器合并停止。
写入%dl 会错误地依赖在某些 CPU(包括当前的 Intel 和所有 AMD)上编写 EDX 的指令(在您的调用者中)。 (Why doesn't GCC use partial registers?)。 Clang 和 ICC 不在乎,照样做,按照你期望的方式实现功能。
movl 写入完整的 64 位寄存器 (by implicit zero-extension when writing a 32-bit register) 避免了该问题。
但如果调用者只使用字节存储,从8(%rsp) 读取双字可能会导致存储转发停顿。如果调用者使用push 写入该内存,那么您就是美好的。但是如果调用者只在call 之前使用movb $123, (%rsp) 进入已经保留的堆栈空间,那么现在你的函数正在从最后一个存储是一个字节的位置读取一个双字。除非存在某种其他停顿(例如,在调用函数后的代码获取中),否则当加载 uop 执行时,该字节可能在存储缓冲区中,但加载需要加上缓存中的 3 个字节。或者来自仍然在存储缓冲区中的一些较早的存储,因此在将存储缓冲区中的字节与缓存中的其他字节合并之前,它还必须扫描存储缓冲区以查找所有潜在的匹配项。仅当您加载的所有数据都来自一个商店时,存储转发的快速路径才有效。 (Can modern x86 implementations store-forward from more than one prior store?)
但是等等,x86-64 System V 调用约定的不成文“扩展”意味着没有存储转发停止的风险
clang/gcc sign- or zero-extend narrow args to 32-bit,即使编写的 System V ABI(还没有?)需要它。 Clang 生成的代码也依赖于它。这显然包括在内存中传递的参数,正如我们从 Godbolt 上的调用者中看到的那样。 (我使用了__attribute__((noinline)),所以我可以在启用优化的情况下进行编译,但仍然没有内联调用并优化掉。否则我可能只是注释掉正文并查看只能看到原型的调用者。
这不是 C 调用非原型函数的“默认参数提升”的一部分。窄 args 的 C 类型仍然是 short 或 char。这只是一个调用约定功能,它允许被调用者对 C 对象的对象表示之外的寄存器(或内存)中的位进行假设。但是,如果要求高 32 位为零会更有用,因为您仍然不能将它们用作 64 位寻址模式的数组索引。但是你可以在没有 MOVSX 的情况下做int_arg += char_arg。因此,当您使用窄 args 时,它可以使代码更高效,并且它们会被 + 等二元运算符的 C 规则隐式提升为 int。
通过用gcc -O3 -maccumulate-outgoing-args(或-O0或-O1)编译调用者,我让GCC用sub保留堆栈空间,然后在call proc调用你的函数之前使用movl $4, (%rsp)。 gcc 使用movb 会更有效(更小的代码大小),但它选择使用带有 32 位立即数的movl。我认为这是因为它在调用约定中实现了这条不成文的规则,而不是其他原因。
通常(没有-maccumulate-outgoing-args)调用者会在加载之前使用push $4 或push %rdi 进行qword 存储,这也可以有效地存储转发到dword(或字节)加载。因此,无论哪种方式,arg 将至少使用 dword 存储写入,从而使 dword 重新加载对性能安全。
dword mov 加载的代码大小比 movzbl 加载小 1 个字节,并且避免了 MOVSX 或 MOVZX 可能产生的额外成本(在旧的 AMD CPU 和极旧的 Intel CPU (P5) 上)。所以我认为这是最优的。
GCC4.7 及更早版本做 使用 movzbl (MOVZX) 加载 char a4 参数,就像我推荐的一般安全选项,但 GCC4.8 及更高版本使用 @ 987654371@.