double, 100で割ってから100倍する

[戻る]
pen 2007/12/11(火) 20:37:18
100で割ってから100倍するプログラムを以下のように書きました。

#include <iostream>
using namespace std;

int main() {
  double b = 2 / 100.0;
  double c = 2;
  double d = b * 100.0;

  cout << (c == d) << endl;
  cout << (b * 100.0 == c) << endl;
  cout << b << endl;
  cout << b * 100.0 << endl;
  cout << c << endl;
  cout << d << endl;
}

実行結果は次のようになります。

1
0
0.02
2
2
2

疑問: c == dがtrueなのに、b * 100.0 == cがfalseになる理由がわかりません。
いったん、double型の変数 d に値を格納するのとしないのとでは、何が違うのでしょうか?

maru 2007/12/11(火) 21:08:08
> 疑問: c == dがtrueなのに、b * 100.0 == cがfalseになる理由がわかりません。
桁落ち、丸め誤差が発生しているからでしょう。

double b = 2 / 100.0;
は正確に0.02ではなく近似値になっています。また
double d = b * 100.0;
は正確にはb * 100.0ではなく、丸められて2になっていると考えられます。
# 正確には検証していない。

浮動少数点数(float, double)では一致判定を==で行うことは一般ではありません。
数学的に一致するはず、というという思い込みは通じません。

wclrp ( 'o') 2007/12/11(火) 21:11:34
推測
コンパイラが最適化しているのかもね。
CPUやFPU等が浮動小数点を計算するのと
double型が完全一致しなければいけない規則じゃないし。

wclrp ( 'o') 2007/12/11(火) 21:20:22
こういう場合俺は最適化できないように

double test();

int main() {
  double b = 2 / 100.0;
  double c = 2;
  double d = b * 100.0;
  double e = test(b);

  cout << (c == d) << endl;
  cout << (c == e) << endl;
  cout << (b * 100.0 == c) << endl;
  cout << b << endl;
  cout << b * 100.0 << endl;
  cout << c << endl;
  cout << d << endl;
}

double test(double b)
{
  return b * 100.0;
}

これでも(c == e)が真なら・・・・なんだろ?
dとeのメモリの内容を見比べるとか。
cout << b;とかじゃ適当に四捨五入および10進数に変換した
ものであって真実の確認にならないからね。

pen 2007/12/11(火) 22:14:36
#include <iostream>
using namespace std;

double test(double b)
{
  return b * 100.0;
}

int main() {
  double b = 2 / 100.0;
  double c = 2;
  double e = test(b);

  cout << (c == e) << endl;
  cout << (c == test(b)) << endl;
}

上のコードを実行すると以下のようになりました:

1
0

うーむ、なんで結果が異なるのだろう…。

maru 2007/12/11(火) 22:35:51
実際にやってみた(最初のプログラム)。
結果は
1
1
0.02
2
2
2
環境はWindows XP Pro SP2 , Microsoft Visual Studio 2005
Version 8.0.50727.42  (RTM.050727-4200)
Microsoft Visual C++ 2005   77971-009-0000007-41987
Debug, Releaseともに同じ結果でした。

ついでに後のプログラム
1
1

何が違うんでしょうね。

とおりすがり 2007/12/11(火) 23:08:04
最適化設定や浮動小数点関連のオプション設定にもよるだろうし
コンパイラのバージョンにも影響受けるかも


某社の音声コーデックのリファレンス実装がオプションどういじっても
VC6とVC8でデコード結果一致しなかった…orz

wclrp ( 'o') 2007/12/11(火) 23:33:59
testを先に持ってきたか!

最適化で自動inline展開されちゃうとどうかと思ってね

wclrp ( 'o') 2007/12/11(火) 23:42:50
CPUやコンパイラによって違うから一概に言えないけど
VCのdoubleがIEEE 754の8バイト
浮動小数点は10バイトで計算しているらしい(違う場合もあるよ)
だからdoubleに一旦入れると精度落ちるよ。

pen 2007/12/12(水) 10:43:32
wclrp ( 'o')さん:

> testを先に持ってきたか!

> 最適化で自動inline展開されちゃうとどうかと思ってね

