【问题标题】:Argument order to std::min changes compiler output for floating-pointstd::min 的参数顺序更改浮点的编译器输出
【发布时间】:2021-01-12 21:00:29
【问题描述】:

我在编译器资源管理器中摆弄,我发现传递给 std::min 的参数顺序改变了发出的程序集。

Here's the example on Godbolt Compiler Explorer

double std_min_xy(double x, double y) {
    return std::min(x, y);
}

double std_min_yx(double x, double y) {
    return std::min(y, x);
}

这被编译(例如,在 clang 9.0.0 上使用 -O3),以:

std_min_xy(double, double):                       # @std_min_xy(double, double)
        minsd   xmm1, xmm0
        movapd  xmm0, xmm1
        ret
std_min_yx(double, double):                       # @std_min_yx(double, double)
        minsd   xmm0, xmm1
        ret

如果我将 std::min 更改为老式三元运算符,这种情况仍然存在。它还存在于我尝试过的所有现代编译器(clang、gcc、icc)中。

底层指令是minsd。阅读文档,minsd 的第一个参数也是答案的目的地。显然 xmm0 是我的函数应该放置其返回值的位置,因此如果 xmm0 用作第一个参数,则不需要 movapd。但如果 xmm0 是第二个参数,那么它必须 movapd xmm0, xmm1 才能将值放入 xmm0。 (编者按:是的,x86-64 System V 在 xmm0、xmm1 等中传递 FP args,并在 xmm0 中返回。)

我的问题:为什么编译器不自己切换参数的顺序,这样movapd 就没有必要了?它肯定必须知道 minsd 的参数顺序不会改变答案?是否有一些我不理解的副作用?

【问题讨论】:

  • 可能仅仅是因为在极少数情况下保存单个寄存器交换是不值得的
  • @AlanBirtles 我希望编写编译器优化的人不是这样想的。这让我很痛苦。我试图让自己不在乎,因为这在我的上下文中无关紧要,但仍然很痛。
  • @AlanBirtles:Rave 是正确的,这不是编译器开发人员的想法。如果这真的是一个错过的优化(而不是严格的 FP 语义所要求的),gcc 和 clang 开发人员可能会喜欢提交一个错过的优化错误。 (虽然它可能已经是一个已知的错误;通过调用约定要求放置的硬寄存器约束有时会在 gcc 进行寄存器分配时导致浪费 mov 或 movaps 指令,您不会在中间看到内联后更大的函数。)
  • @bolov 我的意思是编译器可以切换minsd 操作数的顺序来保存movapd。 (但是,据我所知,它不能那样做。)
  • 为什么这个问题要加android标签?

标签: android c++ assembly x86 floating-point


【解决方案1】:

minsd a,b 对于某些特殊的 FP 值是不可交换的,std::min 也不是,除非你使用 -ffast-math。

minsd a,b 完全 实现了(a<b) ? a : b,包括在严格的 IEEE-754 语义中暗示有符号零和 NaN 的所有内容。 (即,它将源操作数b 保持在无序1 或相等)。正如 Artyer 指出的那样,-0.0 和 +0.0 比较相等(即 -0. < 0. 为假),但它们是不同的。

std::min 是根据 (a<b) 比较表达式 (cppreference) 定义的,(a<b) ? a : b 是一种可能的实现,这与 std::fmin 不同,它保证从任一操作数传播 NaN 等等。 (fmin 最初来自 C 数学库,而不是 C++ 模板。)

有关 minss/minsd / maxss/maxsd(以及相应的内在函数,它们遵循相同的非交换规则,但在某些 GCC 版本中除外)的更多详细信息,请参阅 What is the instruction that gives branchless FP min and max on x86?。

脚注 1:请记住,NaN<b 对于任何 b 和任何比较谓词都是错误的。例如NaN == b 是假的,NaN > b 也是如此。甚至NaN == NaN 也是错误的。当一对中的一个或多个是 NaN 时,它们是“无序的”wrt。彼此。


使用-ffast-math(告诉编译器假设没有NaN,以及其他假设和近似值),编译器将任一函数优化为单个minsd。 https://godbolt.org/z/a7oK91

对于 GCC,请参阅https://gcc.gnu.org/wiki/FloatingPointMath
clang 支持类似的选项,包括 -ffast-math 作为一个包罗万象的选项。

