x86寄存器重命名的成本

8
以下代码可以在amd64上使用gcc或clang进行编译:
// gcc -O2 file.c -c
int f(int a, int b, int c, int d)
{
    return a & b & c & d;
}

生成以下汇编代码:

0000000000000000 <f>:
  0:    89 d0                   mov    %edx,%eax
  2:    21 c8                   and    %ecx,%eax
  4:    21 f0                   and    %esi,%eax
  6:    21 f8                   and    %edi,%eax
  8:    c3                      retq  

作为按位and应该是可结合的,我们可以假设将成对累加进两个寄存器中,然后and这两个寄存器会更有效率。这将打破依赖关系,并允许在具有多个ALU的CPU上进行并行执行。
由于编译器对于所有操作都将and放入相同的寄存器中,我认为它依赖于CPU能够进行寄存器重命名以自行打破依赖关系。
CPU的寄存器重命名功能是否没有成本,并且始终在amd64上可��,那么为什么编译器会像这样编译代码呢?
更新:
我发现如果向gcc传递更高的tree-assoc-width值,则它可以执行预期的依赖关系链断开。
--param tree-reassoc-width=2

1
在这种情况下,编译器无论如何都无法打破依赖链。我不知道为什么会这样。所以重命名的问题是没有意义的。 - Mysticial
3
依赖链仍然存在。CPU在了解前一条指令后才能执行每条指令,而eax的值取决于前一条指令。因此,无论CPU如何重命名变量,它都不能并行地执行这些指令。 - Mysticial
按位与是可结合和可交换的,因此编译器可以自由地重新排序指令以打破依赖关系。 - jtaylor
回到核心问题,寄存器重命名是指令执行算法的一个组成部分。因此,“成本”这个术语有些不确定。每当一条指令需要写入一个寄存器时,它会从物理寄存器文件中提取一个空寄存器,并将其映射到逻辑寄存器名称上。(不同处理器可能会略有不同,但大致如此) - Mysticial
其实,现在我思考了一下,我不知道被调用者保存的寄存器是什么,但使用额外的寄存器来打破链可能需要将寄存器存储在堆栈上并重新加载它。这比让这个链条顺着走更加昂贵。但是就这一点而言,为什么编译器不能先破坏输入寄存器? - Mysticial
显示剩余10条评论
1个回答

5
这似乎是编译器不够聪明。尽管英特尔的Ivy Bridge和Haswell微架构支持移动消除,所以mov %edx,%eax; and %ecx, %eax 将有效地成为and %ecx,%edx -->%eax,但这个序列仍然需要三个周期(忽略了这样一个小的连续依赖链将被适度的乱序执行窗口隐藏的事实)。如果编译器更聪明一些,可能会生成以下内容:
and    %esi,%edi
and    %edx,%ecx
mov    %edi,%eax
and    %ecx,%eax
retq  

正如您所指出的,这将打破依赖链。(通过移动消除,最后三条指令没有数据依赖性,因此,如果函数调用是一条指令(且L2和L3未命中),并且前面的指令在等待指令缓存未命中被处理时提交,并且在返回指令提交后读取零开销计时器[假设没有目标预测错误],可能比gcc生成的代码快一个周期)。双宽有序处理器将在一周期内执行and %esi,%edi; and %edx,%ecx,在下一个周期内执行move %edi,%eax,并在第三个周期内执行and %ecx,%eax; retq,而对于gcc生成的代码,mov %edx,%eax将在第一个周期内执行,and %ecx,%eax将在第二个周期内执行,and %esi,%eax将在第三个周期内执行,and %edi,%eax; retq将在第四个周期内执行。

寄存器重命名不会打破真正的数据依赖关系链,但会消除名称依赖关系(写后读[写入应该发生在读之后,因此读取旧值]和写后写冒险是名称依赖[技术上,一个没有读取的写入可以被丢弃,但检测到没有读取并且后面的写入不是推测性的通常被认为是不值得的];读后写是真正的数据依赖关系,读后读没有依赖)。在执行无序操作的实现中,寄存器重命名是普通操作的一部分;从这个意义上说,它可以被认为是“没有成本”的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接