猿问

GCC为什么不使用分寄存器?

GCC为什么不使用分寄存器?

拆解write(1,"hi",3)在Linux上构建的gcc -s -nostdlib -nostartfiles -O3成果如下:

ba03000000     mov edx, 3 ; thanks for the correction jester!bf01000000     mov edi, 131c0           xor eax, eax
e9d8ffffff     jmp loc.imp.write

我不喜欢编译器开发,但是由于这些寄存器中的每个值都是常量且已知的编译时,我很好奇为什么GCC不使用dldil,和al相反。有些人可能会说,这个特性在性能上不会有任何不同,但是在可执行文件的大小上有很大的差别。mov $1, %rax => b801000000mov $1, %al => b001当我们谈论一个程序中成千上万的寄存器访问时。这不仅是软件优雅的一部分,而且对性能也有影响。

有人能解释一下为什么“GCC”决定不重要吗?


繁星淼淼
浏览 540回答 3
3回答

湖上湖

在许多x86处理器上,部分寄存器会导致性能损失,因为在编写时,它们被重命名为与整个寄存器不同的物理寄存器。(有关启用无序执行的寄存器重命名的更多信息,请参见这个问答).但是,当指令读取整个寄存器时,CPU必须检测一个事实,即它在单个物理寄存器中没有正确的体系结构寄存器值。(这发生在问题/重命名阶段,因为CPU准备将uop发送到无序调度程序中。)这叫做部分寄存器失速. 阿格纳·福格显微建筑手册很好地解释了:6.8部分登记册档位(PPRO/PII/PIII和Pentium-M)部分寄存器失速是一个问题,当我们写到32位寄存器的一部分,然后从整个寄存器或更大的一部分读取。例子:; 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中的行为:英特尔早期P6家族:见上文:暂停5-6时钟,直到部分写入退休。英特尔奔腾-M(模型D)/核心2/Nehalem:暂停2-3个周期,同时插入一个合并uop。(见这个Q&A用于微基准写AX和读取EAX时先使用xor-零或不对零。)英特尔沙梁桥:插入合并uop为低8/低16(AL/AX),没有拖延,或为AH/BH/CH/DH,同时停滞一个周期。Intel Ivybridge(可能),但肯定是Haswell/Skylake:al/ax没有被重命名,但是AH仍然是:Haswell/Skylake上的部分寄存器到底是如何执行的?写AL似乎对Rax有错误的依赖,而且AH是不一致的。.所有其他x86 CPU:Intel Pentium 4,Atom/Silvermont/Knight‘s Landing。所有AMD(以及VIA等):不重命名部分寄存器。写入部分寄存器合并到完整寄存器中,使写入依赖于完整寄存器的旧值作为输入。如果没有部分寄存器重命名,则写入的输入依赖项为假的如果您从未读取完整寄存器,则依赖项。这限制了指令级的并行性,因为对其他事情重用8或16位寄存器实际上并不是独立于CPU的观点(16位代码可以访问32位寄存器,因此它必须在上半部保持正确的值)。同时,它也使AL和AH不独立。当Intel设计P6系列(PPRO于1993年发布)时,16位代码仍然很常见,因此部分寄存器重命名是使现有机器代码运行更快的一个重要特性。(实际上,许多二进制文件不会为新CPU重新编译。)这就是为什么编译器大多避免写字部分寄存器他们用movzx / movsx只要有可能,将窄值扩展到完全寄存器,以避免部分寄存器错误依赖(AMD)或暂存(Intel P6-系列)。因此,大多数现代机器代码并不能从部分寄存器重命名中获益,这就是最近Intel CPU正在简化它们的部分寄存器重命名逻辑的原因。@BeeOnRope的回答指出,编译器仍然朗读,阅读部分寄存器,因为这不是问题。(阅读AH/BH/CH/DH可以在Haswell/Skylake上增加额外的延迟周期,但请参阅关于Sandybridge-族最近成员的部分寄存器的早期链接。)也请注意那,那个write获取参数,对于通常配置为GCC的x86-64,需要完整的32位和64位寄存器,这样就不能简单地组装到mov dl, 3..大小由类型的数据,而不是价值数据。最后,在某些情况下,C默认参数提升要知道,虽然事实并非如此.实际上,作为罗斯里奇指出,这个电话很可能是在没有可见原型的情况下发出的。正如杰斯特指出的那样,你的拆卸是误导人的。例如mov rdx, 3实际上mov edx, 3,尽管两者都有相同的效果-也就是说,将3放在整体中。rdx.这是正确的,因为即时值3不需要符号扩展名和MOV r32, imm32隐式清除寄存器的上32位。

德玛西亚99

事实上,GCC经常使用分式寄存器。..如果您查看生成的代码,您将发现很多使用部分寄存器的情况。简短的回答你的特殊情况,是因为GCC在调用cabi函数时总是将参数符号或零扩展到32位。.这个事实上SysV x 86和x86-64 abigcc和clang要求小于32位的参数为零或符号扩展为32位.有趣的是,它们不需要一直延伸到64位。因此,对于64位平台SysVABI平台上的如下功能:void foo(short s) {  ...}..争论s传入rdi而s的部分如下(但请参阅下面关于icc):  bits 0-31:  SSSSSSSS SSSSSSSS SPPPPPPP PPPPPPPP   bits 32-63: XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX  where:   P: the bottom 15 bits of the value of `s`   S: the sign bit of `s` (extended into bits 16-31)   X: arbitrary garbage密码foo可以依赖于S和P比特,但不是在X比特,这可能是任何东西。同样,对于foo_unsigned(unsigned short u),你会0第16-31段,否则就会是相同的。注意我说过事实上-因为实际上并没有真正记录如何处理较小的返回类型,但是您可以看到彼得的回答详情请到这里。我还问了一个相关的问题这里.经过进一步的测试,我得出结论:icc实际上违反了这个事实上的标准。gcc和clang似乎很坚持,但是gcc只以保守的方式:什么时候呼叫函数,它将零/符号扩展参数扩展到32位,但在其函数实现中,依附在打电话的人身上。clang实现依赖于调用者将参数扩展到32位的函数。所以事实上clang和icc即使对于普通C函数,如果它们的参数小于int.

凤凰求蛊

在类似于最初的IBMPC上,如果已知AH包含0,并且有必要以0x34这样的值加载AX,则使用“MOVAL,34H”通常需要8个周期,而不是“MOVAX,0034h”所需的12个周期-这是一个相当大的速度改进(如果预取的话,这两条指令都可以在两个周期内执行,但在实践中,8088花费了大部分时间等待指令被以每字节四个周期的代价获取)。然而,在当今通用计算机中使用的处理器上,获取代码所需的时间通常并不是影响总体执行速度的一个重要因素,代码大小通常也不是一个特别关注的问题。此外,处理器厂商试图最大限度地提高人们可能运行的代码类型的性能,而8位加载指令不太可能像现在32位加载指令那样经常使用。处理器核心通常包括同时执行多个32位或64位指令的逻辑,但可能不包括与任何其他操作同时执行8位操作的逻辑。因此,尽管在8088上使用8位操作(如果可能的话)是对8088的一个有用的优化,但它实际上对新的处理器来说是一个严重的性能损耗。
随时随地看视频慕课网APP
我要回答