几乎每个人都应该启用其中一些选项,除了奇怪的遗留代码库,例如-fno-math-errno。 (见this Q&A for more about recommended math optimizations)。 gcc -fno-trapping-math 是一个好主意,因为它无论如何都不能完全工作,尽管默认情况下是打开的(一些优化仍然可以改变如果异常被取消屏蔽会引发的 FP 异常的数量,包括有时甚至从 1 到 0或 0 到非零,IIRC)。 gcc -ftrapping-math 还阻止了一些即使是 100% 安全的优化。异常语义,所以它非常糟糕。在不使用fenv.h 的代码中,您永远不会知道其中的区别。

但将 std::min 视为可交换只能通过假定没有 NaN 的选项以及类似的东西来完成,因此对于关心究竟是什么的代码绝对不能称为“安全”发生在 NaN 上。例如-ffinite-math-only 假定没有 NaN(也没有无穷大)

clang -funsafe-math-optimizations -ffinite-math-only 将执行您正在寻找的优化。 (不安全数学优化意味着一堆更具体的选项,包括不关心有符号零语义)。

【讨论】:

  • -ffast-math 忽略的一些细节并不是那么微妙。我对它优化 isnan() 到 false 感到非常惊讶:godbolt.org/z/zs31Yn
  • @jpa:是的,“微妙”不是一个很好的描述,除了 NaN 是在正常条件下不会发生并且您不想处理它的代码。编辑得更具体。
  • 请注意,movapd 的需要是用-mavx 选项固定的(假设目标 CPU 支持 AVX),因为 AVX 增加了指令的非破坏性源(3 操作数)编码。
  • @Ruslan:是的。但是,您仍然可以发明需要额外指令的情况,例如其中一个操作数是像x = std::min(array[i], x) 这样的内存。甚至 AVX 也需要在循环中单独加载而不是内存源操作数,因为只有第二个源可以是内存。 (并且它不能在末尾使用水平最小值自动矢量化:std::min 也不与 FP 关联)。
【解决方案2】:

考虑:std::signbit(std::min(+0.0, -0.0)) == false && std::signbit(std::min(-0.0, +0.0)) == true。

唯一的其他区别是如果两个参数都是(可能不同的)NaN,则应返回第二个参数。


您可以允许 gcc 使用 -funsafe-math-optimizations -fno-math-errno 优化(均由 -ffast-math 启用)重新排序参数。 unsafe-math-optimizations 允许编译器不关心有符号零,finite-math-only 不关心 NaN

【讨论】:

  • -fno-math-errno 在这里应该无关紧要; std::min 是一个 C++ STL 模板函数,不是一个 C 数学库函数,它在历史上具有像 fmin 或 sqrt 这样的 NaN 语义的错误设置。不过,-fno-math-errno 始终是一个好主意,除非在历史代码库中检查 errno 而不是使用 fenv.h。
  • 应该是,但显然不是,使用 clang9.0。 godbolt.org/z/4bdvMa 表明 -funsafe-math-optimizations 单独不会这样做,但添加 -fno-math-errno 确实将其优化为可交换的。这可能是一个 clang9 错误,也许还暗示不安全数学不包括的非 NaN 假设?使用 clang 10.0 和 GCC,unsafe-math + no-math-errno 仍然保留了操作数顺序的差异:fast-math 的其他一些部分产生了差异。
【解决方案3】:

扩展现有答案,即 std::min 不可交换:这是一个具体示例,可以可靠地区分 std_min_xy 和 std_min_yx。 Godbolt:

bool distinguish1() {
    return 1 / std_min_xy(0.0, -0.0) > 0.0;
}
bool distinguish2() {
    return 1 / std_min_yx(0.0, -0.0) > 0.0;
}

distinguish1() 计算为1 / 0.0 > 0.0,即INFTY > 0.0,或true。
distinguish2() 计算为1 / -0.0 > 0.0,即-INFTY > 0.0,或false。
(当然,所有这些都在 IEEE 规则下。我不认为 C++ 标准强制编译器保留这种特殊行为。老实说,我很惊讶表达式 -0.0 实际上在第一名!

-ffinite-math-only eliminates this way of telling the difference 和 -ffinite-math-only -funsafe-math-optimizations completely eliminates the difference in codegen。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-25
    • 1970-01-01
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多