強火で進め

このブログではプログラム関連の記事を中心に書いてます。

Unity の C# で限りなく小さい float 値を使いたい時には float.Epsilon を使ってはダメ

Facebook の Unity ユーザー助け合い所にて、 0〜1 の範囲で 0 と 1 は含まないとう条件で float 型の乱数を取得する方法についての質問が出ていました。

Unityユーザー助け合い所
https://www.facebook.com/groups/unityuserj/permalink/821259101267365/

ここで出た回答の中のプログラムの一つがこちら。

Random.Range ( 0f+float.Epsilon, 1f-float.Epsilon );

自分もこのプログラムを思いついたのですが自分が観た時点では既にこの書き方には問題有りとの追加のコメントが付いていました。

具体的には 1f-float.Epsilon == 1f に成ってしまうというものでした。

if 文を工夫して回避してみる

それなら if 文の方を工夫すれば良いのでは?と思って以下の様なプログラムで検証してみました。

		float v = 1f - float.Epsilon;
		if (Mathf.Abs(v - 1f) < float.Epsilon) {
			Debug.Log ("one");
		} else {
			Debug.Log ("not one");
		}

if 文の部分は以下の様な内容のはずなので else 側になり、 "not one" が出力されると予想していたのですが "one" が出力されました。

 if (Mathf.Abs(v - 1f) < float.Epsilon) { 

   ↓

 if (Mathf.Abs(-float.Epsilon) < float.Epsilon) { 

   ↓

 if (float.Epsilon < float.Epsilon) { 

予想外の結果となったので検証の為に以下のプログラムを実行

		float v;
		v = 1f;
		Debug.Log (v.ToString("G17"));
		v = 1f - float.Epsilon;
		Debug.Log (v.ToString("G17"));
		v = 1f + float.Epsilon;
		Debug.Log (v.ToString("G17"));

結果は3つとも 1 を出力。

ならば 0 の場合はと以下のプログラムを実行

		float v;
		v = 0f;
		Debug.Log (v.ToString("G17"));
		v = 0f - float.Epsilon;
		Debug.Log (v.ToString("G17"));
		v = 0f + float.Epsilon;
		Debug.Log (v.ToString("G17"));

結果は 0 と -1.40129846E-45 と 1.40129846E-45 。 0 に加える場合にはちゃんと反映される様です。浮動小数点数の「丸め誤差」の影響で 0 に加える場合は反映されやすいけど 1 に加える場合には反映されづらい(丸められる)のかな?でも float.Epsilon なのに何故?

.NET の計算機イプシロン

Wikipedia の「計算機イプシロン」の「.NET」についての項目を確認すると恐ろしい記述が

Microsoftの.NET Frameworkのライブラリに、Double.Epsilonというフィールド(プロパティ)があるが、これは浮動小数点方式で表現可能な最小の正の非正規化数であり、計算機イプシロンではない。

計算機イプシロンではない!! 計算機イプシロンではない!!

自分でイプシロンの値を求める

という事で、 Unity は Mono だけど .NET 相当だし、この辺りのルールも同じだろうと考えると float.Epsilon を使うというアイディアはダメですね。

代わりに自分で「計算機イプシロン」の値を求めましょう。
今回はアルゴリズム本として有名な奥村晴彦さんの『コンピュータ・アルゴリズム事典』(技術評論社,1991年)のサポートページにて配布されているソースコードの中の float.c を参考に以下の様なプログラムを作ってみました。

using UnityEngine;
using System.Collections;

public class Test : MonoBehaviour {

	float foo(float x) {  return x;  }

	void Start()
	{
		int b, p;
		float x, y, eps;
		
		x = y = 2;
		while (foo(x + 1) - x == 1) x *= 2;
		while (foo(x + y) == x) y *= 2;
		b = (int)(foo(x + y) - x);
		p = 1;  x = b;
		while (foo(x + 1) - x == 1) {  p++;  x *= b;  }
		eps = 1;
		while (foo(1 + eps / 2) > 1)  eps /= 2;
		eps = foo(1 + eps) - 1;
		Debug.Log(string.Format("b = {0}, p = {1}, eps = {2}\n", b, p, eps));

		float t;
		// Test1
		t = 0;
		Debug.Log(string.Format("0 + eps = {0:G17}\n", t + eps));
		// Test2
		t = 1;
		Debug.Log(string.Format("1 - eps = {0:G17}\n", t - eps));
		// Test3
		t = 0;
		if (0 == (t + eps)) {
			Debug.Log("equal");
		} else {
			Debug.Log("not equal");
		}
		Debug.Log(string.Format("1 / (0 + eps) = {0}\n", 1/(t + eps)));
	}
}

このプログラムの実行結果はこちら。

b = 2, p = 24, eps = 1.192093E-07
0 + eps = 1.192093E-07
1 - eps = 0.999999881
not equal
1 / (0 + eps) = 8388608

すべて良い感じの結果が返ってきています。

という事で Unity ではこういう場面では float.Epsilon の代わりに 1.192093E-07 (※)を使うと良いでしょう。
※あくまで Mac で実行した時に算出した値なので OS や CPU の違いによっては異なる値になる可能性が有ることには注意して使って下さい。

因みに float.Epsilon や Mathf.Epsilon に設定されている値は 1.40129846E-45 でした。

関連書籍

C言語による最新アルゴリズム事典 (ソフトウェアテクノロジー)

C言語による最新アルゴリズム事典 (ソフトウェアテクノロジー)

参考サイト

小数(浮動小数点数型)の計算が思った結果にならない理由と解決法: .NET Tips: C#, VB.NET
http://dobon.net/vb/dotnet/beginner/floatingpointerror.html