在检查各种代码片段的各种编译器的输出时,我注意到英特尔的C编译器(ICC)有一种强倾向于更喜欢发出一对NEG
+ ADD
指令,其他编译器将使用单SUB
条指令。
作为一个简单的例子,请考虑以下C代码:
uint64_t Mod3(uint64_t value)
{
return (value % 3);
}
ICC将其转换为以下机器代码(无论优化级别如何):
mov rcx, 0xaaaaaaaaaaaaaaab
mov rax, rdi
mul rcx
shr rdx, 1
lea rsi, QWORD PTR [rdx+rdx*2]
neg rsi ; \ equivalent to:
add rdi, rsi ; / sub rdi, rsi
mov rax, rdi
ret
尽管其他编译器(包括MSVC,GCC和Clang)都将生成基本相同的代码,但NEG
+ ADD
序列被单个SUB
指令替换。< / p>
就像我说的,这不仅仅是ICC如何编写这个特定片段的怪癖。这是我在分析算术运算的反汇编时反复观察到的模式。我通常不会想太多,除了ICC是一个非常好的优化编译器和它是由拥有有关其微处理器的内幕信息的人开发的。
英特尔是否知道在其处理器上实施SUB
指令会使其更加优化,将其分解为NEG
+ ADD
指令?使用RISC样式的指令解码成更简单的μops是现代微体系结构的众所周知的优化建议,因此SUB
可以在内部分解为单个NEG
和ADD
μops,以及对于前端解码器来说,使用这些“更简单”的指令实际上更有效吗?现代CPU 复杂,所以一切皆有可能。
Agner Fog's comprehensive instruction tables证实了我的直觉,这实际上是一种悲观。 SUB
与所有处理器上的ADD
同样有效,因此额外的必需NEG
指令只会减慢速度。
我还通过Intel's own Architecture Code Analyzer运行了两个序列来分析吞吐量。虽然精确的循环计数和端口绑定从一个微体系结构到另一个体系结构不同,但从Nehalem到Broadwell的每个方面,单个SUB
似乎都是优越的。以下是Haswell工具生成的两份报告:
Intel(R) Architecture Code Analyzer Version - 2.2 build:356c3b8 (Tue, 13 Dec 2016 16:25:20 +0200)
Binary Format - 64Bit
Architecture - HSW
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 1.85 Cycles Throughput Bottleneck: Dependency chains (possibly between iterations)
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.0 0.0 | 1.5 | 0.0 0.0 | 0.0 0.0 | 0.0 | 1.8 | 1.7 | 0.0 |
---------------------------------------------------------------------------------------
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 1 | 0.1 | 0.2 | | | | 0.3 | 0.4 | | CP | mov rax, 0xaaaaaaaaaaaaaaab
| 2 | | 1.0 | | | | | 1.0 | | CP | mul rcx
| 1 | 0.9 | | | | | | 0.1 | | CP | shr rdx, 0x1
| 1 | | | | | | 1.0 | | | CP | lea rax, ptr [rdx+rdx*2]
| 1 | | 0.3 | | | | 0.4 | 0.2 | | CP | sub rcx, rax
| 1* | | | | | | | | | | mov rax, rcx
Total Num Of Uops: 7
的 NEG +添加
Intel(R) Architecture Code Analyzer Version - 2.2 build:356c3b8 (Tue, 13 Dec 2016 16:25:20 +0200)
Binary Format - 64Bit
Architecture - HSW
Analysis Type - Throughput
Throughput Analysis Report
--------------------------
Block Throughput: 2.15 Cycles Throughput Bottleneck: Dependency chains (possibly between iterations)
Port Binding In Cycles Per Iteration:
---------------------------------------------------------------------------------------
| Port | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 |
---------------------------------------------------------------------------------------
| Cycles | 1.1 0.0 | 2.0 | 0.0 0.0 | 0.0 0.0 | 0.0 | 2.0 | 2.0 | 0.0 |
---------------------------------------------------------------------------------------
| Num Of | Ports pressure in cycles | |
| Uops | 0 - DV | 1 | 2 - D | 3 - D | 4 | 5 | 6 | 7 | |
---------------------------------------------------------------------------------
| 1 | 0.1 | 0.9 | | | | 0.1 | 0.1 | | | mov rax, 0xaaaaaaaaaaaaaaab
| 2 | | 1.0 | | | | | 1.0 | | CP | mul rcx
| 1 | 1.0 | | | | | | | | CP | shr rdx, 0x1
| 1 | | | | | | 1.0 | | | CP | lea rax, ptr [rdx+rdx*2]
| 1 | | 0.1 | | | | 0.8 | 0.1 | | CP | neg rax
| 1 | 0.1 | | | | | 0.1 | 0.9 | | CP | add rcx, rax
| 1* | | | | | | | | | | mov rax, rcx
Total Num Of Uops: 8
因此,据我所知,NEG
+ ADD
会增加代码大小,增加μops数量,增加执行端口的压力,并增加周期数,从而导致与SUB
相比,吞吐量净减少。那么为什么英特尔的编译器会这样做呢?
这只是代码生成器的一些怪癖应该报告为缺陷,还是我在分析中遗漏了一些优点?
答案 0 :(得分:1)
奇怪的是,我有一个简单的答案:因为ICC不是最佳的。
当您编写自己的编译器时,您将开始使用一组非常基本的操作代码:NOP
,MOV
,ADD
...最多10个操作码。您暂时不使用SUB
,因为它可能很容易被ADD NEGgative operand
替换。 NEG
也不是基本的,因为它可能被替换为:XOR FFFF...; ADD 1
。
因此,您实现了相当复杂的基于位的操作数类型和大小的寻址。您可以为单个机器代码指令(例如ADD
)执行此操作,并计划将其进一步用于大多数其他指令。但是到了这个时候,你的同事在不使用SUB
的情况下完成剩余部分的最佳计算!想象一下 - 它已经被称为“Optimal_Mod”,所以你错过了一些不理想的东西,不是因为你是一个坏人而且讨厌AMD而只是因为你看到 - 它已经被称为最优,优化。
英特尔编译器总的来说非常好,但它有很长的版本历史,所以在极少数情况下它会表现得很奇怪。我建议你告诉英特尔这个问题,看看会发生什么。