现在的编译器还需要手动展开循环吗_为什么循环总是被编译成“做…?而“风格”(尾跳)?...

循环中较少的指令/uop=更好。构造循环外部的代码以实现这一点通常是一个好主意,无论是它的分支来允许更好的循环结构,还是部分剥离第一次迭代,这样您就可以在一个方便的点进入循环,并可能保存一个mov指令或它里面的任何东西(不确定是否有名称;“软件流水线”是一些特定的东西)。

do{}while()是ASM中所有体系结构中循环的规范/惯用结构,请熟悉它。如果有它的名称,可以使用idk;我会说这样的循环有一个“dowhile结构”。如果你想要名字,你可以叫while()构造“糟糕的未优化代码”或“新手编写的代码”。:p循环-底部的分支是通用的,甚至不值得一提回路优化..你,你们总那就做吧。

这种模式被广泛使用,以至于在对没有分支预测器缓存中的分支使用静态分支预测的CPU上,未知的前向条件分支被预测为不接受,未知的反向分支被预测(因为它们可能是循环分支)。看见在新的Intel处理器上的静态分支预测在马特·戈德波特的博客上,以及阿格纳·福格的分支预测章节中,他的微弓PDF的开头。

这个答案在所有方面都使用了x86示例,但其中大部分都适用于所有体系结构。如果其他超标量/无序实现(如某些ARM或POWER)也有限的分支指令吞吐量(不管是否采用),我不会感到惊讶。但是循环中的指令很少是通用的,只有底部有条件的分支,没有无条件的分支。

如果循环可能需要运行0次编译器更经常将测试和分支放在循环之外以跳过它,而不是跳到底部的循环条件。(也就是说,如果编译器不能证明循环条件在第一次迭代中总是正确的)。

顺便说一下,本文呼叫转换while()到if(){ do{}while; }“反转”,但循环反转通常意味着反转一个嵌套的循环。(例如,如果源以错误的顺序遍历一个主要的多维数组,那么聪明的编译器可能会改变。for(i) for(j) a[j][i]++;进for(j) for(i) a[j][i]++;如果它能证明它是正确的。)但我想你可以看看if()作为一个零或一迭代循环。有趣的是,编译器开发人员教他们的编译器如何为(非常)特定的情况反转一个循环(以允许自动向量化)。为什么SPECint 2006的lib量子基准被“破坏”了?..在一般情况下,大多数编译器无法反转循环,只是那些看起来与SPECint 2006中的循环几乎完全相同的编译器.

您可以通过编写do{}while()循环在C中,当您知道调用者不被允许通过时size=0或者其他保证循环至少运行一次的东西。

(有符号循环界实际上为0或负值。有符号循环计数器和无符号循环计数器是一个棘手的优化问题,特别是当您选择比指针更窄的类型时,请检查编译器的ASM输出以确保它不是符号-如果将它用作数组索引,则在循环中很长时间扩展一个窄循环计数器。但是请注意,签名实际上是有帮助的,因为编译器可以假设i++ <= bound最终会变成假的,因为签名溢出是UB但没有签名就不是了。所以对于没有签名的人,while(i++ <= bound)是无限的,如果bound = UINT_MAX)我没有关于什么时候使用签名和未签名的一般性建议;size_t虽然循环数组通常是一个很好的选择,但是如果您想避免循环开销中的x86-64-REX前缀(为了节省多少代码),但是要说服编译器不要浪费任何指令零或符号扩展,这是很棘手的。我看不出有多大的性能提升

下面是一个例子,在Haswell之前,这种优化将在Intel CPU上加速2倍,因为P6和SNB/IVB只能在端口5上运行分支,包括不带条件的分支。

这种静态性能分析所需的背景知识:阿格纳雾微拱导轨(阅读沙桥部分)。还读了他的优化装配指南,很棒。(不过,有时在一些地方已经过时了。)还请参阅x86标记wiki。另见x86的MOV真的可以“免费”吗?为什么我不能复制这个?对于一些静态分析,用Perf计数器进行实验支持,以及对融合域和未融合域uop的一些解释。

你也可以用英特尔的IACA软件(英特尔架构代码分析器)对这些循环进行静态分析。; sum(int []) using SSE2 PADDD (dword elements)

; edi = pointer,  esi = end_pointer.

; scalar cleanup / unaligned handling / horizontal sum of XMM0 not shown.

; NASM syntax

ALIGN 16          ; not required for max performance for tiny loops on most CPUs

.looptop:                 ; while (edi

cmp     edi, esi    ; 32-bit code so this can macro-fuse on Core2

jae    .done            ; 1 uop, port5 only  (macro-fused with cmp)

paddd   xmm0, [edi]     ; 1 micro-fused uop, p1/p5 + a load port

add     edi, 16         ; 1 uop, p015

jmp    .looptop         ; 1 uop, p5 only

; Sandybridge/Ivybridge ports each uop can use

.done:                    ; }

