std::min の引数の順序により、浮動小数点のコンパイラ出力が変わります。質問する

std::min の引数の順序により、浮動小数点のコンパイラ出力が変わります。質問する

コンパイラ エクスプローラーをいじっていたところ、std::min に渡される引数の順序によって、出力されるアセンブリが変わることがわかりました。

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 が 2 番目の引数である場合は、値を xmm0 に入れるために が必要ですmovapd xmm0, xmm1。(編集者注: はい、x86-64 システムVFP 引数を xmm0、xmm1 などに渡し、xmm0 で返します。

私の質問は、なぜコンパイラは引数の順序自体を切り替えないのでしょうかmovapd。そうすれば、この必要がなくなるのでしょうか。minsd への引数の順序によって答えが変わらないことは、コンパイラは確実に知っているはずです。私が認識していない副作用があるのでしょうか。

ベストアンサー1

minsd a,bは、いくつかの特殊なFP値に対しては可換ではないし、std::minただし、 を使用する場合は除きます-ffast-math

minsd a,b その通りは、厳密な IEEE-754 セマンティクスで符号付きゼロと NaN に関するすべての意味を含む実装(a<b) ? a : bです。(つまり、ソース オペランド をb順序なしの1または等しい値に保持します)。Artyer が指摘しているように、-0.0と は+0.0等しい (つまり、-0. < 0.は false) と比較されますが、それらは異なります。

std::min比較式(a<b)cppref)、(a<b) ? a : b可能な実装としては、std::fminこれにより、どちらのオペランドからも NaN が伝播することが保証されます。(fmin元々は C++ テンプレートではなく、C 数学ライブラリから取得されました。)

見るx86 で分岐のない FP の最小値と最大値を与える命令は何ですか?minss/minsd / maxss/maxsd (および一部の GCC バージョンを除いて同じ非可換ルールに従う対応する組み込み関数) の詳細については、こちらをご覧ください。

脚注 1: はNaN<b任意の に対して偽でbあり、任意の比較述語に対して偽であることを覚えておいてください。たとえば、NaN == bは偽であり、 も同様ですNaN > b。 もNaN == NaN偽です。ペアの 1 つ以上が NaN の場合、それらは互いに対して「順序付けられていません」。

1つの b a < b std::min(a,b)=minsd a,b
非N 非N 偽(順序なし) a(非数)
非N 非NaN 偽(順序なし) a(非数)
非NaN 非N 偽(順序なし) a(非NaN); fminNaNを返す
非NaN 非NaN 価値観によって異なる aまたはb、-Infに近いほど
-0.0 +0.0 偽(等しい) a(-0.0)
+0.0 -0.0 偽(等しい) a(+0.0)

+-Infinity は有限値との比較では正常に機能するため、「有限」ではなく「非 NaN」と記述する必要があることに注意してください。


-ffast-math(コンパイラにNaNやその他の仮定や近似値がないことを仮定するように指示する)と、コンパイラは意思いずれかの関数を 1 つに最適化しますminsdhttps://godbolt.org/z/a7oK91

GCCについては、https://gcc.gnu.org/wiki/FloatingPointMath
-ffast-mathclang は、 catch-all を含む同様のオプションをサポートしています。

これらのオプションのいくつかは、奇妙なレガシーコードベースを除いて、ほぼすべてのユーザーが有効にする必要があります-fno-math-errno。(推奨される数学の最適化の詳細については、このQ&Aをご覧ください。)。そして、gcc は-fno-trapping-mathデフォルトでオンになっているにもかかわらず、完全には機能しないので良いアイデアです (一部の最適化では、例外がマスクされていない場合に発生する FP 例外の数を変更でき、1 から 0 または 0 から 0 以外になることもあります、IIRC)。gcc -ftrapping-mathまた、例外セマンティクスに関しても 100% 安全な最適化もいくつかブロックするため、かなり悪いです。 を使用しないコードではfenv.h、違いがわかりません。

しかし、std::min可換として扱うには、NaN がないことを前提とするオプションなどを使用してのみ実行できるため、決して「安全」とは言えません。NaN で何が起こるかを正確に考慮するコード用。例: -ffinite-math-onlyNaN (および無限大) がないと仮定します。

clang -funsafe-math-optimizations -ffinite-math-only必要な最適化を実行します。(unsafe-math-optimizations は、符号付きゼロのセマンティクスを考慮しないなど、より具体的なオプションを多数意味します)。

おすすめ記事