【问题标题】:Why is x[i]=if faster than if... x[i]=为什么 x[i]=if 比 if... x[i]= 快
【发布时间】:2021-07-11 09:25:07
【问题描述】:

这让我感到困惑/好奇,为什么是这个代码

[see assembly]

void maxArray(double* x, double* y) {
    for (int i = 0; i < 65536; i++) {
        x[i] = ((y[i] > x[i]) ? y[i] : x[i]);
    }
}

...比这段代码更快?

[see assembly]

void maxArray(double* x, double* y) {
    for (int i = 0; i < 65536; i++) {
        if (y[i] > x[i]) x[i] = y[i];
    }
}

并且记录在案,第一个生成的程序集与扩展版本相同:

inline double fn(double a, double b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}
void maxArray(double* x, double* y) {
    for (int i = 0; i < 65536; i++) {
        x[i] = fn(y[i], x[i]);
    }
}

[see assembly]

我明白了。第一个是将x[i]设置为条件,中间是有条件地设置x[i]。两者都有条件,所以都有分支?是不是因为后者的扩展 if 语句被优化为向量程序集max 命令,而前者由于某种原因未被识别为 max 函数?

gcc 10.3 x86_64 -Ofast -march=native

【问题讨论】:

  • 如果您点击任何链接,您可以看到确切的编译器和标志。
  • 看起来这严重依赖于目标 CPU。您使用了-march=native,这是用于托管 godbolt.org 的随机 CPU
  • 在第二个变体中,编译器要么没有将源代码识别为最大计算的实现,要么它试图更接近源代码。在第一个和第三个变体中,您在两个分支中都有分配,在第二个变体中,您仅在一个分支中进行分配。
  • 我怀疑编译器不愿意在您不要求的情况下生成存储。例如,假设x 数组在只读内存中,但您知道它的条目都大于y 中的条目。严格阅读,第二个代码应该可以工作(并且什么也不做),因为测试永远不会为真,并且永远不会分配 x[i]。但是第一个或第三个版本会崩溃。因此,编译器不应将 #2 优化为与 #1 等效的内容,即使它知道 #1 会更快。
  • @NateEldredge:是的,确切地说,C 编译器不得发明写入,如果没有别的,为了线程安全。 (因此,即使在对齐的向量中检查至少一个要更新的元素也无法将它们全部存储起来)。 Is it possible to use SIMD instruction for replace?,另请参阅此 ICC 错误:Crash with icc: can the compiler invent writes where none existed in the abstract machine?

标签: c gcc x86 x86-64 compiler-optimization


【解决方案1】:

从生成的汇编代码中应该很清楚。在第一种情况下,您会得到:

# x[i] = ((y[i] > x[i]) ? y[i] : x[i])

vmovupd ymm1, YMMWORD PTR [rsi+rax
vmaxpd  ymm0, ymm1, YMMWORD PTR [rdi+rax]
vmovupd YMMWORD PTR [rdi+rax], ymm0

而在第二种情况下,您会得到:

# if (y[i] > x[i]) x[i] = y[i];

vmovupd  ymm0, YMMWORD PTR [rsi+rax]
vcmppd   k1, ymm0, YMMWORD PTR [rdi+rax], 14
kortestb k1, k1
je       .L3
vmovupd  YMMWORD PTR [rdi+rax]{k1}, ymm0

正如您在第一个 sn-p 中所见,编译器使用 VMAXPD 专用指令来计算两个双精度浮点值的最大值,而无需分支。不过,在第二个 sn-p 中,有一个比较 (VCMPPD),然后是一个测试 (KORTESTB) 和一个分支 (JE)。

【讨论】:

  • 有趣的是编译器没有得到那个。 Clang 为所有三个生成不同的代码,最后一个(函数版本)是最优化的。
  • @Alasdair 是的,当您处于这种优化级别时,很难获得正确的代码,即使是同一编译器的不同版本也可能会更改生成的代码。
  • @Alasdair 优化器的更正确名称是“改进者”。他们没有找到最佳解决方案。
  • @tstanisl 不,这无关紧要,问题在于数据,而不是始终存在的分支,代码始终相同。相反,这里的问题是,在一种情况下分支存在,在另一种情况下不存在,代码不同。不要混淆两者!
  • @Alasdair:它不能优化为仅max + 矢量存储,因为它不允许发明写入。 Crash with icc: can the compiler invent writes where none existed in the abstract machine? 和 AVX-512 and Branching
猜你喜欢
  • 2012-08-25
  • 1970-01-01
  • 2013-02-01
  • 2019-04-26
  • 2021-09-21
  • 1970-01-01
  • 2020-03-28
  • 2020-05-24
相关资源
最近更新 更多