【実務・中級編】INITIAL属性による変数初期化のタイミングと最適化 – PL/Iの基本構文とデータ制御実践ガイド

おい、調子はどうだ?
今日もどこかのジョブネットで、膨大なVSAMファイルが轟音を立てて回っている頃だな。お前が今手掛けているそのメインフレームのバッチ画面、あるいはマイグレーションの調査、なかなか一筋縄ではいかない泥臭い世界だろう。

今回はな、PL/Iの「INITIAL属性による変数初期化のタイミングと最適化」について、現場の血肉となった知見を叩き込んでやろうと思う。

巷の入門書やIBMのマニュアルを開けば、「変数の初期値を定義するものだ」と、すまし顔で書いてある。だがな、実際の夜間バッチの巨大なJCLと結びついたとき、その「初期化」という甘い言葉が、どれほど恐ろしいパフォーマンス低下や、あるいは予期せぬ「ゴミデータ」の残存を引き起こすか、お前は本当に理解しているか?

今日はその裏側にあるコンパイラの挙動から、VSAMやONユニットが絡む実務的な罠まで、徹底的に紐解いてやる。心して聞け。

1. 予約語を持たないPL/Iと識別子の世界、そしてINITIALの基本

まず大前提として、PL/Iという言語の気高い特徴を思い出しておこう。
C言語やJavaとは違い、PL/Iには厳密な意味での「予約語(Reserved Word)」が存在しない。`IF`であろうが`THEN`であろうが、極端な話、変数名として使うことすら文脈によっては許されてしまう(もちろん、そんな狂ったコーディングをしたらコードレビューで俺にぶん殴られるがな)。コンパイラは文脈から「あ、ここはキーワードだな」「ここはユーザー定義の識別子だな」と判断している。

そんな自由度の高いPL/Iにおいて、変数の宣言時に値をブチ込むのが `INITIAL`(または `INIT`)属性だ。

1
DCL WK-COUNT FIXED BIN(31) STATIC INIT(0);
DCL WK-NAME CHAR(30) STATIC INIT(‘ ‘);

この `INITIAL`、実はいつメモリ上にその値がセットされるかという「タイミング」が、ストレージクラス(`STATIC` なのか `AUTOMATIC` なのか)によって全く異なる。ここを勘違いしていると、オンラインから呼び出されるサブプログラムや、再入可能性(Reentrancy)が求められるモジュールで致命傷を負うことになる。

2. INITIALの初期化タイミング:STATIC と AUTOMATIC の決定的な違い

現場で最もやりがちなミスが、自動変数(`AUTOMATIC`:デフォルト)に対する `INITIAL` の過信だ。

① STATIC ストレージの場合

`STATIC` は、プログラムがロードされてから終了するまで、メモリ上の物理アドレスが固定される。

  • 初期化のタイミング: プログラムが最初に対象ブロックへ制御を移す時(つまり、モジュールのロード時・最初の1回のみ)に、ロードモジュール(PDS/PDSEのメンバー)内に焼き付けられた初期値がそのままメモリに転写される。
  • 2回目以降の呼び出し: 初期化は行われない。前回処理を抜けた時の値がそのまま保持される。ここを忘れていると、カウンター変数を初期化したつもりが、前回の残骸を引き継いで暴走するバグの温床になる。

② AUTOMATIC ストレージの場合

`AUTOMATIC` は、ブロック(PROCEDUREやBEGINブロック)に入った時にスタック上に動的に領域が割り当てられ、ブロックを抜ければ消滅する。

  • 初期化のタイミング: ブロックに侵入し、領域が割り当てられる都度、INITIALの値がセットされる。
  • パフォーマンス上の代償: 「じゃあ毎回安全に初期化されるから便利じゃん」と思ったそこのお前、甘い。`AUTOMATIC` 変数に複雑な初期化や配列の初期化(`INIT((10)0)` など)を記述すると、ブロックに入るたびにCPUが初期化のためのデータ転送(インラインコードの展開やループ生成)を実行する。これが大規模なループ内のBEGINブロックや、数百万件を処理するレコード入出力のサブループ内であれば、立派な性能ボトルネック(CPUオーバーヘッド)に化けるのだ。

3. コンパイラ最適化の魔力:INITIALは省略される?

近年のIBM Enterprise PL/Iコンパイラ(および高度な最適化オプション `OPT(2)` や `OPT(3)`)は、我々が想像している以上に賢く、そして冷酷だ。

もしお前が、以下のようなコードを書いたとする。

1
DCL WK-BUFFER CHAR(100) AUTOMATIC;
WK-BUFFER = ‘HELLOWORLD’;

`INITIAL` を書き忘れたからといって、コンパイラが親切にブランクやバイナリゼロでこの領域を綺麗に埋めてくれると期待していないか?
大間違いだ。 `AUTOMATIC` 変数に `INITIAL` が指定されていない場合、メモリ領域が確保された直後の内容は「前回そこに何が入っていたか分からないゴミ(不定値)」のままだ。

さらに、最適化コンパイラはこう判断する。
「どうせこの後すぐに全桁への代入(`WK-BUFFER = …`)が行われるんだから、わざわざ `INITIAL` で事前にブランクを埋めるコスト(命令の実行)は無駄だな。省略しちまえ!」

――これが、コンパイラ最適化による初期化の省略だ。
理にかなっているが、もしこの `WK-BUFFER` の一部を、代入前に何らかの条件分岐で参照してしまったり、VSAMのレコード構造体(`BASED` や不完全な代入)に絡めたりした場合、一瞬で「S0C4(異常終了)」や「不正データ例外」の泥沼へ引きずり込まれる。

