为什么循环指令慢?英特尔无法有效实施吗?

Pet*_*des 53 performance x86 assembly intel cpu-architecture

LOOP(英特尔参考手动输入)递减ecx/rcx,然后如果非零则跳转.这很慢,但是英特尔不能廉价地把它变得很快吗? dec/jnz已经将宏观融合成 Sandybridge家族的一个 uop; 唯一的区别是设置标志.

loop关于各种微体系结构,来自Agner Fog的说明表:

  • K8/K10:7 m-ops
  • Bulldozer-family/Ryzen:1 m-op(与宏观融合测试和分支相同,或者jecxz)

  • P4:4次(相同jecxz)

  • P6(PII/PIII):8次
  • Pentium M,Core2:11 uops
  • Nehalem:6个uops.(11为loope/ loopne).吞吐量= 4c(loop)或7c(loope/ne).
  • SnB家族:7个uops.(11为loope/ loopne). 吞吐量=每5个循环一个,这是将循环计数器保留在内存中的瓶颈!jecxz只有2 uops,吞吐量与普通吞吐量相同jcc
  • Silvermont:7次
  • AMD Jaguar(低功耗):8 uops,5c吞吐量
  • 通过Nano3000:2 uops

难道解码器不能像lea rcx, [rcx-1]/ 那样解码jrcxz吗?这将是3 uops.至少那是没有地址大小前缀的情况,否则它必须使用ecx和截断RIP,EIP如果跳转; 也许奇怪的地址大小选择控制减量的宽度解释了许多uops?

或者更好,只需将其解码为不设置标志的融合分支和分支? dec ecx/ jnz在SnB上解码为单个uop(设置标志).

我知道真正的代码不使用它(因为至少P5或其他东西它一直很慢),但是AMD决定让Bulldozer加速它是值得的.可能因为它很容易.


  • SnB系列uarch能够快速获得快速loop吗? 如果是这样,他们为什么不呢?如果没有,为什么这么难?很多解码器晶体管?或者在融合的dec&branch uop中的额外位来记录它没有设置标志?7个微博会做什么?这是一个非常简单的指令.

  • Bulldozer有什么特别之处,loop让它快速轻松/值得吗? 或者AMD是否会浪费一堆晶体管来loop快速制造?如果是这样,大概有人认为这是个好主意.


如果loop速度很快,它对于BigInteger任意精度adc循环来说是完美的,可以避免部分标记停顿/减速 (请参阅我对我的回答的评论),或任何其他你想要循环而不触及标志的情况.它还具有较小的代码大小优势dec/jnz.(而且dec/jnz只有SnB系列的宏保险丝).

在现有的CPU中,dec/jnz在ADC循环中没问题,loop对于ADCX/ADOX循环(保留OF)仍然很好.

如果loop速度很快,编译器就已经将它用作窥视孔优化,以便在没有宏融合的情况下实现CPU的代码大小+速度.


它不会阻止我对所有loop用于每个循环的错误16位代码的问题感到恼火,即使他们还需要循环内的另一个计数器.但至少它不会那么糟糕.

Pet*_*des 28

现在我写完问题之后用Google搜索了,结果证明它与comp.arch上的一个完全相同,后者立刻就出现了.我预计它很难谷歌(很多"为什么我的循环慢"命中),但我的第一次尝试(why is the x86 loop instruction slow)得到了结果.

这不是一个好的或完整的答案.

它可能是我们得到的最好的,并且必须足够,除非有人可以更多地了解它.我没有打算把它写成一个回答我自己的问题的帖子.


在该主题中有不同理论的好帖子:

罗伯特

当一些最早的机器(大约486)开始发生重大流水线时,LOOP变得缓慢,并且在管道中运行任何但最简单的指令在技术上是不切实际的.所以LOOP很多代都很慢.所以没人用过它.因此,当它有可能加快速度时,没有真正的动力这样做,因为没有人真正使用它.


Anton Ertl:

IIRC LOOP在某些软件中用于定时循环; 有(重要的)软件不能在LOOP太快的CPU上工作(这是在90年代初左右).因此CPU制造商学会了让LOOP变慢.


(保罗,以及其他任何人:欢迎你重新发布自己的写作作为自己的答案.我会从我的答案中删除它并向你投票.)

