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

銀行勘定系を揺るがす「数ペンスの誤差」:PL/Iの`ROUND`関数と浮動小数点・固定小数点演算の深淵

メインフレームの現場で長くシステムを見守ってきたアーキテクトなら、誰もが一度は「なぜ、最終残高が1セント(あるいは1円)合わないのか」という悪夢のような調査に直面したことがあるはずだ。

JavaやC#といったモダン言語からやってきた若いエンジニアは、「たかが四捨五入、`ROUND`関数を使えば一発だろう」と軽口を叩く。しかし、私たちが日々向き合っているIBM Enterprise PL/Iの世界において、数値演算と丸め処理は、コンパイラの内部挙動やCPUのハードウェア命令(10進演算命令群:PACK, UNPACK, ZAP, APなど)の特性を完全に掌握していなければ、地雷を踏み抜くようなものだ。

特に、金融や公共の基幹システムにおける金利計算や外貨換算では、わずかな丸め誤差の蓄積がコンプライアンス上の致命傷となる。今回は、PL/Iの`ROUND`関数が内部でどのような精度制御を行っているのか、そしてモダン言語へのマイグレーションやDB2/CICSが絡むエッジケースにおいて、いかにしてこの丸め誤差の罠を回避すべきか、アーキテクトの視点から徹底的に紐解いていこう。

—

1. `ROUND`関数の正体:内部加算とシフトアルゴリズムの現実

PL/Iの`ROUND(x, n)`は、一見すると単なる丸め関数に見えるが、その実体は対象データの属性(固定小数点2進: `FIXED BINARY`、固定小数点10進: `FIXED DECIMAL`、あるいは浮動小数点: `FLOAT`)によって、コンパイラが生成する機械語コードが劇的に変化する。

特に基幹システムで主力として使われるパック十進数(`FIXED DECIMAL`)に対する`ROUND`の挙動は、単なる「四捨五入」ではない。IBM Enterprise PL/Iコンパイラは、指定された小数点位置($n$)の一つ下の桁に「5」相当の値を加算し、指定桁で切り捨てる(あるいはシフトする)という最適化されたインラインコード、またはランタイムルーチンを生成する。

ここで問題になるのが、「基底変数の定義精度」と「中間演算の精度(プレシジョン)」のミスマッチである。

実務で遭遇する危険なコードパターン

以下のPL/Iコードを見てほしい。一見、何の問題もないように見えるが、コンパイルオプションや変数の定義次第で、意図せぬ丸め誤差や、最悪の場合はサイズオーバーフロー(S0C7アベンド)を引き起こす。

1
/ —————————————————- /
/ 危険なROUND関数の使用例と精度落ちのメカニズム /
/ —————————————————- /
SAFE_CALC: PROC OPTIONS(MAIN);

DCL WK_AMT1 FIXED DEC(11,2) INIT(123.45);
DCL WK_AMT2 FIXED DEC(11,2) INIT(1.03);
DCL WK_RATE FIXED DEC(5,4) INIT(0.0525);
DCL WK_RESULT FIXED DEC(11,2);

/ 金利計算を行い、小数点以下3桁目を四捨五入して2桁にする /
/ 注意: 暗黙の中間演算プレシジョンの罠が潜んでいる /
WK_RESULT = ROUND((WK_AMT1 + WK_AMT2) WK_RATE, 2);

PUT SKIP LIST (‘RESULT = ‘, WK_RESULT);

END SAFE_CALC;

このコードの何が問題か?
PL/Iの言語仕様では、式の中間結果のプレシジョンはコンパイラの規則(Rules of Evaluation)によって自動決定される。しかし、乗算が行われる際、桁数が意図せず拡張され、最終的な`ROUND`関数が適用される前に、ハードウェアの10進レジスタの限界や、受け側変数への代入時の切り捨て(Truncation)が先行して発生する場合があるのだ。

特に、コンパイラオプションで`TRUNC(BIN)`ではなく`TRUNC(STD)`や`TRUNC(OPT)`を指定している場合、桁あふれに対するハードウェアの例外割り込み(S0C7:データ例外)の検知タイミングが変わり、デバッグを極めて困難にする。

—

2. アベンド(S0C7)と符号反転バグの恐怖

パック十進数(ゾーン10進数から変換されたパッカブルなデータ)を扱う際、`ROUND`や算術演算の過程で最も恐ろしいのは S0C7(Data Exception) と パックデシマルの符号ニブル破壊 である。

メインフレームのCOBOLやPL/Iでは、数値データの最下位バイトの後半4ビット(ニブル)に符号(`C`, `D`, `F`など)が格納される。外部ファイル(VSAMやSequential)やDB2から生データを取得した際、ゾーン抜けや不正な文字データが混入していると、`ROUND`関数が実行された瞬間にCPUが「これは有効なパック形式ではない」と判断し、容赦なくジョブを異常終了させる。

