コンピュータでおかしなことになる計算例 (3) ―たった1度の型変換―
小清水 です。(@curekoshimizu)
今回は C言語のsin関数 で遊んでみましょう。
最後のオチまで気づかない人がいるかも!?
というわけで、
あーなるほどねと思っても読み進めて欲しいです!
今回紹介する例
- sinf は 単精度用 (float) の sin関数
- sin は倍精度用 (double) の sin関数
#include <stdio.h> #include <math.h> int main(int argc, char** argv) { double x = 1.0e30; printf("%.10lf\n", sinf(x)); printf("%.10lf\n", sin(x)); return 0; }
出力結果
-0.7911634445 0.0093314689
そう。全然結果が違う のです!
これは sin と sinf の違いによるものなのでしょうか?
すなわち、 倍精度のsin関数 と 単精度のsin関数 の
実装の問題 なのでしょうか。
それは違います。
sinf は 単精度(float) への入力のみ受け付けることから、
暗黙の型変換 による 丸め誤差が1度 発生します。
検証プログラム
倍精度用 (double) 側にも 単精度(float) への型変換を加えて確認してみましょう。
#include <stdio.h> #include <math.h> int main(int argc, char** argv) { double x = 1.0e30; printf("%.10lf\n", sinf(x)); printf("%.10lf\n", sin((float)x)); // <--- sin(x) から sin((float)x)) にした return 0; }
出力結果
-0.7911634445 -0.7911634385
ほぼほぼ近いです。先程出力された 0.0093314689 とは全然違います。
このことから sin と sinf に大きな違いがあったわけではありません。
違いがあったのは、
倍精度から単精度へのたった一度の丸め誤差が入ったかどうか です。
printf("%.10lf\n", sinf(x)); // <--- x の float への暗黙の型変換
よくよく考えると当たり前だが少し考えさせられる
実はこのプログラム
とういとてつもなく大きな値からスタートしています。
最終的に単精度(2進24桁)へ丸めが入りますので、
ここで説明したルールの RN(x) が入ります。
これにより丸められた値は
です。
この値で近似されるので
その誤差なんと
です。
そのため、ここまで大きな誤差が生じる例となっていた ということになります。
なので 当たり前といえば当たり前 の例かもしれません。
しかしながら、この短いプログラムでそんなことに気がつきましたでしょうか。
桁数が大きい場合の型変換は思っているより近くない
ため、プログラムによっては大ダメージになる ということに注意が必要です。
プログラミングをしているときに大きな桁になっているかも…ということに
気づくことはそうそうできないため、
少し考えさせられる例かもしれません。
最後のオチ
の sin計算を 単精度で計算したのが間違い!
の sin計算 は倍精度側で出力された値の
0.0093314689...
と捉えた人は間違いですよ。
の sin計算 は倍精度側で出力された値の
0.0093314689... ではありません!
倍精度大好きっ子はよくこの結論に達しがちなのですが、
double x = 1.0e30; // <--- 実数から浮動小数点数への丸め
ここも暗黙の型変換があります!
さっきと同じ話で こんな大きな数字の倍精度誤差も、単精度同様 尋常 じゃありません。
の sin計算 が 0.0093314689... というのはあってます!
これがタイトルの「たった一度の型変換」の意味になります。
ちなみに、 の sin計算結果は 負の数になりますので、全然違いますね。
勘違いしてしまった人は
ここを読んで是非丸め誤差の勉強を一緒にしましょう!
そんな、当たり前のようで少し勘違いしそうな例を思いついた、 昼のひとときでした。