すみません、よく分からないのですが、testの定義のあとさきで、
コンパイル結果に違いがあるということですか? 
inline展開されるのは、inline宣言されたもののみが対象だと思っていたのですが…。

pen 2007/12/12(水) 10:45:26
maruさん:

> 環境はWindows XP Pro SP2 , Microsoft Visual Studio 2005
> Version 8.0.50727.42  (RTM.050727-4200)
> Microsoft Visual C++ 2005   77971-009-0000007-41987
> Debug, Releaseともに同じ結果でした。

ぼくの環境はg++ 3.4.4 cygwinです。(VC++の掲示板なのにすみません)

pen 2007/12/12(水) 10:53:17
wclrp ( 'o')さん:

>CPUやコンパイラによって違うから一概に言えないけど
>VCのdoubleがIEEE 754の8バイト
>浮動小数点は10バイトで計算しているらしい(違う場合もあるよ)
>だからdoubleに一旦入れると精度落ちるよ。

だとしても、

double test(double b)
{
  return b * 100.0;
}

int main() {
  double b = 2 / 100.0;
  double c = 2;
  double e = test(b);

  cout << (c == e) << endl;
  cout << (c == test(b)) << endl;
}

の場合に c == eがtrueで、c == test(b)がfalseになるのがよく分かりませんね…。

YuO [E-Mail] 2007/12/12(水) 12:23:54
> の場合に c == eがtrueで、c == test(b)がfalseになるのがよく分かりませんね…。

コンパイル後のアセンブリコードを見ていないので推測ですが,

c == eではFPUから一旦主記憶上のeにデータが移動して,再度cとeの値をFPUに移動して
比較した。
eに代入する時に80ビット→64ビットの変換によって丸めが発生し,結果としてcとeの値
が一致した

c == test(b)では,test(b)の結果はFPUに残したままcの値をFPUに移動して比較した。
この時,test(b)の結果は64ビットに丸められたことがないので,cの値 (64ビットに丸め
られている) と異なった。

ということではないでしょうか。

ITO 2007/12/12(水) 13:58:21
>ぼくの環境はg++ 3.4.4 cygwinです。(VC++の掲示板なのにすみません)
gccになるとまた話が変わるよ、

>CPUやコンパイラによって違うから一概に言えないけど
>VCのdoubleがIEEE 754の8バイト
このはなしが変わってきます。

doubleを使う以上、値は変わってしまうと思います。
データはintで保存しておき、計算の都度doubleに戻す。
計算後は再びintにして保存する。

どうしても、intを使うのがいやなら、固定小数点を使うしかないと思います。

yoh2 [E-Mail] 2007/12/13(木) 00:08:59
同じコンパイラでも最適化オプションによって結果が変わることもありますね。
例えば、penさんの2つめのプログラムをgcc-4.1.2で (これまたVC++
から離れてしまってすみません)、最適化オプションを-O0(最適化なし)から、
-O3(強最適化)まで試してみたところ、以下のような結果になりました。

-O0: (c == e) → 1, (c == test(b)) → 0
-O1: (c == e) → 0, (c == test(b)) → 0
-O2: (c == e) → 0, (c == test(b)) → 0
-O3: (c == e) → 1, (c == test(b)) → 1

アセンブリ出力を見たところ、以下のような傾向になっていました。

-O0については、ソースの字面通りに実行しているようで、
結果が異なるのは多分YuOさんの考察通り。

-O1と-O2については、test(b)を一度だけ呼びだして
その結果を再利用。ただし丸められてcと一致せず。

-O3については、そもそもtest(b)の呼出はせず、
そもそも浮動小数点関連の計算すら一切行わずに直接1を
出力しています。
コンパイル時にすべての計算を終えてしまい、その際、
2 と 2 / 100.0 * 100.0 は同一だと判定したのでしょう。

整数演算では、最適化オプション次第で結果が変わってしまう
ことは考えられない(もしそのようなことがあったら
未定義、未規定の罠に引っ掛かったかコンパイラのバグ)
ですが、浮動小数点数演算については、このようなゆらぎが
規格上認められています。
なぜ結果が一定しないかを考えるのは楽しいですが、(実際
盛り上がりましたしね) 実用的には同値性を比較したい数同士が
厳密に一致することはまずあり得ないと思っておくのが無難かと。

以下はもうご存知かもしれませんが、一応書いておきます。

