おい、最近夜間バッチのABEND解析でハマって徹夜明けか?……まあ、座れ。コーヒーでも飲みながら聞いてくれ。
我々が日々向き合っているIBMメインフレームの基幹系システム、その裏側で何百万行ものPL/Iコードが唸りを上げて動いている。COBOL全盛の時代から生き残っている現場も多いが、計算処理の複雑さや文字列操作の優位性から、勘定系や保険の料率計算などのコアロジックには今でもPL/Iが選ばれ続けているんだ。
さて、今日はそのPL/Iの裏側、特にコンパイラ最適化の魔窟について話をしよう。テーマは`OPTIMIZE(2|3)`コンパイラオプションによるレジスタ割り当ての最適化だ。
「なんで本番稼働中のジョブがいきなりデータ例外(S0C7)を起こしたのに、ストレージダンプを見ても変数の値が変なんだ?」
「ソースコード通りの場所にブレークポイントを張ったのに、変数が消えてやがる……」
そんな経験はないか? それ、まさに今日のテーマである「高次最適化によるレジスタ割り当て」の罠に綺麗にハマっている証拠だ。現場のエンジニアなら絶対に知っておかなければならない、コンパイラとメモリの攻防について徹底的に解説しよう。
—
1. PL/Iの柔軟性と「予約語を持たない」言語仕様の代償
まず大前提として、PL/Iという言語の特殊性に触れておこう。
C言語やJavaとは異なり、PL/Iには「厳密な予約語(Reserved Words)」が存在しない。つまり、`IF`や`READ`といったキーワードであっても、プログラマが変数名として宣言してしまえるほどの懐の深さ(あるいは狂気)を持っている。
コンパイラは、ソースコードを解析する際に「これはキーワードか? それともユーザー定義の変数か?」を文脈から判断しなければならない。この柔軟な構文解析をくぐり抜けたコードを、IBMのPL/Iコンパイラ(Enterprise PL/I)はさらに極限まで効率化しようとする。それが最適化オプションだ。
コンパイル時に `OPTIMIZE(2)` または `OPTIMIZE(3)` を指定すると、コンパイラはCPUの汎用レジスタ(General Purpose Registers)を血眼になって割り当て始める。
「メモリ(ストレージ)へのアクセスは遅い。できる限りレジスタ上でデータを保持し、計算を完結させろ」――これが最適化エンジンの基本思想だ。
—
2. `OPTIMIZE(2|3)` がコード生成とデバッグに与える影響
開発環境(テスト環境)では `OPTIMIZE(0)` や `TEST` オプションをつけてコンパイルするのが普通だ。そのため、変数の値は指定したストレージ(ワーキング・ストレージやスタック)上に常に存在し、ダンプを取れば「その瞬間の変数の値」が綺麗に目視できる。
しかし、本番移行時に性能要件を満たすため、あるいは標準のビルドJCLの規定によって `OPTIMIZE(2)` や `OPTIMIZE(3)` が指定された途端、世界が一変する。
レジスタ常駐(Register Allocation)の恐怖
ループ処理や頻繁に参照される算術変数、インデックス制御変数は、メモリから毎回ロードされるのではなく、CPUのレジスタ内に固定的に保持される。
コンパイラが「この変数はもうメモリに書き戻さなくても、次の演算でレジスタの中身をそのまま使えばいいや」と判断した場合、メインメモリ上のその変数の領域は古い値のまま放置される。
これが何を意味するか分かるか?
もしそのプログラムが途中でABENDし、SYSUDUMPやCEEDUMPが出力されたとする。ダンプ解析ツール(IPCSなど)で該当の変数のメモリアドレスを覗いても、そこにあるのは「最後にメモリへ書き出された時点の古い値」であり、CPUが実際に処理に使っていた最新の値ではないのだ。
「あれ? ソースコードのロジックではここに `+1` されているはずなのに、ダンプの変数値が一致しないぞ! バグか?」
――違う。それはバグではなく、コンパイラが最適化という名の親切心を発揮した結果、君を迷宮に引きずり込んでいるだけなのだ。
—
3. 実践:VSAM入出力とONユニット、そして最適化の交差点
百聞は一見に如かず。実際のメインフレーム開発の現場を想定した、VSAM(KSDS)からのレコード読み込みと、エラー発生時のONユニット制御を含むPL/Iプログラムを見てみよう。
このコードには、あえて最適化の影響を受けやすいループカウンタやフラグ制御が含まれている。
1
———————————————————————-;
- 顧客マスタ(VSAM/KSDS)を順次読み込み、特定条件で演算を行うバッチ処理 ;
———————————————————————-;
CUST_BATCH: PROC OPTIONS(MAIN);
DCL CUST_FILE FILE RECORD SEQUENTIAL INPUT
ENV(VSAM);
DCL 1 CUST_REC,
5 CUST_ID CHAR(8),
5 CUST_STATUS CHAR(1),
5 CUST_BALANCE FIXED DEC(11,2);
DCL W_READ_CNT FIXED BIN(31,16) INIT(0); — 処理件数カウンタ
DCL W_ERR_FLAG CHAR(1) INIT(‘0’); — エラーフラグ
DCL MAX_LIMIT FIXED DEC(11,2) INIT(999999.99);
— 入出力異常(ENDFILE / 読み込みエラー)の捕捉用 ONユニット —;
ON ENDFILE(CUST_FILE)
BEGIN;
PUT SKIP LIST(‘ 顧客マスタの読み込みが正常終了しました ‘);
W_ERR_FLAG = ‘EOF’;
END;
ON ERROR
BEGIN;
PUT SKIP LIST(‘ 予期せぬシステムエラーが発生しました ‘);
/ デバッグ時のヒント: 最適化環境ではW_READ_CNTがレジスタにあり、 /
/ ここで参照する値がメモリ上のものと乖离している可能性がある。 /
PUT SKIP EDIT(‘現在の処理件数(参考値):’, W_READ_CNT) (A, F(10));
SIGNAL ERROR;
END;
— ファイルオープン —;
OPEN FILE(CUST_FILE);
— メイン処理ループ —;
READ FILE(CUST_FILE) INTO(CUST_REC);
DO WHILE (W_ERR_FLAG \= ‘EOF’);
— 処理件数のインクリメント(最適化のターゲットになりやすい) —;
W_READ_CNT = W_READ_CNT + 1;
— データのバリデーションと条件分岐 —;
IF CUST_STATUS = ‘1’ THEN DO;
— 残高の簡易計算(BUILTIN関数を活用した丸め処理) —;
CUST_BALANCE = ROUND(CUST_BALANCE 1.05, 2);
IF CUST_BALANCE > MAX_LIMIT THEN DO;
PUT SKIP EDIT(‘限度額超え顧客発見 ID:’, CUST_ID) (A, A(8));
END;
END;
— 次のレコード読み込み —;
READ FILE(CUST_FILE) INTO(CUST_REC);
END;
— 後処理 —;
CLOSE FILE(CUST_FILE);
PUT SKIP EDIT(‘総処理件数:’, W_READ_CNT) (A, F(10));
END CUST_BATCH;
このコードと最適化に関する実務上の注意点
1. `W_READ_CNT` のレジスタ常駐:
`DO WHILE` ループの中で毎回 `W_READ_CNT = W_READ_CNT + 1` が実行される。`OPTIMIZE(3)` では、このカウンタは完全に汎用レジスタ(例えば `R14` や `R15` など)の中に抱え込まれ、メモリ上の `W_READ_CNT` の領域は、ループを抜けるか他の重い処理が発生するまで更新されないことが多い。
2. ONユニット(例外処理)内での変数の参照:
もし、ループの途中でデータ例外(パック十進数の不正など)が発生し、`ON ERROR` のブロックに制御が飛んだとする。その際、コンソールやダンプに出力される `W_READ_CNT` は、レジスタからメモリへの書き戻しが行われていない直前の値、あるいは最悪の場合、初期値の `0` のままになっていることがある。「おい、処理が1万件進んでいるはずなのにカウンタが0だぞ! ロジックがおかしい!」と勘違いしてソースコードを何時間も睨みつけるのは、レガシー開発現場のあるあるの悲劇だ。
—
4. 現場のシニアが教える「ダンプ解析と最適化」の立ち回り
では、こうした `OPTIMIZE(2|3)` によるダンプ解析の難しさに直面したとき、我々シニアエンジニアはどうやって真実を見つけ出すのか? 実務で使えるノウハウを伝授しよう。
① 疑わしい変数は `VOLATILE` 属性を付与する
もし、非同期の割り込みやONユニット、あるいは外部サブルーチンから頻繁に参照・変更されるため、「絶対にレジスタにキャッシュしてほしくない(常にメモリ上の実体を参照・更新させたい)」という変数があるなら、PL/Iの `VOLATILE` 属性(※コンパイラのバージョンや言語仕様の拡張を確認すること)や、コンパイラ指令を活用する。これにより、コンパイラに対して「この変数は勝手にレジスタに最適化するな」と明示的な指示を出せる。
② テスト時は `NOTEST` ではなく `TEST(NOHOOK,SEP)` などを活用する
本番性能を出すために `OPTIMIZE(3)` が必須であっても、コンパイルオプションに `TEST` を組み合わせることで、最適化を行いつつもデバッグ情報をサイドファイル(SYSDEBUG等)に保持させることができる。完全なノーヒントのバイナリと戦うのとは、解析のスピードが文字通り10倍変わる。
③ ダンプを見る時は「レジスタコンテキスト」を追う
CEEDUMPやSYSUDUMPを読む際、メモリ上の変数値だけに頼ってはいけない。
ダンプの上部にある「Registers at the time of interrupt(割り込み時のレジスタ値)」をじっくり見るんだ。どのレジスタにどの変数のアドレスや値が入っていたかを逆算する。アセンブラの知識(マシー語の勘所)がここで活きてくる。PL/Iコンパイラがどの変数をどのレジスタ(例: GPR 5〜11)に割り当てたかは、コンパイルリスト(Listing)の Storage Map や Cross-Reference、さらには Optimized Code Listing を見れば一目瞭然だ。
—
おわりに:コンパイラを信用し、しかし過信するな
PL/Iの最適化オプション `OPTIMIZE(2|3)` は、リソースの限られたメインフレーム上でバッチ処理の経過時間を劇的に短縮してくれる強力な味方だ。しかしそれは同時に、プログラマが書いた「ソースコードの静的な姿」と、CPU上で実行される「動的な姿」の間に乖離を生む諸刃の剣でもある。
バッチが落ちたとき、画面の向こうのメモリを疑う前に、「コンパイラはこの変数をどこに隠した?」と疑える視点を持つこと。それが、真の意味でメインフレームのシステムアーキテクト、そしてPL/I使いとしての第一歩だ。
さあ、コーヒーを飲み干したら、先ほどのジョブのコンパイルリストを開いてStorage Mapを確認しに行こうぜ。健闘を祈る!
