之前的三个答案都有不同的错误。
Margaret Bloom 的接受的答案暗示了部分寄存器停顿是问题所在。部分寄存器停顿确实存在,但不太可能与 GCC 在此处做出的决定有关。
如果 GCC 将
mov edx,3
替换为
mov dl,3
,那么代码就是错误的,因为对字节寄存器的写入(与对双字寄存器的写入不同)不会清零寄存器的其余部分。参数在
rdx
中的类型为
size_t
,大小为 64 位,因此被调用者将读取整个寄存器,其中位 8 到 63 包含垃圾值。 部分寄存器停顿纯粹是性能问题;如果代码运行得很快但是结果错误,那也没有意义。
可以通过在
mov dl,3
之前插入
xor edx,edx
来修复该错误。使用这种修复方法,就没有部分寄存器停顿,因为使用
xor
或
sub
清零整个寄存器,然后写入低字节,在所有具有停顿问题的 CPU 上都有特殊情况。 因此,修复后仍然与部分寄存器停顿无关。
唯一情况下部分寄存器停顿才会变得相关,就是如果GCC恰好知道该寄存器为零,但它没有被特殊指令之一清零。例如,如果此系统调用之前有:
loop:
...
dec edx
jnz loop
然后GCC可以推断出,在它想要将3放入rdx的位置,rdx为零,
mov dl,3
是正确的。但通常情况下这是不好的,因为它可能会导致部分寄存器停顿。在这里,这并不重要,因为系统调用本来就很慢,但我认为GCC在其内部类型系统中没有“无需加速优化调用”的“慢函数”属性。
为什么GCC不会发出
xor
后跟一个字节移动,难道不是因为部分寄存器停顿吗?我不知道,但我可以猜测。
这只在初始化
r0
到
r3
时节省空间,即使如此,它也只能节省一个字节。它增加了指令的数量,这有其自身的成本(指令解码器经常是瓶颈)。它还会破坏标志,不像标准的
mov
,这意味着它不能作为一种替代品。GCC必须跟踪一个单独的标志破坏寄存器初始化序列,在大多数情况下(15个可能的目标寄存器中的11个)都会明显不太高效。
如果你正在积极优化大小,可以使用
push 3
,然后是
pop rdx
,无论目标寄存器是什么,都可以节省2个字节,并且不会破坏标志。但它可能要慢得多,因为它写入内存并且与
rsp
存在虚假的读写依赖关系,而且空间节省似乎不值得。 (它还修改了
red zone,因此也不能作为一种替代品。)
超级猫的回答说:
处理器核心通常包括执行多个32位或64位指令的逻辑,但可能不包括同时执行8位操作的逻辑。因此,在8088上尽可能使用8位操作是一种有用的优化,但在新型处理器上实际上可能会显著降低性能。
现代优化编译器实际上经常使用8位通用寄存器(GPRs)。 (它们相对较少使用16位GPR,但我认为这是因为16位量在现代代码中不常见。)8位和16位操作至少在大多数执行阶段与32位和64位操作一样快,有些更快。
我之前曾在这里写道:“据我所知,在所有32/64位的x86/x64处理器上,8位操作与32/64位操作一样快或更快。”但是我错了。相当多的超标量x86/x64处理器会将8位和16位的目标合并到完整寄存器中的每个写入操作中,这意味着仅写入指令(如mov)在目标为8/16位时存在虚假读取依赖关系,而32/64位则不存在。如果在每次移动之前不清除寄存器(或者在移动期间使用movzx之类的东西),虚假依赖链会使执行变慢。尽管最早的超标量处理器(Pentium Pro/II/III)没有这个问题,但现代化的优化编译器在我的经验中确实使用较小的寄存器。
BeeOnRope的答案说:
短期内,对于您的特定情况来说,原因是gcc在调用C ABI函数时总是对参数进行32位符号或零扩展。
但是,该函数本身没有比32位短的参数。文件描述符恰好为32位长,而size_t恰好为64位长。即使其中很多位通常为零,这也无关紧要。它们不是可变长度的整数,如果它们很小,就会在1字节中编码。只有当ABI中没有整数提升要求,并且实际参数类型为char或其他8位类型时,使用mov dl,3才是正确的,而rdx的其余部分可能不为零。
3
和1
,它们已经是signed int
,仍然保持为signed int
。 - Ross Ridgexor eax, eax
表示该调用没有原型的范围。它不知道函数是否为可变参数,因此将 AL 设置为 0,表示未传递任何 SSE 寄存器中的参数。你的情况涉及到 ABI 的问题,“好像”规则允许任一实现,只要两端都同意即可。” - Ross Ridge