やあ、今日も深夜のバッチウィンドウの向こう側で悪戦苦闘しているかい?
メインフレームの保守現場にいると、時々「動いているはずのプログラムが、突然変なデータを吐き出した」「他機種からのマイグレーション途中で、数値の切り捨て方が変わって計算が合わなくなった」なんていう、冷や汗もののトラブルに直面するよな。
今日は、そんなトラブルシューティングの現場で若手からよく質問される、PL/Iの奥深くてちょっと厄介な世界、「算術演算における `LIMITS` と `TRUNC` コンパイラオプションの影響」について、俺の実務経験を交えてじっくりと語ろうと思う。
PL/Iという言語の懐の深さ、そしてコンパイラの挙動を甘く見たときに足元をすくわれる恐怖を、骨の髄まで理解してもらうためのセッションだ。心して聞いてくれ。
—
1. 予約語を持たないPL/Iと識別子の世界
本題に入る前に、PL/Iのユニークな基本性質に少し触れておこう。
C言語やCOBOLなんかを触ってきたプログラマが最初の一歩で驚くのが、「PL/Iには厳密な意味での予約語(Reserved Words)が存在しない」という点だ。
どういうことか? `IF` や `THEN`、さらには `READ` や `WRITE` といったキーワードですら、コンテキスト(文脈)によってただの「識別子(変数名)」として定義できてしまう。
1
/ 狂気の変数宣言の例:DOという名前の変数が作れてしまう /
DCL DO FIXED BIN(31);
DCL IF FLOAT;
DO = 10;
IF = 3.14;
こんなコード、実際の現場で書いたらコードレビューで大目玉をくらうどころか、リジェクト不可避の危険な代物だが、構文規則上はエラーにならない。コンパイラは前後の文脈を完璧に解釈して、「あ、ここで出てきた `DO` は変数だな」と判断する。
この「文脈依存性」の裏返しとして、PL/Iのコンパイラは非常に高度な型推論とデータ制御を行っている。その最たるものが、数値計算における精度(Precision)と、今回深掘りする `TRUNC` オプション の挙動なんだ。
—
2. TRUNCオプションの正体:なぜ計算結果が変わるのか?
メインフレームでCOBOLからPL/Iへ移行したプロジェクトや、大規模なバージョンアップ(Enterprise PL/Iへの移行など)の際、最もハマりやすいのがこの `TRUNC`(Truncation)コンパイラオプションだ。
FIXED BINARY(固定小数点二進数、いわゆるフルワードやハーフワード)の変数を扱うとき、コンパイラが生成する機械語コード(マシンインストラクション)の振る舞いを決定づけるのが、この `TRUNC` である。
設定できる値には主に以下の3つがある。
- `TRUNC(STD)`(Standard)
言語仕様(Language Reference)に忠実なモード。宣言された精度(Precision)の桁数を超えないように、算術演算のたびに切り捨て(または桁あふれチェック)を行う。
- `TRUNC(OPT)`(Optimize)
パフォーマンス最優先モード。ハードウェアレジストリのサイズ(32ビットなら32ビットフル)まで目一杯使って演算し、余計な切り捨てを行わない。
- `TRUNC(BIN)`(Binary)
COBOLの `TRUNC` オプションの概念に似せて作られた互換モード。定義された精度(例: `FIXED BIN(5)` なら最大32未満)を超える値が格納された際の振る舞いを保証・制御する。
これが実務でどう影響するか?
例えば、VSAMファイルやデータベースから読み込んだデータを加工する際、`TRUNC(OPT)` でコンパイルされたプログラムは、レジストリ溢れを起こしたときに意図せぬゴミデータを保持したまま次の計算に進んでしまい、後続のIF文の条件判定で思わぬ不具合を引き起こすことがある。
—
3. 実践コード:VSAMアクセスとONユニット、そして算術演算の罠
百聞は一見に如かず。実際のバッチ処理を想定したPL/Iのコードを見てみよう。
VSAM(KSDS)からレコードを読み込み、数量と単価を掛け合わせて金額を計算、データ異常が発生した際にはONユニットでトラップする、という現場の王道パターンだ。
1
CALC_DEMO: PROC OPTIONS(MAIN);
/ — 1. 宣言部 — /
/ 入力用VSAMレコード構造体 /
DCL 1 IN_REC,
5 ITEM_ID CHAR(6),
5 RAW_QTY CHAR(5), / 外部表現の数量 (数値文字) /
5 RAW_PRICE CHAR(7); / 外部表現の単価 (数値文字) /
/ 演算用変数(あえてFIXED BIN(15)の短精度にしてTRUNCの影響が出やすくする) /
DCL W_QTY FIXED BIN(15)
VALUE(0);
DCL W_PRICE FIXED BIN(31)
VALUE(0);
DCL W_TOTAL FIXED BIN(31)
VALUE(0);
DCL EOF_FLAG BIT(1)
INIT(‘0’B);
/ — 2. ONユニット(例外処理)の定義 — /
/ 算術オーバーフローや数値変換エラーを捕捉する /
ON OVERFLOW
BEGIN;
PUT SKIP LIST(‘ 警告: 算術オーバーフローが発生しました ‘);
/ 必要に応じてエラーログ出力や異常終了コード設定を行う /
END;
ON CONVERSION
BEGIN;
PUT SKIP LIST(‘ エラー: 数値データ変換に失敗しました Item = ‘, ITEM_ID);
W_TOTAL = 0;
END;
/ — 3. ファイルオープン(VSAM) — /
OPEN FILE(IN_FILE) INPUT;
/ メインループ /
DO WHILE(^EOF_FLAG);
READ FILE(IN_FILE) INTO(IN_REC);
IF EOF_FLAG THEN LEAVE;
/ 文字列から二進数への暗黙的変換と算術演算 /
/ TRUNC(OPT) の環境下では、ここでレジストリの桁あふれ挙動に注意が必要 /
W_QTY = RAW_QTY;
W_PRICE = RAW_PRICE;
/ 乗算の実行:ここでコンパイルオプションがモノを言う /
W_TOTAL = W_QTY W_PRICE;
/ 組み込み関数(BUILTIN)を使った安全なチェック処理 /
IF W_TOTAL > 9999999 THEN
BEGIN
PUT SKIP LIST(‘高額取引検出: Item = ‘, ITEM_ID, ‘ Total = ‘, W_TOTAL);
END;
END;
CLOSE FILE(IN_FILE);
RETURN;
END CALC_DEMO;
このコードのポイントと「TRUNC」の罠
1. 暗黙のデータ型変換 (`W_QTY = RAW_QTY`)
`CHAR(5)` から `FIXED BIN(15)` への代入時、PL/Iは内部で自動的にDECIMALを経由した変換を行う。このとき、もし `TRUNC(STD)` が指定されていると、変換のたびに厳密な桁数チェックと丸めが行われ、CPUサイクルのオーバーヘッドになる。逆に `TRUNC(OPT)` であれば、余計な命令が生成されないため高速だが、もし入力データが定義桁数を超えていると、後続の計算で予期せぬ値化けを起こす。
2. ONユニット (`ON OVERFLOW`, `ON CONVERSION`) の挙動
メインフレームの基幹系バッチでは、異常系でバッチ全体をアボート(異常終了)させるか、レコードをスキップして処理を継続させるかの判断が極めて重要だ。
`TRUNC(STD)` ではハードウェアレベルの例外がソフトウェア例外(`OVERFLOW`)として正しく拾いやすくなる一方、`TRUNC(OPT)` では最適化の過程で条件分岐がインライン展開され、意図したタイミングでONユニットが発火しないケースが稀にある。ここが、移行プロジェクトでベテランが頭を抱えるポイントだ。
—
4. パフォーマンス最適化とアーキテクトからの提言
じゃあ、俺たちシステムアーキテクトは、この `LIMITS` と `TRUNC` とどう向き合えばいいのか?
現場で後輩によく指導している鉄則をいくつか授けよう。
- 新規開発・完全移行なら基本は `TRUNC(OPT)` か、デフォルトのモダン設定を信じる
現代の z/Architecture(z14やz15、z16など)のハードウェア性能は凄まじい。無駄な桁あふれチェックを省く `TRUNC(OPT)` は、何百万件も回る巨大なループ処理において、バッチ全体の処理時間を数パーセント削る強力な武器になる。
- 他言語(COBOLなど)とのインターフェースやレガシー資産の流用時は `TRUNC(BIN)` を疑え
「COBOLで作られた古いサブプログラムとデータを共有している」「外部から渡ってくるバイナリデータの精度が曖昧だ」というモジュール群に `TRUNC(OPT)` を適用すると、ある日突然、最上位ビットの解釈違いによる「負数の化け」という悪夢を見る。こういう場所では、互換性を維持するためにコンパイル時オプションを厳格に切り分けること。
- 変数の定義(Precision)は必要最小限にせず、基本は `FIXED BIN(31)` を使え
昔のメモリが貴重だった時代名残で、小さな数字だからと `FIXED BIN(15)` や `FIXED BIN(7)` を乱用するプログラマがいるが、現代のメインフレームでは32ビットフルワード(`FIXED BIN(31)`)で処理する方がCPUのレジストリ効率が良い。無駄に小さなサイズを宣言し、かつ `TRUNC(STD)` を強制すると、コンパイラが余計なマスク命令(AND命令などによる桁数制限)を大量に生成し、かえってパフォーマンスが落ちる。
—
おわりに
PL/Iは、書き手の意図を驚くほど素直に機械語に翻訳してくれる、実に懐の深い言語だ。しかしそれは裏を返せば、「コンパイラオプションの挙動を理解していないと、プログラマの意図しない挙動をシレッと生成する」という危険性と表裏一体であるということ。
トラブルシューティングで夜中に呼び出されたとき、コンパイルリストのプロローグに出力されるオプション一覧(`TRUNC`, `LIMITS`, `SUF` など)をパッと見て、「あ、ここが原因だな」と見抜けるカッコいいエンジニアになってくれよ。
さて、コーヒーブレイクはここまでだ。そろそろ次のジョブネットの確認時間だな。
お互い、今日も安定稼働を勝ち取ろうぜ!
