Ábr*_*dre 13 x86 assembly gcc x86-64
write(1,"hi",3)
在linux上反汇编,gcc -s -nostdlib -nostartfiles -O3
结果如下:
ba03000000 mov edx, 3 ; thanks for the correction jester!
bf01000000 mov edi, 1
31c0 xor eax, eax
e9d8ffffff jmp loc.imp.write
Run Code Online (Sandbox Code Playgroud)
我不是到编译器的开发,但由于移动到这些寄存器的每一个值是恒定的和已知的编译时间,我很好奇,为什么不GCC使用dl
,dil
和al
来代替.也许有人会说,此功能不会让任何性能上的差异,但有一个在之间的可执行文件的大小有很大的区别mov $1, %rax => b801000000
,并mov $1, %al => b001
当我们谈论数千寄存器的程序访问.如果软件的优雅部分不仅体积小,它确实会对性能产生影响.
有人可以解释为什么"海湾合作委员会决定"它无所谓?
Mar*_*oom 23
部分寄存器会对许多x86处理器造成性能损失,因为它们在写入时会重新命名为不同的物理寄存器.(有关启用无序执行的寄存器重命名的更多信息,请参阅此问答).
但是当指令读取整个寄存器时,CPU必须检测到它在单个物理寄存器中没有正确的架构寄存器值这一事实.(这发生在问题/重命名阶段,因为CPU准备将uop发送到无序调度程序.)
它被称为部分寄存器停顿.Agner Fog的微体系结构手册很好地解释了它:
6.8部分登记档位 (PPro/PII/PIII和早期Pentium-M)
部分寄存器停顿是一个问题,当我们写入32位寄存器的一部分,然后从整个寄存器或更大的部分读取时,会发生这种情况.
例:Run Code Online (Sandbox Code Playgroud); Example 6.10a. Partial register stall mov al, byte ptr [mem8] mov ebx, eax ; Partial register stall
这给出了5到6个时钟的延迟.原因是已经分配了一个临时寄存器
AL
以使其独立AH
.执行单元必须等到写入AL
已经退出,然后才能将值AL
与其余的值 组合起来EAX
.
不同CPU中的行为:
所有其他x86 CPU:Intel Pentium4,Atom/Silvermont/Knight's Landing.所有AMD(以及Via等):
部分寄存器永远不会重命名.写入部分寄存器合并到完整寄存器中,使写入取决于完整寄存器的旧值作为输入.
如果您从未读取完整寄存器,则在没有部分寄存器重命名的情况下,写入的输入依赖性是错误依赖性.这限制了指令级并行性,因为将8位或16位寄存器重新用于其他内容实际上并不独立于CPU的观点(16位代码可以访问32位寄存器,因此它必须在上层保持正确的值半).而且,它使AL和AH不独立.当英特尔设计P6系列(1993年发布的PPro)时,16位代码仍然很常见,因此部分寄存器重命名是使现有机器代码运行更快的重要特性.(实际上,许多二进制文件不会为新CPU重新编译.)
这就是编译器大多避免编写部分寄存器的原因.它们使用movzx
/ movsx
尽可能将窄值零或符号扩展为完整寄存器,以避免部分寄存器错误依赖(AMD)或停顿(Intel P6系列).因此,大多数现代机器代码并没有从部分寄存器重命名中获益,这就是为什么最近的英特尔CPU正在简化其部分寄存器重命名逻辑.
正如@ BeeOnRope的回答指出的那样,编译器仍然会读取部分寄存器,因为这不是问题.(阅读AH/BH/CH/DH可以在Haswell/Skylake上增加额外的延迟周期,但是,请参阅早期关于Sandybridge家族最近成员的部分注册的链接.)
还要注意的是write
接受参数,对于一个x86-64的通常被配置GCC,需要整个的32位和64位寄存器,因此不能简单地组装成mov dl, 3
.大小由数据类型决定,而不是数据值.
最后,在某些情况下,C有默认参数提升要注意,尽管情况并非如此.
实际上,正如RossRidge指出的那样,调用可能是在没有可见原型的情况下进行的.
正如@Jester指出的那样,你的反汇编会产生误导.
例如mov rdx, 3
,实际上mov edx, 3
,虽然两者具有相同的效果 - 即整体上放3个rdx
.
这是正确的,因为立即值3不需要符号扩展,并且MOV r32, imm32
隐式清除寄存器的高32位.
前面的三个答案在不同方面都是错误的。
\n玛格丽特·布鲁姆接受的答案意味着部分收银机停顿是罪魁祸首。部分寄存器停顿是真实存在的,但不太可能与 GCC 的决定相关。
\n如果 GCC 替换mov edx,3
为mov dl,3
,则代码将是错误的,因为写入字节寄存器(与写入双字寄存器不同)不会将寄存器的其余部分清零。in 的参数rdx
类型为size_t
,为 64 位,因此被调用者将读取完整寄存器,其中将在第 8 至 63 位中包含垃圾。部分寄存器停顿纯粹是一个性能问题;如果代码错误,那么代码运行的速度有多快并不重要。
xor edx,edx
该错误可以通过在 之前插入来修复mov dl,3
。通过该修复,不会出现部分寄存器停顿,因为在所有具有停顿问题的 CPU 中,使用xor
或将整个寄存器清零sub
然后写入低字节是特殊情况。因此,部分寄存器停顿仍然与修复无关。
部分寄存器停顿会变得相关的唯一情况是,如果 GCC 碰巧知道寄存器为零,但它没有被特殊情况指令之一清零。例如,如果此系统调用之前是
\nloop:\n ...\n dec edx\n jnz loop\n
Run Code Online (Sandbox Code Playgroud)\n那么 GCC 可以推断出rdx
在它想要将 3 放入其中的位置为零,并且mov dl,3
\xe2\x80\x93 是正确的,但一般来说这将是一个坏主意,因为它可能会导致部分寄存器停顿。(在这里,这并不重要,因为系统调用无论如何都很慢,但我不认为 GCC 在其内部类型系统中具有“不需要对调用进行速度优化的慢速函数”属性。 )
xor
如果不是因为部分寄存器停顿,为什么 GCC 不发出随后字节移动的信号?我不知道,但我可以推测。
r0
它只是在通过初始化时节省空间r3
,而且即使这样也只节省了一个字节。它增加了指令数量,这也有其自身的成本(指令解码器通常是瓶颈)。与标准不同,它还会破坏标志mov
,这意味着它不是直接替代品。GCC 必须跟踪单独的标志破坏寄存器初始化序列,在大多数情况下(可能的目标寄存器的 11/15),这无疑会降低效率。
如果您正在积极优化大小,则可以执行push 3
后跟pop rdx
,这会节省 2 个字节,而不管目标寄存器如何,并且不会破坏标志。但它可能要慢得多,因为它写入内存并且对 具有错误的读写依赖rsp
,并且节省的空间似乎不太值得。(它还修改了红色区域,因此它也不是直接替代品。)
超级猫的回答说
\n\n\n处理器内核通常包括同时执行多个 32 位或 64 位指令的逻辑,但可能不包括与其他操作同时执行 8 位操作的逻辑。因此,虽然尽可能在 8088 上使用 8 位运算对于 8088 来说是一种有用的优化,但它实际上可能会严重消耗较新的处理器的性能。
\n
现代优化编译器实际上大量使用 8 位 GPR。(他们相对很少使用 16 位 GPR,但我认为这是因为 16 位数量在现代代码中并不常见。)8 位和 16 位操作至少与 32 位和 64 位一样快大多数执行阶段都会进行操作,有些速度更快。
\n我之前在这里写过“据我所知,在有史以来的所有 32/64 位 x86/x64 处理器上,8 位操作与 32/64 位操作一样快,甚至更快。” 但是我错了。相当多的超标量 x86/x64 处理器在每次写入时将 8 位和 16 位目标合并到完整寄存器中,这意味着当mov
目标为 8/16 位时,只写指令具有错误的读取依赖性当它是 32/64 位时存在。如果您在每次移动之前(或在使用类似的方法期间)不清除寄存器,错误的依赖链可能会减慢执行速度movzx
。即使最早的超标量处理器(Pentium Pro/II/III)没有这个问题,较新的处理器也有这个问题。尽管如此,根据我的经验,现代优化编译器确实使用了较小的寄存器。
BeeOnRope 的回答说
\n\n\n针对您的特定情况的简短答案是,因为在调用 C ABI 函数时,gcc 始终将参数符号或零扩展为 32 位。
\n
但这个函数一开始就没有短于 32 位的参数。文件描述符的长度正好是 32 位,并且size_t
正好是 64 位长。这些位中的许多位通常为零并不重要。如果它们很小,则它们不是以 1 字节编码的可变长度整数。如果 ABI 中没有整数提升要求并且实际参数类型是或其他某种 8 位类型,则仅对参数使用mov dl,3
且其余部分可能非零才是正确的。rdx
char
归档时间: |
|
查看次数: |
1655 次 |
最近记录: |