銀行勘定系をJavaへ移植する際、PL/Iの`FIXED DECIMAL`が「死神」と呼ばれる理由
メインフレームの現場で何十年と稼働し続けてきたPL/Iの基幹ロジックを、Javaへマイグレーションする。このプロジェクトにアサインされたテックリードたちが、口を揃えて「地獄」と呼ぶのが、数値演算の精度管理、特に`FIXED DECIMAL`(パック10進数)の扱いです。
IBMのEnterprise PL/Iコンパイラは、演算において極めて厳格なバイナリ・コードを生成しますが、Javaの`BigDecimal`を安易に採用した瞬間、その計算結果は「微妙に」ズレ始めます。今日は、この「一見些細だが致命的な」差異について、アーキテクチャの観点から深掘りします。
—
1. パック10進数の内部構造と「符号反転」の闇
PL/Iの`DCL AMOUNT FIXED DEC(15, 2)`は、メモリ上で4ビット単位のパック10進数として格納されます。特筆すべきは、末尾のニブル(4ビット)が符号(C=プラス、D=マイナス)を保持している点です。
ここで注意すべきは、「無効な符号ビット」によるアベンド(S0C7)です。
レガシーシステムでは、DB2のテーブルから不適切なデータが混入したり、ポインタ操作でメモリを破壊して符号ビットが`F`になった際、演算時にコンパイラが生成したハードウェア命令(`AP`命令や`SP`命令)が例外を吐き出します。
Javaへ移行する際、この「符号ビットの不整合」をどうハンドリングするか。単にDBから値を引いてくるだけでは、PL/Iが実行していた「異常データに対する暗黙的な丸めや例外処理」を再現できません。移行先では、入出力の段階で厳格なバリデーション・ロジックを挟む必要があります。
—
2. 丸め誤差の「絶対値」が一致しない理由
PL/Iの固定小数点演算は、コンパイルオプション `FIXEDOVERFLOW` や `TRUNC` に大きく依存します。例えば、以下のコードを考えてみてください。
/i
/ 固定小数点演算の例 /
DCL VAL1 FIXED DEC(5, 2) INIT(10.00);
DCL VAL2 FIXED DEC(5, 2) INIT(3.00);
DCL RESULT FIXED DEC(5, 2);
/ ここで除算が発生する場合、PL/Iは指定した精度に合わせて切り捨てを行う /
RESULT = VAL1 / VAL2;
PL/Iのコンパイラは、この式に対して「中間結果」をどの程度の精度で保持するかを決定します。一方で、Javaの `BigDecimal.divide()` は、明示的に `RoundingMode` を指定しない限り `ArithmeticException` を投げます。
設計上の鉄則:
マイグレーション時は、PL/Iのコンパイルリスト(`LIST`オプション付与)を解析し、中間変数の精度がどう定義されているかを全てドキュメント化してください。Java側で `RoundingMode.DOWN` を使うか `HALF_UP` を使うか、あるいはPL/I特有の「桁あふれ時の動作」を再現するために `MathContext` を自作するか。ここを疎かにすると、決算処理で1円のズレが数万件発生し、夜通しの突き合わせ作業が確定します。
—
3. ポインタ操作と動的メモリの「地雷」
基幹システムのバッチ処理では、`BASED`変数を使って大きなバッファを動的に再定義することがよくあります。
/i
/ ポインタを用いた動的データ操作の例 /
DCL BUFFER CHAR(1024) BASED(P);
DCL 1 HEADER BASED(P),
2 RECTYPE CHAR(1),
2 RECSIZE FIXED BIN(15);
/ Pを動的に進めることで、バッファを構造体として解釈する /
P = ADDR(STORAGE_AREA);
このポインタ操作をJavaのオブジェクト指向にマッピングしようとすると、`ByteBuffer` を使うか、あるいはPOJOに展開するかという選択を迫られます。しかし、PL/Iではポインタが指す先が物理メモリそのものであるため、最適化オプション(`OPT(3)`など)によっては、コンパイラがレジスタ間で値を保持し、メモリ上の値を即座に更新しないことがあります。
アベンドダンプ(`SYSUDUMP`)を解析した経験がある方ならご存知でしょうが、PL/Iのメモリレイアウトは極めて密です。これをJavaのヒープに展開する際は、「オフセットの計算ミス」が多発します。特に `UNALIGNED` 指定された変数は、境界調整が行われないため、アライメントの差異によるアクセス違反をJava側でシミュレートしなければなりません。
—
4. アーキテクトへの提言:移行の「解」
移行担当者に私がいつもアドバイスするのは、「PL/Iの挙動をJavaで100%再現しようとしない」ことです。それは現代的なアーキテクチャへの冒涜です。
1. 計算エンジンを分離せよ: 計算ロジックだけは、PL/Iのソースコードをリバースエンジニアリングして、高精度なテストケースを自動生成する。
2. 境界チェックを明示化せよ: Java側で `BigDecimal` をラップした専用の数値クラスを作成し、`FIXED DECIMAL` が持っていた「精度管理」と「桁あふれ時の動作」をコンストラクタで強制させる。
3. ダンプを捨てるな: 移行前と移行後のバッチ処理で、同じ入力データに対して中間状態(メモリ上のダンプイメージ)をファイル出力し、バイナリ比較するツールを自作せよ。
PL/Iは、ハードウェアの制約を最大限に引き出すための言語でした。それをソフトウェアの抽象化が進んだJavaへ移すことは、単なる翻訳ではなく「文化の移植」です。
コードを眺める時、その背後に流れる `System/390` のクロック音や、磁気テープが回転する熱を感じ取ってください。それが、真に信頼されるシステムアーキテクトへの第一歩です。技術は変わっても、計算の正確性という「エンジニアの矜持」は、言語の垣根を超えて守り抜かねばなりません。
