为什么 x[i]=if 语句比 if... x[i]= 语句更快?

4

我对此感到困惑/好奇,为什么这段代码

[查看汇编代码]

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

这段代码有什么可以优化的地方吗?

[查看汇编代码]

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]);
    }
}

[查看汇编代码]

我明白它们之间的区别。第一个是将x[i]设置为条件,而中间的是有条件地设置x[i]。虽然两者都有条件,但两者都有分支吗?是因为后面的扩展if语句被优化为向量指令max,而前者由于某些原因没有被识别为最大值函数吗?

gcc 10.3 x86_64 -Ofast -march=native


迭代了多少次 - 运行了多长时间?数组内容始终相同吗?它必须至少持续1秒才有用。 - Weather Vane
2
看起来这取决于目标 CPU。你使用了 -march=native,这是用于托管 godbolt.org 的任意随机 CPU。 - MSalters
1
在第二个变体中,编译器可能没有将源代码识别为最大计算的实现,或者它试图更紧密地遵循源代码。在第一个和第三个变体中,两个分支都有赋值语句,在第二个变体中,只有一个分支有赋值语句。 - Bodo
2
我怀疑编译器不愿意在你没有要求存储时生成存储。例如,假设x数组在只读内存中,但你知道它的条目都大于y中的条目。第二个代码,严格来说,应该可以工作(并且什么也不做),因为测试从未为真,并且x[i]从未被分配。但是第一个或第三个版本会崩溃。因此,即使编译器知道#1更快,它也不应将#2优化为#1的等效形式。 - Nate Eldredge
3
是的,确切地说,C编译器不能发明写操作,这是为了保证线程安全。因此,即使检查对齐向量中至少有一个元素被更新也不能使存储它们全部变得可行。(在链接中提到)能否使用SIMD指令进行替换?同时请参阅ICC错误:用icc编译会崩溃:编译器是否会在抽象机器中不存在写操作时发明写操作? - Peter Cordes
显示剩余5条评论
1个回答

5

从生成的汇编代码中可以清楚地看出。在第一种情况下,您会得到:

# 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

如您在第一个代码片段中所见,编译器使用了VMAXPD专用指令来计算两个双精度浮点数的最大值,而不需要分支。然而,在第二个代码片段中,有一个比较操作(VCMPPD),随后是一个测试操作(KORTESTB)和一个分支(JE)。


有趣的是编译器没有理解那个。Clang为所有三个生成不同的代码,其中最后一个(函数版本)是最优化的。 - Alasdair
@klutt 但这就是优化的意义,提高。否则它们会被称为“最优者”:D - Shark
1
@tstanisl 不,那与此无关,那里的问题在于数据,而不是分支,分支始终存在,代码始终相同。而在这里,问题是在一个情况下分支存在,在另一个情况下则不存在,代码也不同。不要混淆两者! - Marco Bonelli
@MarcoBonelli:这并不是那么简单!两种方法都是矢量化的(如果在Godbolt服务器上使用-march=native,则使用AVX512,但在OP的实际机器上可能完全不同);分支仅在整个向量中没有要存储的元素时才分支。请注意,掩码存储到[rdi+rax]{k1}。(可能是因为使用全零掩码进行存储可能很慢,特别是在写时复制的情况下,它可能需要微码协助来确保实际上不会污染页面。) - Peter Cordes
3
@Alasdair:它无法仅优化为max+向量存储,因为它不被允许发明写操作。使用icc编译器崩溃:编译器是否可以在抽象机器中不存在写操作的情况下发明写操作?AVX-512和分支 - Peter Cordes
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接