おい、最近入った若手がまた「数値計算の辻褄が合わない」「本番バッチで突然データ例外(S0C7じゃない、サイズオーバー系の変な挙動)が起きた」って青い顔して駆け込んできたんだ。
原因を覗いてみたら、やっぱりな。`FIXED BINARY` と `FIXED DECIMAL` をお互いの特性も分からずに、そのまま無造作に算術演算へ放り込んでやがった。
メインフレームの基幹システムを支えるPL/Iにおいて、数値データの扱いは基本中の基本だが、一番油断がならない地雷原でもある。今日は、この「基数の異なる固定小数点数同士の混在演算」について、コンパイラの裏側の動きも含めて徹底的に叩き込んでやる。耳の穴かっぽじってよく聞けよ。
—
1. なぜ「固定小数点数」の混在で事故が起きるのか?
PL/Iには、数値を正確に扱うためにふたつの大きな柱がある。
- `FIXED DECIMAL`(パック十進数 / ZONED十進数):COBOLの `COMP-3` や `DISPLAY` に相当するものだ。人間が読む10進数の世界をそのままハードウェア(あるいはソフトウェアエミュレーション)で処理する。金額や数量など、1円の狂いも許されないビジネスロジックの主役だ。
- `FIXED BINARY`(二進固定小数点数):COBOLの `COMP` や `BINARY` だな。マシン語のレジスタが直接理解する2進数の世界であり、主にループ制御変数(インデックス)や配列の添字、高速なカウンタとして使われる。
この「10進数の住人」と「2進数の住人」を一つの数式の中で混ぜて演算(例:`A (FIXED BINARY) = B (FIXED DECIMAL) + C (FIXED BINARY)` など)させると、コンパイラ(Enterprise PL/I)は親切心(あるいは冷徹な仕様)から、勝手に裏でデータ型の変換を行ってくれる。
しかし、この「自動変換」のルールと、その時の中間ワークエリアの精度決定ロジックを把握していないと、想定外の精度落ち、最悪の場合は桁あふれ(Overflow)を引き起こす。現場で「夜間バッチが突然異常終了した」というトラブルの、実に3割はこのデータ型のミスマッチが原因なのさ。
—
2. コンパイラによる自動変換順序と精度決定のメカニズム
二つの異なる型のオペランドが演算子で結ばれたとき、コンパイラは一体どんな基準でどちらに合わせ、結果をどこに収めているのか。そのアルゴリズムの核心を教えよう。
変換の基本原則:より「広い」型への昇格
基本原則として、精度のロスを防ぐため、コンパイラは表現力の高い方、あるいは情報が失われにくい方へ型を揃えようとする。
基本的には `FIXED DECIMAL` と `FIXED BINARY` が混在する場合、演算の大部分は `FIXED DECIMAL` へ昇格(あるいは共通の形式へ一時変換) して処理されることが多い。だが、これが厄介なのは「どちらの基数に統一されるか」が演算の種類やコンパイラのバージョン、さらに言えば式の評価順序(左から右への結合規則)によって微妙に変化する点だ。
中間ワークエリア(コンパイラが作る隠し変数)の魔力
PL/Iの式評価では、コンパイラが目に見えない「一時的なワークエリア(Temporary Work Area)」をメモリ上に確保して中間結果を保持する。
例えば、次のような演算があったとする。
1
DCL WK_DEC FIXED DEC(9,2) INIT(100.50);
DCL WK_BIN FIXED BIN(31) INIT(3);
DCL ANS FIXED DEC(11,4);
ANS = WK_DEC WK_BIN;
この時、コンパイラは内部で何をしているか?
1. `WK_BIN`(固定二進数)を、一度一時的な `FIXED DECIMAL` に変換する。
2. 変換された十進数と `WK_DEC` を掛け合わせる。
3. その際の中間結果の精度(整数部・小数部の桁数)は、PL/Iの言語仕様で定められた「算術式の精度規則(Precision Rules for Arithmetic Expressions)」に従って自動計算される。
4. 最終的な結果を、代入先の `ANS` の型に合わせて丸める(必要なら切り捨て・四捨五入)。
ここで問題になるのが、「中間結果の精度が、プログラマの意図しないところで勝手に切り詰められる」という現象だ。特に割り算(`/`)や乗算(“)が絡むと、小数点以下の有効桁数が爆発的に増えたり、逆にコンパイラのデフォルト上限に引っかかって桁落ちしたりする。
—
3. 実践!VSAMファイル処理と算術混在演算のサンプルコード
百聞は一見にしかずだ。実際のメインフレームのバッチプログラムを想定したコードを見てみよう。
顧客ごとの単価(`FIXED DEC`)と数量(`FIXED BIN`)を掛け合わせて売上金額を算出し、VSAM(KSDS)のマスターファイルを更新する、というよくあるシチュエーションだ。
1
————————————————————–;
- モジュール名: CALCVLU – 売上計算・VSAM更新バッチサンプル
————————————————————–;
CALCVLU: PROC OPTIONS(MAIN);
/ 宣言部:ファイル定義 /
DCL CUSTMST FILE RECORD SEQUENTIAL UPDATE
ENVIRONMENT(VSAM);
/ 顧客マスターレコード構造体 /
DCL 1 CUST_REC,
5 CUST_ID CHAR(5), / 顧客ID /
5 CUST_NAME CHAR(30), / 顧客名 /
5 UNIT_PRICE FIXED DEC(7,2), / 単価 (DEC) /
5 PURCHASE_QTY FIXED BIN(15), / 購入数量(BIN)/
5 TOTAL_AMT FIXED DEC(11,2); / 総額 (DEC) /
/ ワーキングストレージ変数 /
DCL EOF_FLG CHAR(1) INIT(‘OFF’);
DCL W_CALC_WK FIXED DEC(11,4); / 中間計算用 /
/ 組み込み関数(BUILTIN)の明示的宣言 /
DCL (ABS, ROUND) BUILTIN;
/ 条件制御(ファイル終了等) /
ON ENDFILE(CUSTMST) EOF_FLG = ‘ON’;
/ VSAMファイルのオープン /
OPEN FILE(CUSTMST) UPDATE;
/ メインループ /
READ FILE(CUSTMST) INTO(CUST_REC);
DO WHILE (EOF_FLG = ‘OFF’);
/ ————————————————– /
/ 【重要】FIXED DEC と FIXED BIN の混在演算 /
/ UNIT_PRICE (DEC) と PURCHASE_QTY (BIN) の掛け算 /
/ ————————————————– /
/ 悪い例:中間精度を意識せずダイレクトに代入すると /
/ コンパイラの暗黙の精度規則に依存してしまう /
/ CUST_REC.TOTAL_AMT = CUST_REC.UNIT_PRICE CUST_REC.PURCHASE_QTY; /
/ 良い例:意図した中間精度を確保し、ROUND関数で丸める /
W_CALC_WK = CUST_REC.UNIT_PRICE CUST_REC.PURCHASE_QTY;
/ 四捨五入を行って最終項目へ格納 /
CUST_REC.TOTAL_AMT = ROUND(W_CALC_WK, 2);
/ VSAMマスターの書き換え /
REWRITE FILE(CUSTMST) FROM(CUST_REC);
/ 次レコード読み込み /
READ FILE(CUSTMST) INTO(CUST_REC);
END;
/ クローズ処理 /
CLOSE FILE(CUSTMST);
RETURN;
END CALCVLU;
このコードのポイントは、`UNIT_PRICE`(`FIXED DEC(7,2)`)と `PURCHASE_QTY`(`FIXED BIN(15)`)の演算結果を、一度十分な小数部を持つワーク変数 `W_CALC_WK`(`FIXED DEC(11,4)`)で受けている点だ。
もしこれを直接 `TOTAL_AMT` に代入したり、あるいは逆に `FIXED BIN` 同士の計算に引きずり込もうとすると、コンパイラが生成する機械語コード(アセンブラコード)のなかで予期せぬパック十進数から二進数への変換(あるいはその逆)が多発し、パフォーマンスの低下や微小な丸め誤差を生む原因になる。
—
4. デバッグとONユニットによる例外制御の知見
もし、この混在演算の最中にデータが大きくなりすぎて桁あふれを起こしたらどうなるか? PL/Iの世界では、何も対策していなければ `SIZE` 条件 や `OVERFLOW` 条件 が発生し、即座にU4038やSCC0などのアベンド(異常終了)を引き起こす。
基幹システムのベテランなら、こうした例外を野放しにはしない。必ず `ON` ユニットでトラップを仕掛けるか、あるいはコンパイラオプションで制御する。
1
/ 演算時のサイズ超過(桁あふれ)をトラップするONユニット /
ON SIZE
BEGIN;
DISPLAY(‘【SEVERE ERROR】算術演算でサイズ超過(桁あふれ)が発生しました。’);
DISPLAY(‘該当顧客ID: ‘ || CUST_ID);
/ 必要に応じたエラールーチンやダンプ取得をここに記述 /
GOTO ERR_ABEND;
END;
実務における鉄則として、「演算に関わる変数の型は、極力同じ基数(特に金額や数量なら `FIXED DECIMAL`)に統一する」、これが一番の防御策だ。どうしてもループカウンタの `FIXED BIN` を金額計算に絡めたい場合は、必ずキャスト(PL/I流にいえば `DECIMAL` ビルトイン関数などによる型明示変換)を行うこと。
—
5. 先輩エンジニアからの実務アドバイス
最後に、現場で後輩によく言うアドバイスを授けておこう。
1. コンパイラのリスト(リストリング)を読め!
PL/Iのコンパイル時に出力されるクロスリファレンスや属性リストには、コンパイラが勝手に決定した中間変数のデータ型と精度がすべて記載されている。「動いたからヨシ」ではなく、大規模改修の時は必ずコンパイラリストで中間ワークの属性を確認する習慣をつけろ。
2. 暗黙の型変換に頼るな!
「コンパイラが勝手にやってくれるからいいや」という甘えが、数年後の保守フェーズで「なぜか端数が1円合わない」という悪夢のような調査工数を生む。型を合わせるためのキャストやワーク変数をケチるな。
メインフレームのPL/Iは、私たちが書いたコードの意図を忠実に、かつ厳格に実行する。その厳格さに正面から向き合えるエンジニアこそが、止まらない基幹システムを守り抜く真のプロフェッショナルだ。次のバッチ改修でも、この精度規則のロジックを思い出してスマートにコードを組んでくれよ。期待しているぞ。
