浮動小数点の深淵:PL/IにおけるFLOAT BINARYとFLOAT DECIMALの「誤差」と向き合う
メインフレームの現場で、数十年にわたり稼働し続けるバッチプログラムの保守を任されたとき、最も忌々しい敵の一つが「浮動小数点演算の精度問題」だ。
特に、銀行の利息計算や複雑な金融エンジニアリングを担うPL/Iプログラムにおいて、`FLOAT BINARY`と`FLOAT DECIMAL`の混在は、まさに地雷原を歩くようなものだ。今回は、IEEE 754準拠の挙動が引き起こす丸め誤差と、それをJavaやC#へマイグレーションする際の「落とし穴」について、アーキテクトの視点から紐解いていく。
—
1. なぜ「浮動小数点」は常に裏切るのか
PL/Iの`FLOAT BINARY`は、内部的にバイナリ(2進数)として浮動小数点を保持する。一方で、金融帳票等で馴染み深い`FLOAT DECIMAL`は10進数として振る舞う。ここで直感に反するのが、「10進数で表現可能な数値が、2進数では無限小数になる」という事実だ。
例えば、`0.1`という数値。これをバイナリで表現しようとすると循環小数になる。これを加算・比較する際、わずかなビットのズレが積もり積もり、`IF A = B THEN`という単純な比較演算をFalseに突き落とす。
実例:比較演算での罠
/i
DCL A FLOAT BINARY(21) INIT(0.1);
DCL B FLOAT BINARY(21) INIT(0.0);
/ ループで0.1を10回足し合わせる /
DO I = 1 TO 10;
B = B + 0.1;
END;
/ 理論上はBは1.0になるはずだが、内部的には0.9999999…となる /
IF B = 1.0 THEN
PUT SKIP LIST(‘成功’);
ELSE
PUT SKIP LIST(‘不一致発生:誤差が閾値を超えた’);
このコードを動かせば、ほとんどの環境で「不一致」が弾き出される。基幹システムでは、この「わずかな差異」が、数億円規模の端数処理エラーとして顕在化する。
—
2. マイグレーション時の「符号反転バグ」という悪夢
レガシー移行のプロジェクトにおいて、PL/Iの`FIXED DECIMAL(パックデシマル)`をJavaの`BigDecimal`へ変換する際、最も恐ろしいのは内部符号(Zone/Packed)の扱いの差異だ。
PL/Iでは、パックデシマルの符号ビットが`X’C’`や`X’F’`であれば正、`X’D’`であれば負とみなされる。しかし、レガシーデータの中には、あえて不正な符号ビットを持つデータ(いわゆる「腐ったデータ」)が潜んでいることがある。COBOLやPL/Iのコンパイラは、この「不正な符号」に対して寛容、あるいは特定の最適化オプション下で予期せぬ動作をする。
これをJava等の厳密な言語へ移植すると、`NumberFormatException`で即座にアベンド(ABEND)する。移行設計のフェーズでは、「データの整合性チェック」をアプリケーションロジックよりも先に、データマイグレーションツール側で徹底的に正規化することが、アーキテクトとしての最低限の責務だ。
—
3. ダンプ解析とポインタ操作:極限の現場から
CICS環境や大規模バッチでメモリ保護違反が発生した際、`STORAGE`をポインタで直接操作している箇所は真っ先に疑うべきだ。PL/Iの`ADDR`関数と`BASED`変数の組み合わせは強力だが、浮動小数点演算の結果をメモリ上に直書きしている場合、その変数の属性(精度)をコンパイラがどう解釈しているかを見抜かなければならない。
デバッグの勘所:
- コンパイラオプションの確認: `TRUNC(BIN)`か`TRUNC(STD)`か。この設定一つで、計算途中の端数処理が変わり、計算結果が微妙にズレる。
- ダンプの読み方: ABEND発生時のダンプで、浮動小数点レジスタ(FPR)の中身を16進数で追う。特に`FLOAT BINARY`がIEEE形式か、それとも古いIBM形式(Hexadecimal Floating Point)かを見極める必要がある。現在の大半のモダンな環境はIEEE 754だが、古い資産をそのままコンパイルし直すと、この「形式の不一致」で計算結果が化ける。
—
4. 移行アーキテクトへの提言
JavaやC#への移行を検討する際、PL/Iの`FLOAT`型を単なる`double`や`float`に置き換えてはならない。それは「時限爆弾を移植する」のと同義だ。
1. 金融計算はすべて`BigDecimal`に倒せ: 浮動小数点は計算誤差を許容しない限り、基幹システムには持ち込むべきではない。
2. 比較演算には「ε(イプシロン)」を: どうしても浮動小数点が必要な場合は、`abs(a – b) < 1e-9`のような許容誤差範囲(デルタ)を用いた比較ロジックを強制的に実装させる。
3. コンパイラ最適化の可視化: 既存のPL/Iプログラムが`OPTIMIZE(3)`でコンパイルされている場合、レジスタへの退避・復帰の挙動が特殊である。ソースコードの見た目以上に「コンパイラがどうコードを生成しているか」を理解した上でマイグレーションを行わなければ、単体テストでは見抜けない「本番環境だけの不一致」に泣くことになる。
PL/Iという言語は、非常に厳格で、同時に非常に寛容な側面を持つ。その二面性を理解し、ビットレベルでデータを制御できる感覚こそが、真のメインフレーム・アーキテクトに求められる資質だ。
コードを読み、メモリを読み、そしてデータが辿る運命を予測する。このプロセスこそが、レガシー移行を単なる置き換え作業ではなく、次世代へ向けての「再構築」へと昇華させる鍵となる。
