おい、最近入った若手が「先輩、PL/Iって変数名に `IF` や `GOTO` って使えないんですか?」って聞いてきたんだよ。いやあ、思わずコーヒー吹きそうになったね。
現代のプログラミング言語なら当然「予約語」として構文解析で弾かれるところだが、我が愛するPL/Iの懐の深さ、あるいは狂気的なまでの自由度を知らない証拠だ。PL/Iには、いわゆる「固定の予約語」という概念が存在しない。`IF` も `GOTO` も、コンパイラはただの「文脈依存のキーワード」として見ている。だから、変数名に `IF` と名付けても文法エラーにはならない。……まあ、そんなコードを書いたら、後からコードを読む夜間バッチの保守担当者から呪い殺されること確実だがな。
さて、前置きはこれくらいにしておこう。今回は、基幹システムの現場で浮動小数点数(FLOAT)を弄る際によく直面する `TRUNC` 関数の整数部切り出しと、その裏で行われているレジスタ操作の泥臭い真実 について、徹底的に解説してやる。しっかりついてこいよ。
—
1. 浮動小数点数と `TRUNC` の罠:なぜ現場で整数化に苦労するのか?
金融や流通の基幹システムでは、金利計算や複雑なレート換算で `FLOAT` 型(単精度・倍精度)を使わざるを得ない場面がある。しかし、最終的な結果を帳票に出力したり、VSAMのキー項目に組み込んだりするときには、どうしても「小数点以下を切り捨てた整数(FIXED BINARY あるいは DECIMAL)」に戻さなければならない。
ここで若手がやりがちなミスが、安易な型変換(暗黙の型変換)の放置だ。
「PL/Iが勝手にやってくれるっしょ」なんて甘い考えでいると、コンパイラが吐き出す機械語命令の裏で、意図しない丸め誤差や、パフォーマンスの致命的な劣化を招く。
ここで登場するのが、`TRUNC` ビルトイン関数だ。
`TRUNC` 関数の正しい理解
`TRUNC(x)` は、数学的な床関数(フロア)や四捨五入とは違う。「ゼロ方向への切り捨て(Truncation towards zero)」を行う。つまり、正の数であれば小数点以下切り捨て、負の数であれば小数点以下「切り上げ(ゼロに向かって丸める)」になる。
しかし、IBMメインフレーム(z/Architecture)のハードウェアレベルで見ると、浮動小数点レジスタ(FPR)にあるデータを、そのまま汎用レジスタ(GPR)の固定小数点数としてロードすることはできない。浮動小数点数の内部表現(IEEE 754 または IBM独自形式の 16進浮動小数点)から、指数部をマスクし、仮数部を整数の形にストレージあるいはレジスタ上で再配置するプロセスが必要になるのだ。
—
2. 実践コード:`TRUNC` と最適化されたレジスタ操作
百聞は一見にしかずだ。実際のバッチプログラムを想定したPL/Iのコードを見せよう。
大文字ベースの記述、適切なインデント、そして `BUILTIN` 属性の明示。これぞプロのメインフレームエンジニアの作法だ。
1
- 模範コーディング例: 浮動小数点数の整数化とレジスタ制御
TRUNCSample: PROC OPTIONS(MAIN);
DCL Wk_Float FLOAT DEC(16) INIT(12345.6789); / 倍精度浮動小数点数 /
DCL Wk_Int_Result FIXED BIN(31) INIT(0); / 結果格納用 32ビット整数 /
DCL Wk_Msg CHAR(80) VAR;
/ BUILTIN宣言の明示(コンパイラの誤解釈を防ぐ現場の防衛策) /
DCL TRUNC BUILTIN;
DCL FIXED BUILTIN;
ON ERROR
BEGIN;
PUT SKIP LIST(‘ 致命的な演算エラーが発生しました ‘);
EXIT;
END;
/ 1. 浮動小数点数からTRUNC関数で整数部を抽出 /
/ ハードウェア上ではFPR(浮動小数点レジスタ)間での演算が走る /
Wk_Float = TRUNC(Wk_Float);
/ 2. FIXEDビルトインで明示的に固定小数点(整数)へ変換 /
/ ここで内部的に指数部の解析とマスク処理、GPR(汎用レジスタ)への転送が行われる /
Wk_Int_Result = FIXED(Wk_Float, 31, 0);
/ 結果の出力確認 /
Wk_Msg = ‘変換後の整数値 = ‘ || TRIM(Wk_Int_Result);
PUT SKIP LIST(Wk_Msg);
/ 負数の場合の挙動確認(ゼロ方向への切り捨ての確認) /
Wk_Float = -98765.4321;
Wk_Float = TRUNC(Wk_Float);
Wk_Int_Result = FIXED(Wk_Float, 31, 0);
Wk_Msg = ‘負数変換後の整数値 = ‘ || TRIM(Wk_Int_Result);
PUT SKIP LIST(Wk_Msg);
END TRUNCSample;
—
3. アーキテクチャの裏側:コンパイラが生成する機械語とレジスタの動き
さて、上記のコードがIBMの z/OS 上でコンパイルされ、実行されるとき、ハードウェアの内部では何が起きているか? ここを知っているかどうかが、一人前のシステムアーキテクトと、ただコードを動かしているだけの素人の分かれ道だ。
1. 浮動小数点演算 (FPRの占有)
`Wk_Float = TRUNC(Wk_Float);` の行では、コンパイラ(Enterprise PL/Iコンパイラなど)は浮動小数点荷重命令(例えば `LD` や `AXR` などの類、あるいはアーキテクチャに応じたFIXED/FLOAT変換命令)を生成する。この時点ではデータはまだFPR(浮動小数点レジスタ、FPR 0, 2, 4, 6など)の中に閉じ込められている。
2. 指数部のマスクとスケーリング
PL/Iの `FIXED` 変換が絡むと、コンパイラは浮動小数点の「指数部(Exponent)」を剥ぎ取り、符号ビットを考慮しながら、仮数部を整数として扱えるようにシフト演算やマスク処理を行う機械語列を組み立てる。
3. 汎用レジスタ(GPR)への転送
最終的に `FIXED BIN(31)` の変数やレジスタに値を落とし込む際、データは FPR から GPR(汎用レジスタ、R0〜R15) へ転送される。ここで使われるのが、おなじみの `CDFR` (Convert to Fixed-Point Double-Word Register) や、近年の z/Architecture であれば `CXGB` などの強力なマシンインストラクションだ。
もし、このレジスタ間の移動(FPR ⇄ GPR)を無駄に何回も発生させるようなヘタクソなコーディング(例えば、ループの中で無駄に型変換を繰り返すなど)をすると、CPUサイクルの無駄遣いとなり、夜間バッチ全体の処理時間が数十分単位で膨れ上がる。メインフレームの世界では、これが「バッチ窓(処理時間制限)からの溢れ」という致命傷につながるんだ。
—
4. 保守現場からのアドバイス:デバッグとコーディングの鉄則
最後に、現場で後輩によく言う「三箇条」を授けておく。
- `BUILTIN` 属性は面倒くさがらずに書け
デフォルトでビルトイン関数とみなされるからといって、`DCL TRUNC BUILTIN;` をサボるな。コンパイラオプションの変更やインクルードファイルの競合で、ユーザー定義関数と誤認されたときのデバッグ地獄は、二度と味わいたくないはずだ。
- データ例外(S0C7)への備えを忘れるな
VSAMや外部ファイルから読み込んだパック十進数(COMP-3)やゾーン十進数を、一度もバリデーションせずにいきなりFLOATに突っ込んでTRUNCをかますと、データ異常時に容赦なくシステム異常終了(ABEND: S0C7)が飛んでくる。ON-UNIT(特に `ON CONVERSION`)を適切に配置して、異常系を優しく拾う設計にしろ。
- 無駄な型変換ループを排除せよ
ループ内で `FLOAT` と `FIXED` を行ったり来たりさせるな。計算のフェーズと、入出力・格納のフェーズは完全に分離し、レジスタのロード/ストア回数を最小限に抑えるのが、優秀なアーキテクトの仕事だ。
レガシーシステムの寿命は長い。私たちが書く一本のPL/Iコードが、これからの日本のインフラを何十年も支え続ける。その重みを背負って、明日からのコーディングに臨んでくれ。期待しているぞ。