@Paul A. Clayton(偶尔的SO海报和CPU架构人员) 猜测你如何使用那么多uops.(这看起来像loope/ne检查计数器 ZF):

我可以想象一个可能合理的6-μop版本:

virtual_cc = cc; 
temp = test (cc); 
rCX = rCX - temp; // also setting cc 
cc = temp & cc; // assumes branch handling is not 
       // substantially changed for the sake of LOOP 
branch 
cc = virtual_cc 
Run Code Online (Sandbox Code Playgroud)

(注意,对于LOOPE/LOOPNE,这是6 uop,而不是SnB的11,并且总是猜测甚至没有考虑从SnB perf计数器中获知的任何东西.)

然后保罗说:

我同意一个较短的序列应该是可能的,但我试图想到一个膨胀的序列,如果 允许最小的微架构调整可能是有意义的.

摘要:设计人员希望通过微码loop支持,不需要对硬件进行任何调整.

如果将无用的,仅兼容性指令交给微代码开发人员,他们可能无法或不愿意建议对内部微体系结构进行微小更改以改进此类指令.他们不仅会更有效地使用他们的"变更建议资本",而且对无用案例进行变更的建议会降低其他建议的可信度.

(我的观点是:英特尔可能仍然会故意放慢速度,并且长时间没有为它重写微码而烦恼.现代CPU对于任何loop以天真的方式正常工作的东西来说可能太快了.)

......保罗继续说道:

Nano背后的建筑师可能已经发现避免使用LOOP的特殊外壳简化了他们在面积或功率方面的设计.或者他们可能有来自嵌入式用户的激励来提供快速实现(代码密度优势).这些只是怀旧的 猜测.

如果LOOP的优化不属于其他优化(比如比较和分支的融合),那么将LOOP调整为快速路径指令可能比在微代码中处理它更容易,即使LOOP的性能不重要.

我怀疑这些决定是基于实施的具体细节.有关此类详细信息的信息似乎并不普遍可用,并且解释此类信息将超出大多数人的技能水平.(我不是硬件设计师 - 从来没有在电视上播放或者住在Holiday Inn Express.:-)


然后,线程进入AMD的领域,让我们有机会清理x86指令编码中的错误.很难归咎于他们,因为每次改变都是解码器无法共享晶体管的情况.在英特尔采用x86-64之前,它甚至都不清楚它会流行起来.如果AMD64没有流行,AMD不希望用没有人使用过的硬件加载他们的CPU.

但是,仍然有很多小事情:本 setcc可以改为32位.(通常你必须使用xor-zero/test/setcc来避免错误的依赖,或者因为你需要一个零扩展的reg).Shift可以具有无条件写入的标志,即使在零移位计数的情况下(对于OOO执行的可变计数移位,移除输入数据对eflags的依赖性).上次我输入这个宠物小便的列表,我认为还有第三个...哦,是的,bt/ bts等内存操作数的地址取决于索引的高位(位串,而不仅仅是机器内的位)字).

