【问题标题】:Assembler task : translations汇编任务:翻译
【发布时间】:2018-05-14 08:12:12
【问题描述】:

为什么.L21(比.L27长)比.L27快?

为什么标志 -funroll-loops 加速 loop1 但不加速 loop2?

【问题讨论】:

  • 因为第一种情况没有适当优化...可以缩短为return a[n-1] * x * x * x * x;,丢弃整个循环。 =我的意思是,您的问题毫无意义,编译器首先必须生成正确的代码,是否接近最佳解决方案是次要的,取决于源有多大,编写得如何,使用了哪些选项和编译器,等等......你甚至没有指定使用了哪个编译器和哪些选项,所以完全不可能说出为什么会出现特定的机器代码,但即使有细节也很难。
  • 也许我对缩短的理解是错误的,是吗?必须检查一下,gcc 7.3 没有找到它,我想这是我这边的错误,而不是这么早成为那个超人......哦,clang 找到了,所以它真的只是 gcc 未能识别简化的公式。有趣的。 godbolt.org/g/p2MGHs(我没有提到n的测试当然是需要的,对于(n <= 0)完全跳过for)。
  • 什么硬件有速度差异? IvyBridge 和更高版本应该能够以每时钟 1 个 imul 运行 loop1 的汇总版本(感谢 mov-elimination 的 3 个 ALU 微指令),所以最近的硬件 -funroll-loops 只有在有帮助的情况下才有帮助gcc 省略无用的imul 指令以进行除最后一个以外的循环迭代,或完全优化循环。
  • 是的,-funroll-loops 只是帮助 gcc 解决错过的优化问题。虽然这段代码似乎来自一个古老的 gcc 版本,比如 gcc4.1 godbolt.org/g/eDe8FX。甚至 gcc4.9 也将imul 排除在循环之外,但即使 gcc8.1 仍然会执行所有加载。 (不过,使用 SSE2 或 AVX2 进行自动矢量化!)

标签: assembly


【解决方案1】:

无论如何,关于你的问题......第一个循环在循环内部没有依赖关系,即循环的每次迭代都是独立的并且可以尽快计算(实际上除了最后一次迭代之外的所有迭代都可以被丢弃,因为它们完全不影响返回值)。

每次迭代的第二个循环取决于前一个结果,因此 CPU 必须等待每个下一个 imul 直到前一个结果准备好。我猜现代 x86 上的 imul 仍然具有大约 1.0 的吞吐量,但延迟可能高于 1.0 并且不确定依赖项会做什么,这完全取决于您没有指定的目标 CPU 平台。 (像 Peter Cordes 这样的人肯定可以为特定的现代英特尔微架构回答这个问题,或者您可以自己阅读 Agner 的表格,但由于您没有指定目标架构,因此我认为制作任何特定的现实世界示例对我来说没有意义这个一般的聊天级别就够了)

例如在 80386 上,我猜第二个循环会更快,因为它的指令更少,而且 80386 内部仍然非常“简单”,imul 在任何一种情况下都需要几个时钟。在最新的 Intel CPU 上,这种依赖性可能会偏向于第一个,但不会太大,因为 imul 在今天相当快。

无论如何,这是一个很好的例子,如何首先整理算法并对其进行调整,这将为您带来最大的性能提升,因为第一个循环不是真正的循环,将其编写为简单的公式将使代码更快.

奇怪的是,我在 Godbolt explorer 中尝试过,现代编译器对此做了什么,而 gcc 做了一些相当复杂的事情来读取每个数组成员,或者那道指令墙到底做了什么(懒得检查调试器) ,而 clang 编译器确实看穿了它并生成了简化的公式:https://godbolt.org/g/p2MGHs

附:第一个循环可以简化为:

int loop1_fix(int *a, int x, int n) {
    if (0 < n) return a[n-1]*x*x*x*x;
    else return x*x*x;
}

【讨论】:

  • 根据agner.org/optimizeimul r32, r/m32 是 3c 延迟,Intel 上的 1c 吞吐量可以追溯到 Core2 和 Ryzen。 (因此对于大 n,依赖循环比独立循环慢约 3 倍)。在 Ryzen 之前的 AMD 上,延迟为 4c,吞吐量为 2c,吞吐量与延迟瓶颈之间的差异仅为 2 倍。
猜你喜欢
  • 1970-01-01
  • 2015-01-13
  • 1970-01-01
  • 2012-10-17
  • 2016-02-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-05
相关资源
最近更新 更多