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

Cod*_*ray 43 x86 assembly icc micro-optimization

在研究各种编译器的各种代码段的输出,我已经注意到,英特尔的C编译器(ICC)具有很强的偏爱发射一对趋势NEG+ ADD指令,其中其他的编译器将使用一个单一的SUB指令.

举个简单的例子,考虑以下C代码:

uint64_t Mod3(uint64_t value)
{
    return (value % 3);
}
Run Code Online (Sandbox Code Playgroud)

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         
Run Code Online (Sandbox Code Playgroud)

而其他编译器(包括MSVC,GCC和Clang)都将生成基本相同的代码,除了NEG+ ADD序列被单个SUB指令替换.

就像我说的,这不仅仅是ICC如何编写这个特定片段的怪癖.这是我在分析算术运算的反汇编时反复观察到的模式.我通常不会考虑这个,除了ICC是一个非常好的优化编译器,它是由拥有有关其微处理器的内幕信息的人开发的.

英特尔是否知道有关SUB其处理器上指令的实现的信息,将其分解为NEG+ ADD指令会更加优化?使用RISC样式的指令解码成更简单的μops是现代微体系结构的众所周知的优化建议,因此有可能SUB在内部分解为单个NEGADDμops,并且前端解码器使用这些实际上更有效"更简单"的指示?现代CPU很复杂,所以一切皆有可能.

然而,Agner Fog的综合指导表证实了我的直觉,这实际上是一种悲观情绪.SUBADD所有处理器一样有效,因此额外的必需NEG指令只会减慢速度.

我还通过英特尔自己的架构代码分析器运行这两个序列来分析吞吐量.虽然精确的循环计数和端口绑定从一个微体系结构到另一个体系结构不同,SUB但从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
Run Code Online (Sandbox Code Playgroud) 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
Run Code Online (Sandbox Code Playgroud)

因此,据我所知,NEG+ ADD增加了代码大小,增加了μops的数量,增加了执行端口的压力,并增加了循环次数,从而导致吞吐量净减少SUB.那么为什么英特尔的编译器会这样做呢?

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

kay*_*y27 2

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

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

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

英特尔编译器总体来说相当不错,但它有很长的版本历史,因此在某些罕见的情况下它可能会表现得很奇怪。我建议您将此问题告知英特尔并看看会发生什么。

  • 你的第一段是正确的,但国际刑事法院并没有那么简单,以至于你的第二段是合理的。(此外,“xor”与 -1 是补码否定(“not”),而不是 2 的补码(“neg”)。“x ^ 0xFFFFFFFF = -x - 1”;因此“not”+“inc” 模拟“负`)。您的第三段更合理:这可能是按常量取模的“固定”序列的一部分,并且不会以正常方式进行优化。 (2认同)