厳動小数点数の同値判定は、 == で直接比較せずに、2つの数の差の
絶対値を見ることが多いです。
例えば、

    if( x == y ) { ... }

と書く代わりに

    if( fabs(x - y) < 小さな数 ) { ... }

とします。

この「小さな数」は、浮動小数点数の精度や、比較するまでに行った
計算の性質を考慮して決定する必要があります。
あまりに小さ過ぎると、一致すべき値が一致せず、大き過ぎると
区別すべき値が同値と判定されてしまうので難しいところ。
このあたりについては、「数値計算」「誤差」あたりで
検索するといろいろと情報が見付かります。

pen 2007/12/13(木) 11:51:54
ありがとうございます。とても勉強になりました。

maru 2007/12/13(木) 12:54:21
> -O3については、そもそもtest(b)の呼出はせず、
> そもそも浮動小数点関連の計算すら一切行わずに直接1を
> 出力しています。
> コンパイル時にすべての計算を終えてしまい、その際、
> 2 と 2 / 100.0 * 100.0 は同一だと判定したのでしょう。
最近のコンパイラはそこまでやるか!

> なぜ結果が一定しないかを考えるのは楽しいですが、
御意。

2007/12/14(金) 01:50:12
完全に余談ですが

>最近のコンパイラはそこまでやるか!
なんかで、
int fac(int n){
  if(n<=1)
    return 1;
  else
    return fac(n-1)*n;
}
を作ってfac(5)とか書いたら最適化で全部展開されて定数になって出たって話も
聞いた覚えが…

最適化による計算内容の変動も面白いものがありますよね。

PATIO 2007/12/14(金) 10:13:29
>最近のコンパイラはそこまでやるか!

最近のコンパイラの最適化はかなり賢いので
人間がアセンブラで組むよりもよっぽど速かったりしますね。
昔のコンパイラだと高速化が必要とかだとアセンブラ必須でしたけど。
今のコンパイラだと相当の達人で無いと人間が負けるんじゃないかなぁ。

maru 2007/12/17(月) 10:56:37
余談は続く。

> 今のコンパイラだと相当の達人で無いと人間が負けるんじゃないかなぁ。
確かにそうですね。
インテルのコンパイラなんてHT用に命令の順序を変更するなんてことも聞いていますし。
この辺になるとCPUの内部アーキテクチャーが分からないとどうにもならない。

ところで、今回の件で、
>  double e = test(b);
>  cout << (c == e) << endl;
>  cout << (c == test(b)) << endl;
の e に volatile をつけるとコンパイルすると、どうなるんでしょう。
ちょっと興味があります。
# gcc は入っていないので...

ITO 2007/12/17(月) 12:07:19
余談です。

> 今のコンパイラだと相当の達人で無いと人間が負けるんじゃないかなぁ。
>> 確かにそうですね。
>> インテルのコンパイラなんてHT用に命令の順序を変更するなんてことも聞いています
>> し。
>> この辺になるとCPUの内部アーキテクチャーが分からないとどうにもならない。
日立系のCPU(H8,SH,SH2,SH3,SH4)はC言語が優勢ですね。
PIC/AVR関連も言語が使えるようになってます。
# 僕はPICのC言語にいまいち慣れてないです。
アセンブラは、単純動作でスピード重視の部分に限られるようになるのかも
知れませんね。

PATIO 2007/12/17(月) 14:45:24
> アセンブラは、単純動作でスピード重視の部分に限られるようになるのかも
> 知れませんね。

下手するとそういう領域もコンパイラが吐き出すコード十分と言う事にも
なりかねませんね。
こっちの方面の職人は段々需要が無くなっていくのかも。
コンパイラを書くような人たちは話が別なんでしょうけれど。

maru 2007/12/18(火) 11:17:12
さらに余談は続く。

> アセンブラは、単純動作でスピード重視の部分に限られるようになるのかも
> 知れませんね。
電力関係などの特に高信頼性が求められる分野では、組み込みソフトウェアのバグ修正の際に
新旧ROMの照合を行なって修正した部分以外に変更が無いことの確認が求められることがある
そうです。
このような場合、アセンブラでないとなかなか難しいのではないかとも思います。

古いスレッドには追加発言できません。