bts指令对于位字段的东西非常有用,并且比它们需要的速度慢,因此您几乎总是想要加载到寄存器然后使用它.(通常更快地移动/掩码以获得地址,而不是bts [mem], reg在Skylake 上使用10 uop ,但它确实需要额外的指令.所以它在386上有意义,但在K8上没有意义).原子位操作必须使用memory-dest形式,但locked版本无论如何都需要大量的uops.它仍然比在dword它运行之外无法访问时更慢.

  • @RossRidge和未来的读者:一个很好的例子是[为了避免'adc`循环中的部分标记问题](http://stackoverflow.com/questions/32084204/problems-with-adc-sbb-和INC日 - 12月在紧包环路上,一些-的CPU/32087095#32087095).一个廉价的循环而不触摸标志的方式正是你想要的任意大小的BigInteger`adc`循环.所以AMD Bulldozer家族在那里有着坚实的优势,甚至与英特尔布罗德威尔相比,后来在那里"adc` 1-uop insn.编译器已经可以将字节计数放入ecx中用于`rep stos`等等; 我不认为这很难使用. (5认同)
  • 我的理解基本上就是罗伯特所说的.自'386以来,LOOP指令比DEC/JNZ慢.即使在'86和'286上,它只有2和1个周期更快,这意味着那些使用限制性更强的LOOP指令的处理器经常出错.我不确定当时是否有任何常见的16位编译器生成指令.即使在今天,我认为编写一个能够有效使用它的编译器也很困难.所以没有代码使用它,甚至他们确实改进了指令,它不清楚它是否真的开始被使用. (2认同)

I. *_*edy 18

1988年,IBM同事格伦亨利刚刚加入戴尔公司,戴尔当时有几百名员工,在他的第一个月,他就386个内部组织进行了技术讲座.一群美国BIOS程序员一直想知道为什么LOOP比DEC/JNZ慢,所以在问题/答案部分,有人提出了这个问题.

他的回答很有道理.它与分页有关.

LOOP由两部分组成:递减CX,然后如果CX不为零则跳跃.第一部分不能导致处理器异常,而跳转部分可以.例如,您可以跳转(或通过)到段边界外的地址,从而导致SEGFAULT.对于两个,您可以跳转到换出的页面.

SEGFAULT通常表示进程的结束,但页面错误是不同的.当发生页面错误时,处理器抛出异常,OS执行内务处理以将页面从磁盘交换到RAM.之后,它会重新启动导致故障的指令.

重新启动意味着将进程状态恢复到违规指令之前的状态.特别是在LOOP指令的情况下,它意味着恢复CX寄存器的值.有人可能会认为你可以只添加1到CX,因为我们知道CX已经减少,但显然,它并不那么简单.例如,查看英特尔的这个错误:

涉及的保护违规通常表示可能的软件错误,如果发生其中一个违规,则不需要重新启动.在任何总线周期期间具有等待状态的保护模式80286系统中,当80286组件检测到某些保护违规,并且组件将控制转移到异常处理例程时,CX寄存器的内容可能不可靠.(CX内容是否更改是内部微码检测到保护违规时总线活动的函数.)

为了安全起见,他们需要在LOOP指令的每次迭代中保存CX的值,以便在需要时可靠地恢复它.

节省CX的额外负担使LOOP变得如此缓慢.

与当时的其他所有人一样,英特尔正在获得越来越多的RISC.旧的CISC指令(LOOP,ENTER,LEAVE,BOUND)正在逐步淘汰.我们仍然在手工编码的程序集中使用它们,但编译器完全忽略它们.

  • LOOP指令本身不会导致页面错误.如果未映射目标页面,则会发生页面错误,并将CS:EIP设置为目标并更新ECX.但是,如果目标超出CS段限制,则LOOP指令可能会导致一般性保护(#GP)故障,在这种情况下,ECX需要保持不变.但是,最简单的实现方法是仅在(ECX - 1)== 0时跳转,检查分段边界,然后减少ECX.请参阅"英特尔软件开发人员手册"中的LOOP条目,以了解其工作原理的详细信息. (4认同)
  • 实际上,更仔细地阅读本手册,操作部分建议如果LOOP指令导致#GP故障,ECX将被更改,所以我不确定实际情况如何. (3认同)
  • 不过我很惊讶;我认为从无效页面获取代码会给你一个 EIP = 跳转目标的页面错误,所以重新运行跳转指令本身不会发生。但也许英特尔将检查内置到跳转指令中?如果fall-through也能做到,那么任何指令在页面末尾都有潜在的问题。(除非我弄错了,从逻辑上讲,在 x86 中跳转到无效页面会成功并且本身不会出错,但是从该新地址获取代码可能会出错。)不过,+1 因为 286 勘误是一些确凿的证据,表明存在这里是真实的。 (2认同)
  • 那么为什么 `call` 比 `push $+X, call Y` 更受欢迎,其中 X 是 `call` 的返回地址呢?由于“call”本质上做了两件事:将返回地址压入堆栈,然后跳转到某个地址,因此它应该具有与“loop”指令相同的问题,不是吗? (2认同)

Max*_*tin 7

请参阅Abrash,迈克尔的好文章,发表在Dr. Dobb's Journal 1991年3月v16 n3 p16(8):http://archive.gamedev.net/archive/reference/articles/article369.html

该文章的摘要如下:

优化8088,80286,80386和80486微处理器的代码很困难,因为芯片使用明显不同的存储器架构和指令执行时间.代码无法针对80x86系列进行优化; 相反,代码必须设计为在一系列系统上产生良好性能,或者针对处理器和内存的特定组合进行优化.程序员必须避免8088支持的异常指令,这些指令在后续芯片中失去了性能优势.应使用字符串指令但不依赖于字符串指令.应该使用寄存器而不是内存操作.所有四个处理器的分支也很慢.应该对齐内存访问以提高性能.通常,优化80486需要与优化8088完全相反的步骤.

通过"8088支持的异常指令",作者也意味着"循环":

任何8088程序员都会本能地替换:DEC CX JNZ LOOPTOP:LOOP LOOPTOP因为在8088上LOOP明显更快.在286上LOOP也更快.然而,在386上,LOOP实际上比DEC/JNZ慢两个周期.钟摆在486上进一步摆动,其中LOOP大约是DEC/JNZ的两倍 - 而且,请注意,我们正在讨论最初可能是整个80x86指令集中最明显的优化.

这是一篇非常好的文章,我强烈推荐它.虽然它是在1991年出版的,但它今天出人意料地高度相关.

但是这篇文章只是给出了建议,它鼓励测试执行速度并选择更快的变体.它没有解释为什么一些命令变得非常慢,所以它没有完全解决你的问题.

答案是早期的处理器,如80386(1985年发布)及之前,按顺序逐个执行指令.

后来的处理器开始使用指令流水线 - 最初,简单,804086,最后,Pentium Pro(1995年发布)引入了完全不同的内部流水线,称其为Out Of Order(OOO)核心,其中指令被转换为小碎片称为微操作或微操作的操作,然后将不同指令的所有微操作放入大量微操作池中,只要它们不相互依赖,它们就应该同时执行.这种OOO管道原理在现代处理器上仍然使用,几乎没有变化.您可以在这篇精彩的文章中找到有关指令流水线的更多信息:https://www.gamedev.net/resources/_/technical/general-programming/a-journey-through-the-cpu-pipeline-r3115

为了简化芯片设计,英特尔决定以一种非常有效的方式将一条指令转换为微操作的方式构建处理器,而其他指令则不然.

从指令到微操作的高效转换需要更多的晶体管,因此英特尔决定以更慢的解码和执行某些"复杂"或"很少使用"指令为代价来节省晶体管.

例如,"英特尔®架构优化参考手册" http://download.intel.com/design/PentiumII/manuals/24512701.pdf提到以下内容:"避免使用复杂的指令(例如,输入,离开或循环) )通常具有超过4μs并且需要多个周期来解码.改为使用简单指令序列."

因此,英特尔不知何故已经决定"循环"指令是"复杂的",从那以后,它变得非常缓慢.但是,英特尔没有关于指令细分的官方参考:每个指令产生多少个微操作,以及解码它需要多少个周期.

您还可以阅读"英特尔®64和IA-32架构优化参考手册"中的无序执行引擎 http://www.intel.com/content/dam/www/public/us/en/文件/手册/ 64-ia-32-architecturalures-optimization-manual.pdf 2.1.2节.

  • P6对uops的解码解释了为什么LOOP在PPRO中的速度很慢,但是Sandybridge将`dec rcx/jnz looptop`解码为单个uop(宏融合).问题是为什么LOOP*在Sandybridge上仍然*缓慢,因为单个uop可以执行LOOP所做的一切(除了保持标志不被修改). (2认同)
  • 至于解码和执行需要多少周期,Agner Fog的实验测试告诉我们它可以在Skylake上以每5个周期一个吞吐量执行.它产生多个uop,因此必须由第一个(复杂的)解码器解码,然后在一个周期内解码.由于它产生超过4个uop(Skylake上有7个),因此从微码ROM中读取微指令.从uop-cache切换到microcode会降低前端的速度(http://stackoverflow.com/questions/26907523/branch-alignment-for-loops-involving-micro-coded-instructions-on-intel-snb-famil ). (2认同)
  • 我们知道它在SnB系列上解码为7 uop,我们也知道解码器/ uop-cache/microcode ROM如何工作,足以详细说明你的理论.有许多事件的CPU性能计数器,英特尔已经在其CPU内部发布了一些信息.Agner Fog使用这些信息+他自己的实验来编写CPU微体系结构的详细描述.请参阅http://agner.org/optimize/上的microarch.pdf,以及[x86标签维基]中的其他内容(http://stackoverflow.com/tags/x86/info) (2认同)