おい、最近あがってきた口座振替のバッチ処理で、また変な金額のズレが見つかったって?
……原因はだいたい想像がつくよ。PICTURE句の「V」、つまり仮想小数点(Assumed Decimal Point)の扱いを舐めてかかった結果だな。
COBOLから入った連中によくある勘違いなんだが、「PL/IのPICTURE変数は数値としてよしなにやってくれる」なんて思っていると、夜間バッチの真っ最中に桁落ちや予期せぬ四捨五入の罠に足元をすくわれる。今日は、このPICTURE ‘V’が算術演算の裏側でコンパイラにどんなコードを書かせているのか、そしてどうして桁落ちが起きるのか、俺が骨の髄まで叩き込んでやる。
—
1. PICTURE ‘V’ の本質とコンパイラの裏の顔
まず大前提として、PL/IにはCOBOLのような「予約語」という概念が極めて薄い。変数名に `TOTAL` や `VALUE` を使っても文脈でコンパイラが賢く解釈してくれる懐の深さがある。しかし、その「賢さ」に甘えていると、データ定義の裏側でコンパイラが何をやっているかを見失う。
PICTURE ‘V’ は、データ領域上に「実際の小数点文字(`.`)を持たない」。
例えば `PIC ‘999V99’` と定義された5バイトの領域には、`12345` という数字が入っているだけで、どこが整数部でどこが小数部かは、コンパイラが持っている「型情報(属性)」だけに依存している。
ここで問題になるのが、「仮想小数点位置が異なる変数同士の演算」だ。
コンパイラが挿入する「見えないシフト命令」
PL/Iのランタイムは、異なる精度のPICTURE変数同士で足し算や掛け算を行うとき、そのままでは計算できないため、ハードウェア命令(System/z のパック十進数演算命令など)を実行する前に、自動的に小数点位置を合わせるための位置合わせ(アライメント・シフト)を行う。
お前らが何気なく書いた以下のコードを例に取ろう。
1
DCL A PIC ‘999V99′ VALUE(123.45);
DCL B PIC ’99V4’ VALUE(12.3456);
DCL C PIC ‘9999V99’;
C = A + B;
このとき、コンパイラは内部でどう動くか?
- 変数 `A` は小数点以下 2桁(内部表現としては整数扱い)。
- 変数 `B` は小数点以下 4桁。
これを足し合わせるためには、小数点位置を右側に合わせる(あるいは左側に揃える)必要がある。コンパイラは、演算の精度を落とさないために、より桁数の多い方に合わせて一時的なレジスタやワークエリア上でデータをシフトする。
だが、この「代入先の変数(左辺)の器の大きさ」を誤ると、容赦なく桁落ち(Truncation)やシグニフィカン・フォールト(有効数字の脱落)が発生するのだ。
—
2. 実践:VSAM入出力とONユニットを伴う堅牢なPL/Iプログラム
百聞は一見にしかず。実際の現場で動くレベルの、VSAM(KSDS)からレコードを読み込み、異なる仮想小数点を持つ項目同士で厳密な計算を行い、例外処理(ONユニット)で安全にトラップするバッチプログラムのサンプルを見せてやろう。
1
—————————————————————-
- 仮想小数点演算と桁落ち制御のサンプルプログラム
—————————————————————-
CALCEX: PROC OPTIONS(MAIN);
/ 宣言部:VSAMキー順データセット(KSDS)のレコードレイアウト /
DCL 1 ACCOUNT_RECORD,
3 ACC_ID PIC ‘9(08)’, / 口座番号 /
3 ACC_AMT_A PIC ‘9(003)V99’, / 数量A (小数2桁) /
3 ACC_AMT_B PIC ‘9(002)V4’, / 単価B (小数4桁) /
3 ACC_RESULT PIC ‘9(005)V99’; / 計算結果 (小数2桁) /
/ ファイル定義 /
DCL ACCOUNT_FILE FILE RECORD
ENVIRONMENT(
INPUT
VSAM
ORG(KS)
);
/ 終了フラグ /
DCL EOF_FLG CHAR(1) INIT(‘0’);
/ 条件処理(ONユニット):固定小数点オーバーフローの捕捉 /
ON FIXEDOVERFLOW
BEGIN;
PUT SKIP LIST(‘ 警告: 固定小数点オーバーフロー(桁あふれ)を検出しました ‘);
/ ここでエラーログ出力や異常終了コードの設定を行う /
SIGNAL ERROR;
END;
/ ファイルオープン /
OPEN FILE(ACCOUNT_FILE) INPUT;
/ メインループ /
DO WHILE(EOF_FLG = ‘0’);
READ FILE(ACCOUNT_FILE) INTO(ACCOUNT_RECORD);
IF EODOF(ACCOUNT_FILE) THEN
EOF_FLG = ‘1’;
ELSE
DO;
/
- 【重要】
- ACC_AMT_A (V99) と ACC_AMT_B (V4) の乗算。
- 小数点以下桁数は 2 + 4 = 6 桁となるが、
- 左辺の ACC_RESULT は V99(小数2桁)であるため、
- 代入時に下位4桁が丸められる(あるいは切り捨てられる)。
- コンパイラ任せにせず、BUILTIN関数で意図的に制御する。
/
/ 危険な暗黙的代入(コンパイラが自動で丸めるが意図が不明確) /
/ ACC_RESULT = ACC_AMT_A ACC_AMT_B; /
/ 安全かつ明示的な丸め処理の例(ROUND関数を使用) /
/ 小数点以下2桁で四捨五入して代入する /
ACC_RESULT = ROUND(ACC_AMT_A ACC_AMT_B, 2);
/ デバッグ用コンソール出力 /
PUT SKIP EDIT(
‘口座番号: ‘, ACC_ID,
‘ 演算結果(C): ‘, ACC_RESULT
) (A, A, A, F(10,2));
END;
END;
/ ファイルクローズ /
CLOSE FILE(ACCOUNT_FILE);
PUT SKIP LIST(‘正常終了しました。’);
END CALCEX;
—
3. ここがエンジニアの腕の見せ所:デバッグとコーディング標準
このコードのどこがポイントか、後輩のバカスカ打ったコードと何が違うのかを解説してやる。
① `ROUND` ビルトイン関数の積極的活用
コンパイラの暗黙の型変換に頼ると、コンパイラオプション(`TRUNC` や算術演算の精度モード)の差異によって、本番機(メインフレーム)とテスト機(AixやPC上のエミュレータ)で計算結果の端数処理が微妙にズレるという、悪夢のような現象を引き起こす。
異なる `V` 位置の演算結果を代入するときは、必ず `ROUND(expression, precision)` を使って、どこで丸めるかをプログラマが明示しろ。これがメインフレームエンジニアのプライドだ。
② `FIXEDOVERFLOW` ONユニットの常時配備
仮想小数を掛け合わせると、一時的に桁数が爆発的に増える。例えば `PIC ‘999V99’`(5桁)同士を掛けたら、理論上は整数部6桁・小数部4桁の器が必要になる。
もし左辺の変数の桁数が足りていない場合、コンパイラは上位桁の切り捨て(Truncation)を行うが、これを野放しにすると、金融システムでは大スキャンダルになる。上記のコードのように `ON FIXEDOVERFLOW` を仕込んでおき、予期せぬ桁あふれを確実に検知できるようにしろ。
—
先輩からのメッセージ
「たかが小数点、されど小数点」だ。
COBOLからPL/Iへの移行案件や、レガシーバッチのチューニングにおいて、PICTURE ‘V’ の仕組みを理解していないプログラマは、コンパイラが出す警告メッセージ(Wレベルの診断)を無視し続け、最終的に本番データの金額不整合という爆弾を爆発させる。
次にコードを書くときは、「この演算でコンパイラはどこにシフト命令を挿入しているか」「左辺に代入される瞬間に、どの桁が切り捨てられているか」を頭の中でアセンブラの動きまで想像しながら手を動かせ。
お前ならできる。次の結合テスト、頼むぞ。
