【问题标题】:Why loop unroll brings so much speedup on ARM Cortex-a53?为什么循环展开会在 ARM Cortex-a53 上带来如此多的加速?
【发布时间】:2020-12-26 01:03:08
【问题描述】:

我正在以 AArch64 状态运行的 ARM Cortex-a53 处理器上使用以下代码进行循环展开:

void do_something(uint16_t* a, uint16_t* b, uint16_t* c, size_t array_size)
{
  for (int i = 0; i < array_size; i++)
  {
    a[i] = a[i] + b[i];
    c[i] = a[i] * 2;
  }
}

使用标志 -O1,我得到了以下程序集,

.L3:
    ldrh    w3, [x0, x4]
    ldrh    w5, [x1, x4]
    add     w3, w3, w5
    and     w3, w3, 65535
    strh    w3, [x0, x4]
    ubfiz   w3, w3, 1, 15
    strh    w3, [x2, x4]
.LVL2:
    add x4, x4, 2
.LVL3:
    cmp x4, x6
    bne .L3

在 162ms 内完成(a、b、c 的大小都很大)。为简单起见,我在循环之前省略了一些 prolog 和 epilog 代码,但它们仅用于堆栈设置等。

然后我展开循环,得到如下代码:

void add1_opt1(uint16_t* a, uint16_t* b, uint16_t* c, size_t array_size)
{
  for (int i = 0; i < array_size/4; i+=4)
  {
    a[i]   = a[i] + b[i];
    c[i]   = a[i] * 2;
    a[i+1] = a[i+1] + b[i+1];
    c[i+1] = a[i+1] * 2;
    a[i+2] = a[i+2] + b[i+2];
    c[i+2] = a[i+2] * 2;
    a[i+3] = a[i+3] + b[i+3];
    c[i+3] = a[i+3] * 2;
  }
}

它给出如下汇编(仍然使用 -O1,因为使用 -O0 编译器正在做一些愚蠢的事情):

.L7:
    ldrh    w1, [x0]
    ldrh    w5, [x3]
    add w1, w1, w5
    and w1, w1, 65535
    strh    w1, [x0]
    ubfiz   w1, w1, 1, 15
    strh    w1, [x2]
    ldrh    w1, [x0, 2]
    ldrh    w5, [x3, 2]
    add w1, w1, w5
    and w1, w1, 65535
    strh    w1, [x0, 2]
    ubfiz   w1, w1, 1, 15
    strh    w1, [x2, 2]
    ldrh    w1, [x0, 4]
    ldrh    w5, [x3, 4]
    add w1, w1, w5
    and w1, w1, 65535
    strh    w1, [x0, 4]
    ubfiz   w1, w1, 1, 15
    strh    w1, [x2, 4]
    ldrh    w1, [x0, 6]
    ldrh    w5, [x3, 6]
    add w1, w1, w5
    and w1, w1, 65535
    strh    w1, [x0, 6]
    ubfiz   w1, w1, 1, 15
    strh    w1, [x2, 6]
.LVL8:
    add x4, x4, 4
.LVL9:
    add x0, x0, 8
    add x3, x3, 8
    add x2, x2, 8
    cmp x4, x6
    bcc .L7

这几乎就像复制和粘贴其他汇编代码 4 次。问题是,为什么这段代码只需要 28 毫秒就可以运行,这就像 5 倍的速度一样。像这样简单的循环条件,我认为分支预测应该在两个代码中都做得很好,对吧?在第二个汇编代码中,商店也是交错的。所以我无法想象这样的代码是如何获得如此快的加速的。

【问题讨论】:

  • 您如何衡量代码的性能?如果可能,请提供完整的基准线束。
  • 在对第一个代码进行计时后,我将函数调用替换为第二个代码并重建所有内容并再次运行。我认为不应该有任何热缓存问题?如果我颠倒运行顺序,结果是相似的。
  • 这仍然无法解释您如何准确地对这段代码进行基准测试。为了重现您的结果,我需要您的完整基准测试代码,以便我可以在自己的计算机上编译和运行它并运行分析程序。这称为制作minimal reproducible example。在您的下一个问题中记住这一点!
  • 为什么只使用-O1,而不是使用-O2 进行正常优化或使用-O3 进行全面优化(使用自动矢量化应该会有所帮助)? (是的,当然-O0 is terrible, it's for fully consistent debugging,对性能毫无用处。)顺便说一句,gcc -O3 -funroll-loops 甚至可以为您展开循环。 (也作为-fprofile-use 的一部分启用)。
  • 我不建议展开每个循环,因为它可能会不必要地污染 I-Cache,而是仅在需要时使用 #pragma GCC unroll &lt;n&gt;。在为 ARM 编译时始终使用最新的 GCC,因为在过去几年中已经投入了大量精力来优化 ARM 代码生成并正确指定架构(例如-O3 -march=armv8-a+simd+sb+predres

标签: assembly arm loop-unrolling


【解决方案1】:

问题出在这里:for (int i = 0; i &lt; array_size/4; i+=4)

循环到array_size/4 将完成四分之一的工作。

应该是for (int i = 0; i &lt; array_size; i+=4)

那么您应该会看到几个百分比的更易于解释的加速。

【讨论】:

  • 啊..你是对的。这是一个愚蠢的问题。再也不要在深夜为我写代码是我学到的教训。
  • @DaTeng:如果 ARM 有它们,硬件性能计数器(用于 L1d 缓存加载等事件)可以检查您的代码是否在执行相同数量的工作,如果您知道负载的范围有多大在不同版本的循环中。或者只计算分支指令;这会发现你做的分支减少了 16 倍,而不是 4 倍。
猜你喜欢
  • 1970-01-01
  • 2017-08-02
  • 2016-03-01
  • 1970-01-01
  • 2015-04-20
  • 2019-08-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-11
相关资源
最近更新 更多