Java は浮動小数点数の表現に IEEE 754 標準を使用している。この表現形式では、単精度浮動小数点数は、符号部1ビット、指数部8ビット、仮数部23ビットでエンコードされる。倍精度浮動小数点数も同様にエンコードされるが、符号部1ビット、指数部11ビット、仮数部52ビットとなる点が異なる。これらのビット列が表現する値をそれぞれ、符号部分 s、仮数部 M、指数部 E とすると、浮動小数点数の値は (-1)s * M * 2 E と計算される。
通常、仮数部のすべてのビットを使って有効数字を表現しており、先頭の 1 は省略している。そのため、単精度浮動小数点数は有効数字24ビット、倍精度浮動小数点数は有効数字53ビットとなる。これらは正規化数と呼ばれる。
表現したい値が小さすぎて正規化数としては表現できない場合、非正規化形式でエンコードされる。非正規化数は、指数部の値を(単精度浮動小数点数の場合) Float.MIN_EXPONENT - 1、あるいは(倍精度浮動小数点数の場合) Double.MIN_EXPONENT - 1 とする。非正規化数では1の位は0とし、小数部分を表す仮数部の先頭部分には1個以上の0が続く。これら先頭からの0の列は有効数字を表さない。したがって、非正規化数の有効数字は正規化数のそれよりも小さくなる。精度が求められる場合には、非正規化数ではなく正規化数を使ったとしてもリスクは存在する。詳しくは「NUM04-J. 正確な計算が必要なときは浮動小数点数を使わない」を参照。
非正規化数を使用すると浮動小数点数演算の精度は大きく損われる。したがって、非正規化数は使うべきではない。
非正規化数の検出
以下のコードは、float の値が、FP-strict モードすなわち "extended range support" のないプラットフォーム向けに非正規化されているかどうかを検査している。"extended range support" がある場合に行う非正規化数の検査は、プラットフォーム依存である。詳しくは「NUM06-J. どのプラットフォームでも一貫した浮動小数点数演算を行うために strictfp 修飾子を使う」を参照。
strictfp public static boolean isDenormalized(float val) { if (val == 0) { return false; } if ((val > -Float.MIN_NORMAL) && (val < Float.MIN_NORMAL)) { return true; } return false; }
double 型の値が非正規化されているかどうかの検査も同様である。
非正規化数の表示
非正規化数はその表示形式も通常と異なるので、問題となりうる。単精度浮動小数点数と正規化された倍精度浮動小数点数を %a 書式指定子を使って表示すると、0でない数字が先頭に現れる。非正規化倍精度浮動小数点数の場合、仮数部の小数点の左側にゼロがくることがある。
非正規化浮動小数点数の表示形式を確認するプログラムとその出力を以下に示す。
strictfp class FloatingPointFormats { public static void main(String[] args) { float x = 0x1p-125f; double y = 0x1p-1020; System.out.format("normalized float with %%e : %e\n", x); System.out.format("normalized float with %%a : %a\n", x); x = 0x1p-140f; System.out.format("denormalized float with %%e : %e\n", x); System.out.format("denormalized float with %%a : %a\n", x); System.out.format("normalized double with %%e : %e\n", y); System.out.format("normalized double with %%a : %a\n", y); y = 0x1p-1050; System.out.format("denormalized double with %%e : %e\n", y); System.out.format("denormalized double with %%a : %a\n", y); } }
normalized float with %e : 2.350989e-38 normalized float with %a : 0x1.0p-125 denormalized float with %e : 7.174648e-43 denormalized float with %a : 0x1.0p-140 normalized double with %e : 8.900295e-308 normalized double with %a : 0x1.0p-1020 denormalized double with %e : 8.289046e-317 denormalized double with %a : 0x0.0000001p-1022
違反コード
以下の違反コード例では、浮動小数点数から非正規化数を計算し、さらに元の値を得ようとしている。
float x = 1/3.0f; System.out.println("Original : " + x); x = x * 7e-45f; System.out.println("Denormalized : " + x); x = x / 7e-45f; System.out.println("Restored : " + x);
この演算では正確な値は得られないため、FP-strictモードで実行すると以下が出力される。
Original : 0.33333334 Denormalized : 2.8E-45 Restored : 0.4
適合コード
非正規化数が発生する可能性のあるコードを使わないこと。float 型データを使用した計算で非正規化数が発生する場合、double 型を使うことで十分な精度が得られる場合がある。
double x = 1/3.0; System.out.println("Original : " + x); x = x * 7e-45; System.out.println("Denormalized : " + x); x = x / 7e-45; System.out.println("Restored : " + x);
このコードの出力は以下のようになる。
Original : 0.3333333333333333 Denormalized : 2.333333333333333E-45 Restored : 0.3333333333333333
例外
NUM05-EX0: アプリケーションが要求する精度や振舞いを計算結果が満たしていることを数値解析によって確認できるのであれば、非正規化数を使ってもよい。
リスク評価
浮動小数点数は近似であり、非正規化浮動小数点数はさらに精度の低い近似である。非正規化数を使うと、精度が低下し、不正確なあるいは予期せぬ計算結果が得られる可能性がある。以下に示すリスク評価における深刻度は「低」であるが、正確な計算結果を必要とするアプリケーションでは、できるかぎり本ルールに適合するべきである。
ルール | 深刻度 | 可能性 | 修正コスト | 優先度 | レベル |
---|---|---|---|---|---|
NUM05-J | 低 | 中 | 高 | P2 | L3 |
関連する脆弱性
CVE-2010-4476 (CVE 2008) は、Java 1.6 update 23 およびそれ以前、Java 1.5 update 27 およびそれ以前、1.4.2_29 およびそれ以前に実装されている Double.parseDouble() メソッドの脆弱性であり、細工された文字列を与えるとサービス運用妨害を引き起こす。値 2.2250738585072012e-308 は正の正規化倍精度浮動小数点数の最小値に近い値であり、この値を表す文字列を浮動小数点数に変換する際、double 型の正規化数あるいは非正規化数で近似しようとして、無限ループを引き起こす。
関連するガイドライン
The CERT C Secure Coding Standard | FLP05-C. Don't use denormalized numbers |
参考文献
[Bryant 2003] | Computer Systems: A Programmer's Perspective, Section 2.4, Floating Point |
[CVE 2008] | CVE-2010-4476 |
[IEEE 754] |
翻訳元
これは以下のページを翻訳したものです。
NUM05-J. Do not use denormalized numbers (revision 41)