以下代码使用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可以执行预期的依赖关系链断开:
--param tree-reassoc-width=2
这看起来像编译器只是不够聪明。虽然英特尔的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
in接下来,和and %ecx,%eax; retq
在第三个,而gcc生成的代码mov %edx,%eax
将在第一个周期执行,and %ecx,%eax
在第二个周期,and %esi,%eax
在第三个周期,and %edi,%eax; retq
在第四个周期。
寄存器重命名不会破坏真正的数据依赖关系链,但会删除名称依赖关系(Write-After-Read [在读取后应该发生写入,因此read获取旧值]和Write-After-Write危险是名称依赖关系[从技术上讲,可以删除没有读取的写入,但是检测到没有读取并且后面的写入不是推测性的通常不被认为是值得的;; Read-After-Write是真正的数据依赖性而Read-After-Read没有依赖)。在无序执行的实现中,寄存器重命名是普通操作的一部分;从这个意义上说,可以认为它“没有成本”。