Assembly 为什么英特尔&x27;s编译器更喜欢NEG+;加分?
在检查各种编译器输出的各种代码片段时,我注意到英特尔的C编译器(ICC)倾向于发出一对Assembly 为什么英特尔&x27;s编译器更喜欢NEG+;加分?,assembly,x86,micro-optimization,icc,Assembly,X86,Micro Optimization,Icc,在检查各种编译器输出的各种代码片段时,我注意到英特尔的C编译器(ICC)倾向于发出一对NEG+ADD指令,而其他编译器则使用一条SUB指令 作为一个简单的例子,考虑下面的C代码: uint64_t Mod3(uint64_t value) { return (value % 3); } ICC将其转换为以下机器代码(无论优化级别如何): 而其他编译器(包括MSVC、GCC和Clang)都将生成本质上等效的代码,除了NEG+ADD序列被单个SUB指令替换 正如我所说,这不仅仅是ICC如何
NEG
+ADD
指令,而其他编译器则使用一条SUB
指令
作为一个简单的例子,考虑下面的C代码:
uint64_t Mod3(uint64_t value)
{
return (value % 3);
}
ICC将其转换为以下机器代码(无论优化级别如何):
而其他编译器(包括MSVC、GCC和Clang)都将生成本质上等效的代码,除了NEG
+ADD
序列被单个SUB
指令替换
正如我所说,这不仅仅是ICC如何编译这个特定片段的一个怪癖。这是我在分析算术运算的反汇编时反复观察到的一种模式。我通常不会太在意这个,除了ICC是一个非常好的优化编译器,它是由那些掌握微处理器内幕信息的人开发的
英特尔是否了解在其处理器上实现SUB
指令的一些信息,从而使其能够更好地分解为NEG
+ADD
指令?使用RISC风格的指令解码为更简单的微体系结构是众所周知的现代微体系结构优化建议,因此,SUB
是否可能在内部分解为单个NEG
和ADD
微体系结构,前端解码器使用这些“更简单”的指令实际上效率更高说明书现代CPU是复杂的,所以一切皆有可能
不过,请证实我的直觉,这实际上是一种悲观<在所有处理器上,code>SUB与ADD
一样高效,因此额外需要的NEG
指令只会降低速度
我还运行了两个序列来分析吞吐量。尽管精确的循环计数和端口绑定因微体系结构而异,但从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
相比,吞吐量净下降。那么英特尔的编译器为什么要这么做呢
这只是代码生成器的一些怪癖,应该被报告为缺陷,还是我在分析中遗漏了一些优点?奇怪的是,我有一个简单的答案:因为ICC不是最优的
当您编写自己的编译器时,您将开始使用一些非常基本的操作代码集:NOP
,MOV
,ADD
。。。最多10个操作码。暂时不要使用SUB
,因为它很容易被替换为:ADD NEGgative operand
NEG也不是基本的,因为它可能被替换为:XOR FFFF。。。;添加1
因此,您可以实现相当复杂的操作数类型和大小的基于位的寻址。您可以为单个机器代码指令(例如,ADD
)执行此操作,并计划将其进一步用于大多数其他指令。但此时,您的同事完成了余数优化计算的实现,而无需使用SUB
!想象一下——它已经被称为“优化模式”,所以你错过了一些不适时的事情,不是因为你是个坏人,讨厌AMD,而是因为你看到了——它已经被称为优化模式,优化模式
“英特尔编译器”总体上相当不错,但它有很长的版本历史,所以在某些罕见的情况下,它的行为可能会很奇怪。我建议你将这个问题通知英特尔,看看会发生什么。我想知道。如果您替换了添加rdi、rsi;mov rax,rdi
与lea rax,[rdi+rsi]
?更有趣的是,多代Intel编译器似乎都在这样做(Cody Gray似乎在使用2016版,而我使用2013版),而且这似乎独立于指定的体系结构目标,使其不太可能用于解决旧体系结构中的错误。即使在OOO机器中,像这里发生的那样,延长依赖链通常也不是一个好主意。将此作为性能缺陷报告给英特尔可能是个好主意,看看他们会带来什么。无论如何,这不是英特尔编译器正在做的,所以这与这个问题有些无关。如果你想看到更多关于它的讨论,你可以发布一个新问题。至于老定时器的评论,是的,我完全希望答案有点模糊,因为我在提问之前已经做了基本的尽职调查。但这肯定不会让这个问题成为“基于意见的”。正如njuffa所说,我在ICC上看到的所有架构目标都是这样的,所以这似乎不太可能是针对某一代的缺陷或怪癖。也许这会损害AMD。是的,这段代码在Intel上比optimal慢,但它对AMD的伤害更大,因此相比之下,Intel处理器看起来不错。ICC充满了代码,如果它检测到非英特尔处理器,就会主动禁用优化。结果比我想象的要简单得多……ICC在明显的情况下使用sub
,比如减去两个参数。我记不起任何其他复杂代码的好例子,我看到它生成了neg
+add
而不是简单的子代码,但是。你的第一段是对的,但ICC并没有简单到你的第二段是可信的。(还有,xor
和-1是一个补码否定(not
),而不是2的补码(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