【问题标题】:Why is LOOP so slow? [duplicate]为什么 LOOP 这么慢? [复制]
【发布时间】:2014-03-01 03:55:42
【问题描述】:

这让我很惊讶,因为我一直认为loop应该有一些内部优化。

这是我今天做的实验。我使用的是 Microsoft Visual Studio 2010。我的操作系统是 64 位 Windows 8。我的问题在最后。

第一个实验:

平台:Win32
模式:调试(禁用优化)

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    loop start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

输出:passed time: 3.583
(每次运行的数字都会略有变化,但在道德上是相同的大小。)

第二次实验:

平台:Win32
模式:调试

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    dec ecx
    jnz start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

输出:passed time: 0.903

第三和第四个实验:

只需将平台更改为 x64。由于 VC++ 不支持 64 位内联汇编,我必须将循环放在另一个 *.asm 文件中。但最后结果是一样的。

从这一点开始,我开始使用我的大脑 - loopdec ecx, jnz start 慢 4 倍,它们之间的唯一区别,AFAIK,是 dec ecx 更改标志,而 loop 没有。为了模仿这个标志,我做了

第五次实验:

平台:Win32(以下我一直假设平台对结果没有影响)
模式:调试

begin = clock();
_asm
{
    mov ecx, 07fffffffh
    pushf
start:
    popf
; do the loop here
    pushf
    dec ecx
    jnz start
    popf
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

输出:passed time: 22.134

这是可以理解的,因为pushfpopf 必须玩内存。但是,例如,寄存器eax 不保留在循环的末尾(这可以通过更好地安排寄存器来实现),并且在循环中不需要标志OF (这简化了事情,因为OF不在flag的低8位中),那么我们可以使用lahfsahf来保留标志,所以我做了

第六次实验:

平台:Win32
模式:调试

begin = clock();
_asm
{
    mov ecx, 07fffffffh
    lahf
start:
    sahf
; do the loop here
    lahf
    dec ecx
    jnz start
    sahf
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

输出:passed time: 1.933

这还是比直接使用loop好很多吧?

我做的最后一个实验是尝试同时保留OF 标志。

第七次实验:

平台:Win32
模式:调试

begin = clock();
_asm
{
    mov ecx, 07fffffffh
start:
    inc al
    sahf
; do the loop here
    lahf
    mov al, 0FFh
    jo dec_ecx
    mov al, 0
dec_ecx:
    dec ecx
    jnz start
}
end = clock();
cout<<"passed time: "<<double(end - begin)/CLOCKS_PER_SEC<<endl;

输出:passed time: 3.612

这个结果是最坏的情况,即OF 没有在每个循环中设置。而且和直接用loop差不多……

所以我的问题是:

  1. 我说的对吗,使用循环的唯一优点是它可以处理标志(实际上只有dec 对其中的 5 个标志有效)?

  2. 是否有更长的lahfsahf也移动OF,这样我们就可以完全摆脱loop

【问题讨论】:

  • 因为 pushf 和 popf 必须使用内存。 是的,循环携带的依赖项包括从 pushf 到 popf 的存储转发,然后通过 EFLAGS 到下一个 pushf。但是这里最大的成本是popf 本身,它非常慢,因为它写入了所有的 EFLAGS,包括 IF 和其他机器状态标志,而不仅仅是条件代码。 (但如果您不尝试从用户空间更改它们,则不必出错)。例如,Sandybridge 上的popf 是 9 微指令,每 18 个时钟吞吐量 1 (agner.org/optimize)

标签: assembly


【解决方案1】:

从历史上看,在 8088 和 8086 处理器上,LOOP 是一种优化,因为它只比条件分支多花费一个周期,而在分支之前放置一个 DEC CX 将花费三个或四个周期(取决于预取队列)。

然而,今天的处理器与 8086 的工作方式大不相同。在几代处理器中,尽管制造商制造的机器基本上可以正确处理 8088/8086 或其后代曾经拥有的所有文档化指令,但它们'他们将精力集中在只提高最有用指令的性能上。由于各种原因,英特尔或 AMD 必须添加到现代 CPU 中以使 LOOP 指令像 DEC CX/JNZ 一样有效地工作的电路数量可能会超过电路的总数量在整个 8086 中,可能相差很大。制造商并没有增加其高性能 CPU 的复杂性,而是包含了一个更简单但速度较慢的处理单元,可以处理“晦涩”的指令。虽然高性能 CPU 需要大量电路来允许多条指令的执行重叠,除非后面的指令需要早期计算的结果(并且必须等到它们可用),但“晦涩的指令处理单元”可以避免需要这样的电路,只需一次执行一条指令。

【讨论】:

  • 简短版:多年来,“RISCy”指令比“CISCy”指令得到了更多的优化。因此,“明显”的老式优化现在会损害性能。 :-)
  • @BrianKnoblauch:一些 CISCy 指令也得到了优化(例如 REP MOVSD)。我认为主要问题是,如果一个 CISCy 指令代表一个通用操作,该操作可以被优化以比一系列 RISCy 指令执行 更好,那么这种优化是值得的(让 REP MOVSD 在两个周期/字,而不是使 4-5 指令循环以该速度运行),但如果 CISC 不能胜过 RISC,则建议人们避免使用 CISC 指令与通过添加硬件使它们一样快,可以获得同样多的性能作为 RISC。
  • @BrianKnoblauch:顺便说一下,考虑到 JVM 和 .NET 运行时的流行,我想知道是否值得为面向对象编程和垃圾收集提供一点硬件。例如,由于很少有程序需要接近 40 亿个不同的对象,它们也不需要超过 4 个 gig 的单个对象,具有 32 位对象引用,并且结合对象引用和偏移量的 64 位指针类型可以提供比使用 64 位对象引用)。此外,如果在复制引用时可以廉价地标记对象......
  • ...并发 GC 可以避免“停止世界”的暂停。这种标记可能需要一个具有某种“不寻常”语义的单独缓存(如果两个内核几乎同时尝试标记同一个对象,则两个线程都不需要知道另一个线程“首先”标记了该对象 - 不像CompareExchange 场景——但有必要将对象识别为已标记)。不完全是正常意义上的 RISCy 概念,但仍然是其他 RISCy 指令无法做好的一种有用的操作。
  • 现代英特尔已经将 dec-and-branch 宏融合到一个 uop 中,所以这不是原因。不过,可能需要很多晶体管才能制作出不更新标志的版本。有趣的事实:AMD Bulldozer/Ryzen 确实有高效的loop,即使编译器从不使用它。查看链接的副本。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-01
  • 2015-03-15
  • 2018-01-20
  • 2013-05-03
  • 2021-09-03
  • 1970-01-01
相关资源
最近更新 更多