为什么英特尔编译器更喜欢NEG + ADD而不是SUB?

在研究各种代码片段的各种编译器的输出时,我注意到英特尔的C编译器(ICC)倾向于在其他编译器使用单个SUB指令的情况下发出一对NEG + ADD指令。

作为一个简单的例子,考虑下面的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如何编写这个特定片段的一个怪癖。 这是我在分析算术运算反汇编时反复观察到的一种模式。 除了ICC被认为是一个相当不错的优化编译器,并且由拥有关于他们的微处理器的内部信息的人员开发的,我通常不会考虑这些。

英特尔是否知道在处理器上执行SUB指令会使它更适合将其分解为NEG + ADD指令? 使用解码成简单的μopsRISC式的指令是现代微体系结构的知名的优化建议,所以是有可能, SUB内部分解为单独的NEGADD μops,而且它实际上是前端解码器更有效使用这些“更简单”的说明? 现代的CPU很复杂,所以任何事情都是可能的。

Agner Fog的综合指导表确认了我的直觉,但这实际上是一种悲观。 SUB在所有处理器上的效率与ADD相当,因此额外所需的NEG指令只会减慢速度。

我还通过英特尔自己的架构代码分析器运行这两个序列来分析吞吐量。 尽管从一个微体系结构到另一个微体系结构的确切的周期数和端口绑定是不同的,但从Nehalem到Broadwell的每一个方面,单个SUB似乎都是优越的。 以下是由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相比,吞吐量净减少。 那么为什么英特尔的编译器会这样做呢?

这只是代码生成器的一些怪癖,应该作为缺陷报告,还是我在分析中缺少某些优点?


奇怪的是我有一个简单的答案:因为ICC不是最优的。

当您编写自己的编译器时,您将开始使用一些非常基本的操作代码: NOPMOVADD ...最多10个操作码。 您暂时不使用SUB ,因为它可能很容易被替换为: ADD NEGgative operandNEG也不是基本的,因为它可能被替换为: XOR FFFF...; ADD 1 XOR FFFF...; ADD 1

因此,您需要对操作数类型和大小进行相当复杂的基于比特的寻址。 您可以为单个机器代码指令(例如ADD )执行此操作,并计划在大多数其他指令中进一步使用它。 但到目前为止,您的同事在不使用SUB情况下完成余数的最优计算! 想象一下 - 它已经被称为“Optimal_Mod”,所以你错过了一些不理想的东西,并不是因为你是一个坏人并且讨厌AMD,而仅仅是因为你看到了 - 它已经被称为优化,优化了。

英特尔编译器一般来说相当不错,但它有很长的版本历史,因此在极少数情况下可能会出现奇怪现象。 我建议你告诉英特尔这个问题,看看会发生什么。

链接地址: http://www.djcxy.com/p/2465.html

上一篇: Why does Intel's compiler prefer NEG+ADD over SUB?

下一篇: Why can't GCC optimize out `std::sqrt`?