【テクニカル・上級編】FLOAT BINARYとFLOAT DECIMALの精度と丸め誤差 – PL/Iの基本構文とデータ制御実践ガイド

浮動小数点の深淵:FLOAT BINARYとDECIMALが基幹システムで引き起こす「微小な誤差」の正体

メインフレームの現場で長く生きていると、ある種の「職人芸」に近い勘が働くようになる。特に、PL/Iで書かれた数十万行のレガシー資産をJavaやC#へマイグレーションする際、最もエンジニアを悩ませるのが「浮動小数点数の計算差異」だ。

「なぜ、現新比較で最後の1円が合わないのか?」
この問いに対する答えは、多くの場合、コンパイラの最適化やCPUのレジスタ操作ではなく、`FLOAT BINARY`と`FLOAT DECIMAL`という、一見似て非なるデータ型の特性に深く根ざしている。

1. 二進と十進の決定的な断絶

まず押さえておくべきは、`FLOAT BINARY`が2進数で表現されるのに対し、`FLOAT DECIMAL`は(たとえ内部的にはハードウェアが二進で処理していても)10進の精度の概念を保持しようとする点だ。

/i
/ FLOAT BINARYとFLOAT DECIMALの精度宣言の例 /
DCL B_VAR FLOAT BINARY(21); / 2進数での精度(約7桁) /
DCL D_VAR FLOAT DECIMAL(6); / 10進数での精度 /

/

  • 現場の教訓:
  • 0.1 という数値は、2進浮動小数点数では循環小数となり、無限の精度がない限り正確に表現できない。
  • この「表現できない誤差」が、大量の計算を繰り返すバッチ処理の中で雪だるま式に蓄積される。

/

マイグレーション時に「とりあえず`double`型に変換すれば良い」と考えるのは危険極まりない。PL/Iの`FLOAT BINARY(21)`はJavaの`float`に近いが、厳密な丸めモード(ROUNDED指定の有無やコンパイラオプション)が異なる場合、DB2のSQL内での計算結果と、アプリケーションサーバー上のJavaコードでの結果が、小数第10位で乖離する。この「1ビットの差」が、突き合わせ処理の`ABEND`や不整合を引き起こすトリガーになるのだ。

2. 比較演算の罠:なぜ「0.1 + 0.2 == 0.3」は偽になるのか

メインフレームのバッチ処理では、IF文による比較が頻発する。ここで浮動小数点数同士を`=`で比較してはならないのは鉄則だが、これを失念してコードを移行すると、特定のケースでしか発生しない「消えるレコード」というバグを生む。

/i
DCL X FLOAT BINARY(21) INIT(0.1);
DCL Y FLOAT BINARY(21) INIT(0.2);
DCL Z FLOAT BINARY(21) INIT(0.3);

IF (X + Y) = Z THEN / この比較は極めて危険 /
PUT SKIP LIST(‘成功’);
ELSE
PUT SKIP LIST(‘失敗’); / 実際にはこちらに流れる可能性が高い /

実務では、あらかじめ「許容誤差(イプシロン)」を定数として持ち、`ABS((X + Y) – Z) < EPSILON` といった比較関数を噛ませるのが定石だ。マイグレーションにおいても、このロジックを共通部品としてJava側に移植することが、将来のトラブルを未然に防ぐ鍵となる。

3. メモリ操作とダンプ解析:深淵を覗くとき

私がトラブルシューティングで最も嫌うのは、`POINTER`を駆使して`BASED`変数に浮動小数点数を流し込むコードだ。特に、`CICS`や`DB2`の埋め込みSQLで動的にメモリを確保し、データ構造を再定義(`DEFINED`属性)している箇所は、アベンド時のダンプ解析において地獄を見る。

もし、浮動小数点数のビットパターンが不正(`NaN`や`Infinity`など)な状態で計算ユニットに送られた場合、ハードウェア例外が発生する。この際、`CEE`(Language Environment)のダンプを紐解くと、レジスタ内の値がパック10進数(`FIXED DECIMAL`)と誤認されているケースに遭遇することがある。これは、`UNION`や`DEFINED`による型混同が原因であることが多い。

  • ダンプ解析のヒント:
  • `CEE3DMP`の「Floating Point Registers」を凝視せよ。
  • 期待値とビットパターンがわずかに違う場合、それは浮動小数点の丸めではなく、エンディアンやパック10進の符号反転(`X’F’`と`X’C’`の混同)を疑うべきだ。

4. マイグレーションを成功させるための「アーキテクチャの矜持」

PL/IからJava/C#への移行において、最も重要なのは「計算ロジックの完全な再現」ではない。「計算の誤差の性質を理解し、ビジネスロジックとして許容可能な精度の枠組みを再定義すること」である。

1. 型変換を自動化しない: 全ての`FLOAT`型を、単なる浮動小数点型として移行するな。金額計算であれば、迷わず`java.math.BigDecimal`(または同等の任意精度演算型)へ置き換えるべきだ。
2. コンパイラオプションの差異を文書化せよ: PL/Iの`RULES(FLOAT(HEX))`と`RULES(FLOAT(IEEE))`の挙動の違いを理解していないチームは、マイグレーションに失敗する。ハードウェア依存の挙動を、ソフトウェア側でどうエミュレートするか、それがアーキテクトの腕の見せ所だ。

最後に

技術は進歩するが、浮動小数点の抱える「不完全さ」は変わらない。汎用機の堅牢さは、この不完全さを「計算誤差」としてではなく「厳密なルール」として扱い、制御してきた歴史の上に成り立っている。

レガシー移行は、単なるコードの書き換えではない。それは、過去のシステムが積み上げてきた「丸め誤差という名の真実」を、新しいプラットフォームで再解釈する儀式なのだ。この深い理解なしに、基幹システムの移行は語れない。

タイトルとURLをコピーしました