4. 実践:VSAMアクセスとONユニットが交錯する堅牢なコード

百聞は一見にしかずだ。実際の基幹系バッチを想定した、実用的なPL/Iコードを見せてやろう。
VSAM(KSDS)からのレコード読み込みを行い、データ例外やファイル終了(ENDFILE)を `ONユニット` で安全にハンドリングしつつ、初期化のコストを最適化した構成だ。

1
——————————————————————

  • メインフレーム基幹バッチ:VSAM顧客マスタ順次読み込み処理

——————————————————————
CUSTIN: PROC OPTIONS(MAIN);

— VSAMファイル(KSDS)のファイル宣言 —
DCL CUSTFILE FILE RECORD SEQUENTIAL
ENVIRONMENT(VSAM);

— 顧客レコード構造体(ストレージの明確な定義) —
DCL 1 CUST-REC,
5 CUST-ID CHAR(8),
5 CUST-NAME CHAR(40),
5 CUST-BALANCE FIXED DEC(9,2);

— 制御用変数の定義(STATICで静的に初期化を保証) —
DCL EOF-FLAG CHAR(1) STATIC INIT(‘N’);
DCL W-READ-COUNT FIXED BIN(31) STATIC INIT(0);

— 組み込み関数(BUILTIN)の明示的宣言 —
DCL (SYSNULL, LENGTH) BUILTIN;

— ONユニット:ファイル終了(ENDFILE)の捕捉 —
ON ENDFILE(CUSTFILE)
BEGIN;
EOF-FLAG = ‘Y’;
END;

— ONユニット:データ例外(CONVERSION等)の安全網 —
ON CONVERSION
BEGIN;
DISPLAY(‘【警告】数値変換エラー発生:不正なレコードをスキップします’);
— 本来はここでエラーログ出力や異常フラグを立てる —
END;

— ファイルのオープン —
OPEN FILE(CUSTFILE) INPUT;

— 初回読み込み(リード・アヘッド処理) —
READ FILE(CUSTFILE) INTO(CUST-REC);

— メイン処理ループ —
DO WHILE(EOF-FLAG = ‘N’);

— 読み込みカウンタのインクリメント —
W-READ-COUNT = W-READ-COUNT + 1;

— ここでレコード処理を行う(例として残高チェック) —
IF CUST-BALANCE < 0 THEN DO; DISPLAY('マイナス残高検知 ID: ' || CUST-ID); END; -- 次レコードの読み込み -- READ FILE(CUSTFILE) INTO(CUST-REC); END; -- 終了処理 -- CLOSE FILE(CUSTFILE); DISPLAY('正常終了:総読み込み件数 = ' || TRIM(W-READ-COUNT)); RETURN; END CUSTIN; このコードのポイントを解説しておこう。 1. `STATIC INIT(…)` の活用:
`EOF-FLAG` や `W-READ-COUNT` はプログラム全体で状態を維持すべきものであるため、`STATIC` を明示し、意図通り最初に一度だけ `0` や `’N’` で初期化されるようにしている。
2. ONユニットによる制御フロー:
VSAMの終端制御を `ENDFILE` 割り込みでトラップし、フラグを切り替える。レガシーシステムの王道だが、最も確実なハンドリング手法だ。また、VSAMから読み込んだ数値フィールド(`FIXED DEC`)に空白やゴミが入っていた場合に備え、`ON CONVERSION` を張ることで、システム全体が即座にU0000などで墜落するのを防いでいる。
3. AUTOMATIC領域の排除と無駄な初期化の回避:
レコード構造体 `CUST-REC` は `READ … INTO` によって毎回完全に上書きされる。そのため、この構造体に無駄な `INITIAL` 属性を付与していない。コンパイラに余計な初期化コードを吐かせず、I/Oバッファからの転送効率を最大限に高めているというわけだ。

5. シニアアーキテクトからの教訓:デバッグとコーディングの鉄則

最後に、現場で後輩によく言う鉄則を授けておく。

  • 「初期化してあるはず」という思い込みを捨てろ:

dumpリスト(SYSABENDやCEEDUMP)を眺めるとき、`AUTOMATIC` 変数が初期化されておらず、前回のゴミを抱えているせいで不正なアドレスを指しているバグに何度泣かされたことか。特にポインタやベース変数(`BASED`)を使うときは、領域を割り当てたら(`ALLOCATE`)、自前のロジックでクリアするか、確実に値を詰める癖をつけろ。

  • INITIALとALLOCATEの同時使用に注意せよ:

`ALLOCATE` 文で動的変数にメモリを割り当てる際も `INITIAL` は有効だ。
1
DCL P_REC RECORD_TYPE POINTER;
ALLOCATE RECORD_TYPE SET(P_REC);

この時も、構造体側に `INIT` があれば、割り当てられた瞬間に値がセットされる。だが、動的割り当てのたびに初期化処理走るため、高頻度な `ALLOCATE` / `FREE` を繰り返す設計は、メインフレームのストレージ管理(サブプール)に深刻な断片化と性能劣化をもたらす。本当に動的割当てが必要か、静的(STATIC)なプールで回せないか、常にアーキテクチャの視点を持て。

PL/Iは古い言語だと言われる。だが、ハードウェアの特性を極限まで引き出し、巨大なデータを高速に処理するための思想が、この言語の隅々にまで染み込んでいる。コンパイラが裏で何を考え、どうコードを最適化しているか――その解像度を上げることで、お前の書くコードは一級品の信頼性を手に入れるはずだ。

さて、講義はここまでだ。
溜まっているJCLのチェックに戻るとしようか。何か詰まったら、いつでも俺の席に聞きにこい。

タイトルとURLをコピーしました