在检查各种代码片段的各种编译器的输出时,我倾向于选择发出一对 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
指令替换 .
就像我说的那样,这并不是我认为很多的模式,除了ICC是一个非常好的优化编译器,它是由拥有有关其微处理器的内幕信息的人开发的 .
可能有一些英特尔知道在其处理器上执行 SUB
指令的事情,这使得将其分解为 NEG
ADD
指令更加优化吗?使用RISC样式的指令解码成更简单的μops是现代微体系结构的众所周知的优化建议,因此 SUB
可能在内部分解为单个 NEG
和 ADD
μops,并且它实际上对于前端解码器更有效使用这些"simpler"说明?现代CPU很复杂,所以一切皆有可能 .
但是,确认我的直觉,这实际上是一种悲观情绪 . SUB
在所有处理器上与 ADD
同样有效,因此额外需要的 NEG
指令只会减慢速度 .
我还通过Intel's own Architecture Code Analyzer运行了两个序列来分析吞吐量 . 虽然精确的循环计数和端口绑定从一个微体系结构到另一个体系结构不同,但从Nehalem到Broadwell,每个方面看起来都是优越的 . 以下是Haswell工具生成的两个报告:
SUB
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+ADD
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
相比净减少 . 那么为什么英特尔的编译器会这样做呢?
这只是代码生成器的一些怪癖应该被报告为缺陷,还是我在分析中遗漏了一些优点?
1 回答
奇怪的是我有一个简单的答案:因为ICC不是最优的 .
当您编写自己的编译器时,您将开始使用一组非常基本的操作代码:
NOP
,MOV
,ADD
...最多10个操作码 . 您暂时不使用SUB
因为它可能很容易被替换为:ADD NEGgative operand
.NEG
也不是基本的,因为它可能被替换为:XOR FFFF...; ADD 1
.因此,您实现了相当复杂的基于位的操作数类型和大小的寻址 . 您可以为单个机器代码指令(例如
ADD
)执行此操作,并计划将其进一步用于大多数其他指令 . 但到了这个时候,你的同事完成了剩余部分的最佳计算,而不使用SUB
!想象一下 - 它是一个坏人并讨厌AMD但只是因为你看到 - 它已被称为最佳,优化 .英特尔编译器总的来说非常好,但它有很长的版本历史,因此在极少数情况下它会表现得很奇怪 . 我建议你告诉英特尔这个问题,看看会发生什么 .