float と 80bit FPU

Annotated C# Standard を読んでみたシリーズその 2。
同じ計算の結果なのに != になる、という話。

参考: 2007-06-03

何はともあれサンプル

public float f1 = 2.82323f;
public float f2 = 2.3f;
public float member;

public float Calc() {
	return this.f1 * this.f2;
}
public void FloatTest() {
	this.member = this.Calc();
	float local = this.Calc();

	Console.WriteLine(this.member == this.Calc());	// True
	Console.WriteLine(local == this.Calc());	// True
	Console.WriteLine((this.f1 * this.f2) == this.Calc());	// False
	Console.WriteLine((this.f1 * this.f2) == (this.f1 * this.f2));	// True
}

Debug ビルドして実行したところ、手持ちの .net framework 3.5 (x86*1 )、mono 1.2 (x64*2 )、mono 2.0 (x86*3 ) のどれでも↑の通りであった。

ちなみに、このサンプル自体、Annotated C# Standard を元に*4書いたものである。

浮動小数の奇妙な精度 または私は如何にして浮動小数を止めて有理数を愛するようになったか

この現象の原因は、参考の 2007-06-03 においても言及されている通り、x86 では浮動小数レジスタが 80bit の精度を持っており、レジスタから float/double として取り出したときに丸めが発生していることにある。

これによって、上のサンプルコードで False になる例では、

を比較してしまい、false になるのである。

こういう挙動をして良い、という根拠

早速この挙動の根拠を探してみると、C# 2.0 の仕様書や Annotated C# Standard の§11.1.6 に、

Floating-point operations can be perfomed with higher precision than the result type of the operation.

と書かれており、さらに直後の example で、Some hardware でサポートされている "exntended" or "long double" な浮動小数演算について述べられている。さらにその例では、x * y / z といった演算が、浮動小数の演算精度によって Inf になったりならなかったりしうる旨も述べられている。ので、上の例で == になっていないのも仕様の範囲内なのである。

C# の float, double は IEC 60559*5 formats によって represent される型であると仕様(§11.1.6)に明示してある。が、計算過程で一時的に 80bit などに拡張される(ことによって float, double の精度で計算した場合と結果が異なりうる)ことは仕様に織り込み済み、ということだろう。

そもそも

float やら double やらを ==, != している時点で間違っていると思わんでもない。
思わぬずれや丸めが起こることを承知で扱うのでないならば、整数や整数を用いた型を使うべきであろう。

*1:VMWare fusion @ Intel Core 2 E6600

*2:Intel Xeon X3220

*3:AMD Opteron 1000

*4:個人的に読みやすいと思う書き方に変えながら

*5:IEEE 754 とも言われる