さらに悪質なのは、マイグレーション時のデータ移行や、CICSのコンテキストエリア(COMMAREA)経由でのデータ受け渡しにおいて、符号ビットが反転・喪失するケースだ。マイナス値に対する`ROUND`処理は、単なる切り捨てではなく「ゼロ方向への丸め」なのか「負の無限大方向への丸め(算術的四捨五入)」なのかというセマンティクスの違いがある。PL/Iの`ROUND`は数学的な四捨五入(四捨五入して絶対値を大きくする方向)を基本とするが、マイグレーション先のJava(`BigDecimal.setScale(2, RoundingMode.HALF_UP)`など)との間で、負数の丸め結果が1単位ズレるというエッジケースが頻発する。これが「数ペンスの消えた監査法人指摘」の正体である。

—

3. 埋め込みSQL(DB2)およびCICS環境におけるエッジケース

オンラインCICSトランザクションの中で動的な金額計算を行う場合、パフォーマンスと精度のバランスは死活問題となる。

DB2のDECIMAL型(例: `DECIMAL(15,2)`)と、PL/I側の`FIXED DEC(15,2)`は親和性が高いように思えるが、ホスト変数としてSQLに渡す際、プレシジョンの定義が一致していないと、DB2プリコンパイラは暗黙の型変換コードを生成する。この変換の過程で、意図しない丸めや切り捨てがデータベース側で行われ、PL/I側の`ROUND`ロジックと二重に丸め処理が走ることで、精度が崩壊する現象が起きる。

1
/ —————————————————- /
/ CICS / DB2環境における安全な演算とホスト変数制御 /
/ —————————————————- /
EXEC SQL INCLUDE SQLCA;

DCL HV_BALANCE FIXED DEC(11,2);
DCL WK_FEE FIXED DEC(5,4) INIT(0.0125);
DCL LK_ACCOUNT CHAR(10);

/ DB2から残高を取得 /
EXEC SQL
SELECT CURRENT_BALANCE
INTO :HV_BALANCE
FROM ACCOUNT_TBL
WHERE ACCOUNT_ID = :LK_ACCOUNT;

IF SQLCODE ^= 0 THEN DO;
/ エラー処理 /
CICS ABEND ABCODE(‘DBER’);
END;

/ 手数料計算:厳密な精度制御のため、中間変数に一度受ける /
DCL WK_CALC_FEE FIXED DEC(11,4);
WK_CALC_FEE = HV_BALANCE WK_FEE;

/ 最終的な丸め:ここで明示的にROUNDをかけ、桁あふれを防ぐ /
HV_BALANCE = HV_BALANCE – ROUND(WK_CALC_FEE, 2);

このように、中間変数を`FIXED DEC(11,4)`のように「演算用として余分な小数桁」を持たせて定義し、最後に必要桁数で`ROUND`を適用する設計こそが、基幹システムエンジニアが身につけるべき「黄金律」である。

—

4. モダン言語(Java/C#)へのマイグレーション設計指針

レガシーシステムをJavaやC#へ移行する際、PL/Iの`ROUND`や算術演算の挙動をそのままエミュレートしようとして失敗するプロジェクトが後を絶たない。最大の理由は、Javaの`double`や`float`は論外として、`BigDecimal`を使用した場合でも「丸めモード(RoundingMode)」のデフォルト挙動が異なる点にある。

1. 丸めモードの統一:
PL/Iの`ROUND`は伝統的に算術四捨五入(HALF_UPに近い挙動、ただし負数の扱いに注意)だが、Javaのデフォルトやシステム共通ライブラリの設計によっては`HALF_EVEN`(銀行家ズールー丸め)が採用されている場合があり、これだけで月次決算の総額に数円の差異が生じる。
2. プレシジョンとスケールの厳格な定義:
Java側でDTOやエンティティを設計する際、PL/Iの`FIXED DEC(p, q)`のセマンティクスをアノテーションやバリデーション(例:JSR-303の`@Digits`)で完全再現し、演算の各ステップでスケールを明示的に指定するラッパーユーティリティを必ず作成すべきである。

—

アーキテクトからの提言

PL/Iの`ROUND`関数に代表される数値制御は、単なるプログラミングの構文要素ではない。それは、ハードウェアのアーキテクチャ、コンパイラの最適化ロジック、そして何十年と積み重ねられてきた業務データの歴史そのものと結びついている。

「動けばいい」という安易なコード記述や、中身を理解しないままのモダン言語への自動変換ツール頼みは、必ずや本番稼働後のデータ不整合という形で牙をむく。コンパイラの挙動を熟知し、データが流れる一瞬一瞬の精度をコントロールし続けること――それこそが、真のメインフレーム・アーキテクトに求められる矜持である。

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