ユーザー
yruslanは、浮動小数点演算に関する優れた記事
「浮動小数点演算について知っておくべきこと」を公開しました。
いくつかの有益な例を追加したいと思います。 私が説明する状況は、私の実践の中で何度か出会いました。 生成されたエラーは非常にまれで、再現するのが難しく、見つけるのが困難でした。
おそらく私は今、よくある真実を語りますが、IEEE754規格で記述されている数字が実際の数字と同じではないことを人々が忘れる頻度は驚くべきことです。
部門
戻り番号を取得する前に、除数をゼロでチェックするとよいでしょう。 一部のプログラマは、これを大まかに次のように実行します。
if (d != 0.f) result = 1.f / d;
非正規化された数値を完全に忘れます。
以下に小さな例を示します。
float var = 1.f; for (int i = 0; i < 50; i++) { var /= 10.f; float result = 1.f / var; printf("1 / %e = %e\n", var, result); }
そして、これが彼の仕事の結果です。
...
1 / 9.999999e-035 = 1.000000e+034
1 / 9.999999e-036 = 1.000000e+035
1 / 9.999999e-037 = 1.000000e+036
1 / 1.000000e-037 = 1.000000e+037
1 / 9.999999e-039 = 1.000000e+038
1 / 1.000000e-039 = 1.#INF00e+000 << —
1 / 9.999946e-041 = 1.#INF00e+000
1 / 9.999666e-042 = 1.#INF00e+000
1 / 1.000527e-042 = 1.#INF00e+000
1 / 9.949219e-044 = 1.#INF00e+000
1 / 9.809089e-045 = 1.#INF00e+000
1 / 1.401298e-045 = 1.#INF00e+000
...
除数の絶対値を定数FLT_MINと比較することにより、無限を回避できます。
関数定義ドメインを超えて
たとえば、区間[-1;の値をとる逆余弦関数acos()を考えます。 1]。 次に、2つのベクトル間の角度を取得するために使用します。 これを行うには、正規化されたベクトルのスカラー積をacos()に渡し、出力で角度をラジアンで取得する必要があります。 すべてがシンプルです。 そこで、コードを記述します。
float x1 = 10.f;
次に、このコードがほぼ平行なベクトルでどのように動作するかを確認しましょう。
for (int i = 0; i < 100; i++) { float x1 = 10.f; float y1 = 5.e-3f * (rand() % 10000) / 10000;
...
dotProduct = 1.00000000 acos(dotProduct) = 0.000000e+000
dotProduct = 1.00000000 acos(dotProduct) = 0.000000e+000
dotProduct = 0.99999994 acos(dotProduct) = 3.452670e-004
dotProduct = 1.00000012 acos(dotProduct) = -1.#IND00e+000 << NaN
dotProduct = 1.00000000 acos(dotProduct) = 0.000000e+000
dotProduct = 1.00000012 acos(dotProduct) = -1.#IND00e+000 << NaN
...
この合成例では、100回の反復で、NaNを含む9つの角度値を取得しました。 実際のプロジェクトでは、このエラーは非常にまれにしか発生しません。
この場合、acos(dotProduct)をより安全なacosの呼び出し(clamp(dotProduct、-1.f、1.f))に置き換える必要があります。
おわりに
計算されたパラメーターが渡されるコード内の分割、または次の関数:asin、acos、sqrt、fmod、log、log10、tanが表示される場合、パラメーターが定義ドメインの境界内に収まるかどうかを検討しますか? もしそうなら、いつか彼が国境を越えて行くことを確認してください。