这是4个完全融合域的uop(的宏融合cmp/jae),因此它可以在每个时钟一次迭代时从前端发出到无序核心。但在未融合域中,有4个ALU uops,Intel Pre-Haswell只有3个ALU端口。

更重要的是,端口5压力是瓶颈:此循环每两个周期只能执行一次迭代。因为CMP/JAE和JMP都需要在端口5上运行。其他uop窃取端口5可能会降低实际吞吐量,略低于此。

习惯性地为ASM编写循环,我们得到:ALIGN 16

.looptop:                 ; do {

paddd   xmm0, [edi]     ; 1 micro-fused uop, p1/p5 + a load port

add     edi, 16         ; 1 uop, p015

cmp     edi, esi        ; 1 uop, port5 only  (macro-fused with cmp)

jb    .looptop        ; } while(edi 

请立即注意,与其他所有内容无关,这是循环中的少一个指令。这种循环结构至少对从简单的非流水线8086到经典RISC(就像早期的MIPS),特别是长时间运行的循环(假设它们没有内存带宽瓶颈)。

Core2及更高版本应该在每个时钟上运行一次迭代。,速度是while(){}-结构化循环,如果内存不是瓶颈(即假设L1D命中,或者实际上至少L2;这只是每个时钟的SSE 2 16字节)。

这仅仅是3个融合域uop,因此可以在任何自Core2之后的任何时钟上以更好的一个时钟发出,或者如果问题组总是以一个已捕获的分支结束,则每个时钟仅发出一个。

但重要的是,端口5的压力大大降低了:只有cmp/jb需要它。其他uop可能会安排在某些时候移植5并从循环分支吞吐量中窃取周期,但这将是几%,而不是2的因子。确切地说,x86 uop是如何安排的?.

大多数CPU通常每2个周期有一个分支吞吐量,但仍然可以在每时钟1次执行微小的循环。不过也有一些例外。(我忘了哪个CPU不能以每时钟一个的速度运行紧环;也许是推土机家族?或者只是一些低功耗的CPU,比如通过Nano。)沙桥和核心2绝对可以在每个时钟运行一个紧密的循环。它们甚至有循环缓冲器;Core2在指令长度解码之后但在常规解码之前有一个循环缓冲区。Nehalem和稍后在队列中循环uop,该队列提供问题/重命名阶段。(除了skylake上的微码更新之外,因部分寄存器合并错误,Intel不得不禁用循环缓冲区。)

然而,循环携带的依赖链在……上面xmm0:Intel CPU有一个周期延迟。paddd所以我们也遇到了瓶颈。add esi, 16也是一个周期延迟。在Bulldozer家族中,即使整型向量运算也有2c延迟,因此在每次迭代2c时,循环都会受到瓶颈。(AMD自K8和Intel自SNB以来,每个时钟可以运行两个负载,因此我们需要展开无论如何最大吞吐量。)用浮点,你一定希望使用多个累加器展开。为什么毛罗斯在哈斯韦尔只需要三个周期,不同于阿格纳的指令表?.

如果我使用索引寻址模式,比如paddd xmm0, [edi + eax],我可以用sub eax, 16 / jnc在循环条件下。子/jnc可以在沙桥家族上进行宏融合,但是索引负载。在SNB/IVB上会不会层压板?(除非您使用AVX表单,否则请在Haswell和以后继续使用。); index relative to the end of the array, with an index counting up towards zero

add   rdi, rsi          ; edi = end_pointer

xor   eax, eax

sub   eax, esi          ; eax = -length, so [rdi+rax] = first element

.looptop:                  ; do {

paddd   xmm0, [rdi + rax]

add     eax, 16

jl    .looptop          ; } while(idx+=16 

(通常最好是展开一些以隐藏指针增量的开销,而不是使用索引寻址模式,特别是对于存储,部分原因是索引存储不能使用Haswell+上的port7存储AGU。)

关于核2/Nehalemadd/jl不要使用宏熔断器,因此即使在64位模式下,这也是3个融合域uop,而不依赖于宏融合.对于AMD K8/K10/Bulldozer家族/Ryzen:没有融合循环条件,但PADDD与内存操作数为1mop/uop。

在SNB上,padddUn-层板从负载,但添加/JL宏熔断器,因此再次3个融合域uop。(但在未融合域中,只有2个ALU uops+1个负载,因此可能减少了资源冲突,从而降低了循环的吞吐量。)

在hsw和更高版本上,这是2个融合域uop,因为索引负载可以保持与padd的微融合,并且add/jl宏观引信。(预期的分支运行在端口6上,因此永远不会出现资源冲突。)

当然,循环每个时钟最多只能运行一次迭代,因为即使对于微小的循环也有分支吞吐量限制。如果您在循环中还有其他事情要做,这个索引技巧可能很有用。

但是所有这些循环都没有展开

是的,这夸大了循环开销的影响。但GCC即使在-O3(除非委员会决定充分展开)。它只展开配置文件引导的优化,让它知道哪些循环是热的。(-fprofile-use)。您可以启用-funroll-all-loops,但我只建议在每个文件的基础上这样做,因为您知道编译单元有一个需要它的热循环。或者,即使是在每个函数的基础上,__attribute__,如果有这样的优化选项的话。

因此,这与编译器生成的代码高度相关。(但clang默认情况下是4展开小循环,2展开小循环,最重要的是使用多个累加器隐藏延迟。)

迭代次数非常低的好处:

考虑一下当循环体运行一两次时会发生什么:除了do{}while.为do{}while,执行是一条直线,底部没有分支,没有分支。太棒了。

为了if() { do{}while; }它可能会运行0次循环,这是两个未被接受的分支。那还是很不错的。(对于前端而言,未采取的价格略低于两者都被正确预测时的价格)。

对于一个从下到下的JMPjmp; do{}while(),它是一个无条件的分支,一个采取的循环条件,然后循环分支不被接受。这有点笨重,但是现代的分支预测器是非常好的.

为了while(){}结构,这是一个未被接受的循环出口,一个被占用。jmp在底部,然后在顶部有一个循环出口分支.

通过更多的迭代,每个循环结构都会多执行一个分支。while(){}此外,每次迭代还会多做一个未被接受的分支,因此很快就会变得更糟。

后两个循环结构有更多的跳跃小行程计数。

跳入底部对于非小循环也有一个缺点,即如果L1I缓存有一段时间没有运行,那么循环的底部可能会很冷。代码提取/预取很擅长用直线将代码带到前端,但是如果预测没有足够早地预测分支,则可能会出现跳到底部的代码丢失。此外,并行解码程序可能已经(或可能)解码了循环顶部的某些部分,同时对jmp到底部。

有条件地跳过do{}while循环避免了所有这些:只有在您跳过的代码根本不应该运行的情况下,您才会跳转到尚未运行的代码中。它通常能很好地预测,因为很多代码实际上从来没有在循环中执行0次。(也就是说,它可能是一个do{}while,编译器只是没有设法证明这一点。)

跳到底部也意味着核心不能开始工作在真正的循环身体后,直到前端追逐两个采取的分支。

在有些情况下,使用这种方式编写它是最容易的,而且对性能的影响很小,但编译器通常会避免这种情况。

具有多个退出条件的循环:

考虑memchr循环,或strchr循环:它们必须停在缓冲区的末尾(基于计数)或隐式长度字符串的末尾(0字节)。但他们也必须break如果他们在结束前找到匹配的话,就退出循环。

所以你经常会看到这样的结构do {

if () break;

blah blah;

} while(condition);

或者只是底部附近的两个条件。理想情况下,您可以使用相同的实际指令测试多个逻辑条件。5 < x && x < 25使用sub eax, 5 / cmp eax, 20 / ja .outside_range、用于范围检查的无符号比较技巧,或将其与OR到检查4条指令中任何一种情况下的字母字符)但有时您不能而且只需要使用if()break样式循环-退出分支以及一个正常的向后执行的分支。

进一步读:马特·戈德波特2017年的演讲:“我的编译器最近为我做了什么?”打开编译器的盖子“寻找查看编译器输出的好方法(例如,什么样的输入给出了有趣的输出,并为初学者提供了阅读x86 ASM的入门)。有关:如何消除GCC/clang组件输出中的“噪音”?

现代微处理器90分钟指南!..详细信息查看超标量流水线CPU,主要是与体系结构无关的。非常好。解释指令级并行性之类的东西。

和微弓pdf。这将使您从能够编写(或理解)正确的x86asm到能够编写

高效率ASM(或者查看编译器应该做什么)。

中的其他链接。x86标签维基,包括英特尔的优化手册。此外,我的一些答案(在标签wiki中链接)有一些Agner在最近的微体系结构测试中漏掉的东西(比如SNB上的微融合索引寻址模式的不分层,Haswell+上的部分注册)。

第7课:循环变换(也在Archive.org上)。编译器对循环做了很多很酷的事情,使用C语法来描述ASM。

有点离题:内存带宽几乎总是很重要,但大多数现代x86 CPU上的单个核心不能使DRAM饱和,这一点并不广为人知,而且甚至在单线程带宽为多个核心Xeons的情况下也不能接近。更糟而不是在具有双通道存储器控制器的四核上。.

关于内存,每个程序员都应该知道些什么?(我的回答评论了什么已经改变了,什么在UlrichDrepper的著名优秀文章中仍然有意义。)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值