おい、最近オンラインでC#やJavaばかり触っている若手が、「メインフレームのPL/Iって、なんで変数の定義順序でレコードの長さが変わるんですか?」なんて質問しに来たんだよ。
笑っちまうかもしれないが、これ、笑い事じゃねぇんだ。バッチのマイグレーションや、VSAMファイルをオープンした瞬間に「OCCURS DEPENDING ON」やオフセットズレでバグる原因の九割は、このメモリ配置とアライメントを理解していないことから起きる。
今日はな、PL/Iにおける `FIXED BINARY(15)` と `FIXED BINARY(31)` が、IBMメインフレームのハードウェア(Zアーキテクチャ)上でどうメモリを占有し、アライメント(境界調整)によってどのようなパディング(隙間)を生み出すのか、俺が骨の髄まで叩き込んでやる。心して聞け。
—
1. なぜ「予約語がない」PL/Iでアライメントが重要なのか
PL/Iには、COBOLのような「ガチガチの予約語の呪縛」が比較的少ない。変数名に `FIXED` や `BINARY` すら使おうと思えば使えてしまう(コンパイラが文脈で判断するからおすすめはしないがな)ほど、柔軟な言語だ。
しかし、その柔軟さの裏で、コンパイラはハードウェア(IBM z/Architecture)が最も効率よくデータを読み書きできるように、メモリ上の配置を裏でこっそり最適化している。これがアライメント(境界調整)だ。
- `FIXED BINARY(15)`(ハーフワード / 2バイト整数):
メモリ上の「2の倍数(偶数番地)」に配置されなければならない。奇数番地からロードしようとすると、ハードウェア側で例外(スロット違反)が起きるか、数サイクル余分な処理を食う。
- `FIXED BINARY(31)`(フルワード / 4バイト整数):
メモリ上の「4の倍数番地」に配置されなければならない。
この物理的な制約があるため、構造体(`STRUCTURE`)の中でこれらを混在させて定義すると、コンパイラが「おっと、ここはアドレスを4の倍数に合わせるために隙間を開けなきゃな」と、自動的にパディング(無駄なバイト)を挿入するのさ。
—
2. 構造体におけるパディング発生のメカニズム
百聞は一見にしかずだ。以下のソースコードを見てくれ。
メインフレームの夜間バッチでよく見る、顧客マスタのレコードレイアウトを模した定義だ。
DCL 1 CUST_REC_BAD,
5 CUST_ID FIXED BIN(15), / 顧客ID (2バイト) /
5 CUST_NAME CHAR(10), / 顧客名 (10バイト) /
5 CUST_POINT FIXED BIN(31); / 保有ポイント (4バイト) /
一見すると、`2 + 10 + 4 = 16バイト` のレコード長になりそうだと思うだろ?
甘い。甘すぎるんだよ、兄ちゃん。
実際のメモリ上の配置はこうなる:
1. `CUST_ID`(`FIXED BIN(15)`)は2バイト。偶数番地(例: 0番地)からスタート。消費:0〜1番地。
2. 次の `CUST_NAME`(`CHAR(10)`)は1バイト文字の連続だから、そのまま3番地から12バイト分(3〜12番地)配置できる……と、思いきや!
3. その次にある `CUST_POINT` は `FIXED BIN(31)`(4バイト整数) だ。これは4の倍数番地から始まらなければならない。
4. その結果、コンパイラは `CUST_NAME` の直後に、1バイトのパディング(隙間)を勝手にブチ込む。さらに、構造体全体のサイズも、最大の要素である4バイト境界に合わせるため、末尾にもパディングが入ることがある。
結果として、意図せずレコード長が膨らみ、COBOL側で作った外部ファイルとの間で「OCURRED 0C4」や「データ切り捨て」の大惨事が起るわけだ。
—
3. 実践:アライメントを制御したスマートなコーディング
では、どうすればいいのか?
答えは簡単だ。「サイズの大きい順、あるいはアライメントの厳しい順に並べる」、そして「必要に応じて `ALIGNED` または `UNALIGNED` 属性を明示する」ことだ。
現場で後輩によく教える、修正後の模範的なコードを見せてやろう。
/ ————————————————– /
/ 顧客レコード定義(アライメント最適化モデル) /
/ ————————————————– /
DCL 1 CUST_REC_GOOD UNALIGNED,
/ 最も境界条件の厳しい(あるいは大きい)フィールドを前へ /
5 CUST_POINT FIXED BIN(31), / 保有ポイント (4バイト) /
5 CUST_ID FIXED BIN(15), / 顧客ID (2バイト) /
5 CUST_NAME CHAR(10); / 顧客名 (10バイト)/
/ ————————————————– /
/ 処理ロジック部分 /
/ ————————————————– /
PROCESS_CUSTOMER: PROC OPTIONS(MAIN);
DCL WS_IO_AREA CHAR(16) BASED(ADDR(CUST_REC_GOOD));
DCL 1 LOG_MSG,
3 FILLER CHAR(5) INIT(‘LEN=’),
3 REC_LEN FIXED BIN(31) INIT(0);
/ 構造体の実際のバイト数を取得(UNALIGNEDの効果を確認) /
REC_LEN = STORAGE(CUST_REC_GOOD);
DISPLAY(‘— 構造体サイズ診断 —‘);
DISPLAY(‘CUST_REC_GOOD の実サイズ: ‘ || TRIM(CHAR(REC_LEN)));
/ VSAMからの読み込みを想定したデータ移動 /
/ ※実際の現場ではREAD FILE(…) INTO (…)を使用する /
RETURN;
END PROCESS_CUSTOMER;
コードのポイントと現場の知見
1. `UNALIGNED` 属性の威力
構造体全体の定義に `UNALIGNED` を指定すると、コンパイラに対して「パディングを極力排除し、バイト単位で詰めて配置せよ」と指示できる。外部ファイル(VSAMやQSAM)とのI/Oレイアウトを厳密に一致させたい時には必須のテクニックだ。
2. `STORAGE` ビルトイン関数の活用
「この構造体、何バイトだっけ?」と迷ったら、`STORAGE(変数名)` を使え。実行時ではなくコンパイル時にサイズが確定するものはこれで一発でわかる。バッチのabend解析でストレージダンプを睨む前段階で、これを出力するロジックを入れておくだけで救われる命がある。
3. パフォーマンスとのトレードオフ
`UNALIGNED` はメモリを節約し外部ファイルとの互換性を保つ反面、奇数番地にある `FIXED BIN(31)` にアクセスする際、CPUは内部で余計なメモリアクセスサイクルを消費する。
大原則: ループ内で何百万回も高速演算するワークエリア内の変数は `ALIGNED`(デフォルト)で高速化を図り、ファイル入出力に直結するレコード構造体は `UNALIGNED` でレイアウトを死守する。この使い分けができるかどうかが、一人前のPL/Iエンジニアの分水嶺だ。
—
4. ベテランからの現場の教訓
メインフレームの世界はシビアだ。C#やJavaのように、メモリ管理をランタイムやGC(ガベージコレクション)が勝手に綺麗にしてくれるわけじゃない。私たちが書いたコードの通りのバイト列が、そのままハードウェアに解釈される。
もし君が今度、既存のPL/Iプログラムのフィールド追加改修を頼まれたら、安易に構造体の末尾に `FIXED BIN(31)` をポツンと追加するな。
「おい、そこパディング挟まるぞ」「VSAMの定義(COBOLコピーブック)とオフセットがズレるぞ」と、一瞬で気づける眼光を持ってほしい。
基本を軽んじる者はお手馬のバグに足元をすくわれる。今日の話を胸に刻んで、次のデバッグに臨んでくれ。それじゃ、続きの作業に戻るとするか。
