为什么ICC会以这种方式展开此循环并使用LEA进行算术
Why does ICC unroll this loop in this way and use lea for arithmetic?
查看ICC 17生成的代码,用于迭代:: unordered_map<>(使用https://godbolt.org)使我感到非常困惑。
我将示例提炼为:
long count(void** x)
{
long i = 0;
while (*x)
{
++i;
x = (void**)*x;
}
return i;
}
用ICC 17与-O3标志一起编译,从而导致以下拆卸:
count(void**):
xor eax, eax #6.10
mov rcx, QWORD PTR [rdi] #7.11
test rcx, rcx #7.11
je ..B1.6 # Prob 1% #7.11
mov rdx, rax #7.3
..B1.3: # Preds ..B1.4 ..B1.2
inc rdx #7.3
mov rcx, QWORD PTR [rcx] #7.11
lea rsi, QWORD PTR [rdx+rdx] #9.7
lea rax, QWORD PTR [-1+rdx*2] #9.7
test rcx, rcx #7.11
je ..B1.6 # Prob 18% #7.11
mov rcx, QWORD PTR [rcx] #7.11
mov rax, rsi #9.7
test rcx, rcx #7.11
jne ..B1.3 # Prob 82% #7.11
..B1.6: # Preds ..B1.3 ..B1.4 ..B1.1
ret #12.10
与明显的实现(GCC和Clang使用,即使在-O3)相比,它似乎对一些事情有所不同:
- 它可以展开循环,在循环后循环之前,有两个降低 - 但是,中间有一个有条件的跳跃。
- 它使用lea作为某些算术
- 它可以保留每个两个迭代的计数器(INC RDX),并立即计算每次迭代的相应计数器(进入RAX和RSI)
做所有这一切的潜在好处是什么?我认为这可能与计划有关?
仅用于比较,这是GCC 6.2生成的代码:
count(void**):
mov rdx, QWORD PTR [rdi]
xor eax, eax
test rdx, rdx
je .L4
.L3:
mov rdx, QWORD PTR [rdx]
add rax, 1
test rdx, rdx
jne .L3
rep ret
.L4:
rep ret
这不是一个很好的例子,因为循环在指针降低延迟上琐碎的瓶颈,而不是uop吞吐量或任何其他类型的环路。但是,有些情况下,UOPS较少的情况可能会帮助您可能会发现更远的情况。,或者我们可以谈论循环结构的优化并假装它们重要,例如对于做其他事情的循环。
即使在循环跳闸计数不可计算的情况下,展开也可能有用。(例如,在这样的搜索循环中,它在找到哨兵时停止)。一个未验证的条件分支与一个分支不同,因为它对前端没有任何负面影响(当它正确预测时)。
基本上ICC只是在展开此循环的情况下做得不好。它使用LEA和MOV处理i
的方式是BrainDead,因为它使用的UOPS多于两个inc rax
说明。(尽管它确实使关键路径变短,但在IVB上,后来具有零延迟的mov r64, r64
,因此运行这些UOPS可以取得成功)。
当然,由于这种特殊的循环瓶颈在指针播种的延迟上,您将充其量获得一个每4个时钟的长链吞吐量(Skylake上的L1负载延迟,用于整数寄存器)或大多数其他英特尔微体系结构上的每5个时钟。(我没有仔细检查这些潜伏期;不要相信这些特定的数字,但是它们是正确的)。
IDK如果ICC分析了循环依赖链以决定如何优化。如果是这样,它可能根本没有展开,如果知道它确实试图展开时做得很差。
对于短链,止境执行可能能够在循环后开始运行某些内容,如果 loop-exit分支正确预测了。在这种情况下,优化循环是有用的。
展开还会在问题上抛出更多的分支机构条目。,而不是一个带有长图案的环路 - exiT分支(例如,在15次服用后不知所措),您有两个分支。在同一例子中,一个从未被拍摄过的例子,一个需要7次,然后第八次未能。
这是一个手写的通过两个实现的移动外观,看起来像:
在一个出口点之一的循环外路路径中修复i
,因此您可以在循环内廉价处理。
count(void**):
xor eax, eax # counter
mov rcx, QWORD PTR [rdi] # *x
test rcx, rcx
je ..B1.6
.p2align 4 # mostly to make it more likely that the previous test/je doesn't decode in the same block at the following test/je, so it doesn't interfere with macro-fusion on pre-HSW
.loop:
mov rcx, QWORD PTR [rcx]
test rcx, rcx
jz .plus1
mov rcx, QWORD PTR [rcx]
add rax, 2
test rcx, rcx
jnz .loop
..B1.6:
ret
.plus1: # exit path for odd counts
inc rax
ret
如果两个测试/JCC配对宏观融合,则可以使Loop Body 5融合域UOPS。Haswell可以在单个解码组中进行两个融合,但是早期的CPU不能。
GCC的实现仅为3个UOPS,这比CPU的问题宽度小。请参阅此Q& a关于从循环缓冲区发出的小循环。没有CPU实际上每个时钟都能执行/退休一个以上的分支,因此不容易测试CPU的发行循环如何少于4个UOPS,但显然Haswell可以以每1.25个周期的一个1循环发出5-UOP循环。较早的CPU可能只以每2个周期的一个一个循环发出。
-
没有明确的答案,因为它是专有编译器,因为它是这样做的。只有英特尔知道为什么。也就是说,英特尔编译器通常在循环优化方面更具侵略性。这并不意味着更好。我已经看到,与Clang/GCC相比,英特尔的侵略性内在表现的情况下。在这种情况下,我不得不明确禁止在某些呼叫站点上内部。同样,有时候有必要通过英特尔C 的布拉格斯(Pragmas)进行展开以获得更好的性能。
-
lea
是一项特别有用的指令。它允许一次班次,两次添加和一个移动,仅在一项指令中全部移动。这比进行这四个操作分开要快得多。但是,这并不总是有所作为。而且,如果lea
仅用于添加或移动,则可能会更好。因此,您在7.11中看到它使用移动,而在接下来的两行中,lea
用于进行加法和移动,加法,移动以及移动 -
我看不到这里有可选的好处
- 如何循环打印顶点结构
- 如何在C++中从两个单独的for循环中添加两个数组
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 正在尝试了解输入验证循环
- std::map<struct,struct>::find 找不到匹配项,但是如果我循环通过 begin() 到 end(),我在那里看到匹配项
- 循环后如何继续阅读
- Ardunio UNO解决了多个重叠的定时器循环
- Eigen如何在容器循环中干净地附加矩阵
- 在某些循环内使用vector.push_back时出现分段错误
- 我正在使用嵌套的while循环来解析具有多行的文本文件,但由于某种原因,它只通过第一行,我不知道为什么
- 为什么我的for循环不能正确获取argv
- 如何声明特征矩阵,然后通过嵌套循环初始化它
- while循环中while循环的时间复杂度是多少
- C++中的高效循环缓冲区,它将被传递给C样式数组函数参数
- 为什么在这个代码结束循环中没有得到结束
- 在基于范围的for循环中使用结构化绑定声明
- 用于C++中带有数组和指针的循环
- 循环中的随机函数
- 是什么阻止DOMTimerCoordinator::NextID进入无休止的循环
- 为什么ICC会以这种方式展开此循环并使用LEA进行算术