【发布时间】: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进行全面优化(使用自动矢量化应该会有所帮助)? (是的,当然-O0is terrible, it's for fully consistent debugging,对性能毫无用处。)顺便说一句,gcc -O3 -funroll-loops甚至可以为您展开循环。 (也作为-fprofile-use的一部分启用)。 -
我不建议展开每个循环,因为它可能会不必要地污染 I-Cache,而是仅在需要时使用
#pragma GCC unroll <n>。在为 ARM 编译时始终使用最新的 GCC,因为在过去几年中已经投入了大量精力来优化 ARM 代码生成并正确指定架构(例如-O3 -march=armv8-a+simd+sb+predres)
标签: assembly arm loop-unrolling