おい、最近配属された若手が、夜間バッチの性能劣化の原因調査で頭を抱えていたんだ。「先輩、文字データを一部だけ書き換えるために、わざわざ`SUBSTR`関数を代入文の左辺に使ったんですが、これが何かマズかったんですか?」ってね。
……ほう、来たか。
メインフレームの現場でPL/Iを触るなら、避けて通れない「予約語を持たない言語仕様」の罠と、コンパイラの裏側の挙動の話だ。今日は、この`SUBSTR`ビルトイン関数を左辺値(Lvalue)として使うときの恐怖、いや、メモリコピーのオーバーヘッドとコンパイラの隠れた苦労について、みっちり叩き込んでやろう。
コーヒーでも飲みながら聞いてくれ。
—
1. そもそもPL/Iに「予約語」はないという変態仕様
本題に入る前に、PL/Iという言語の根本的な思想についておさらいしておこう。
C言語やJavaなんかとは違い、PL/Iには厳密な意味での「予約語(Reserved Words)」が存在しない。`IF`だろうが`DECLARE`だろうが、極端な話、変数名として使うことすら可能だ(コンテキストによってコンパイラが文脈判断するからだが、そんな狂気のコードを書いたら後輩の私刑ものだぞ)。
この「何でもあり」の懐の深さが、時に恐ろしい柔軟性を生む。その代表例が、今回取り上げる`SUBSTR`ビルトイン関数を代入文の左辺に置く構文だ。
1
/ こんな書き方がサラッとできてしまうのがPL/Iの恐ろしさ /
DCL WORK_AREA CHAR(100) INIT(‘IBM MAINFRAME SYSTEM’);
/ SUBSTRを左辺に置く(左辺値としての利用) /
SUBSTR(WORK_AREA, 5, 9) = ‘SUPERIOR’;
一見すると、「あぁ、`WORK_AREA`の5文字目から9文字分を置き換えてくれるんだな、便利じゃん」と思うだろう。
だがな、これが大規模なVSAMファイルからのレコード入出力や、数百万件を回す高速バッチのループ内で行われたらどうなるか。メインフレームのCPU使用率(秒数)が跳ね上がり、運用管理課からお叱りの飛ぶ原因になるのだ。
—
2. 現場のエンジニアが泣く「一時領域の生成とメモリコピー」の裏側
コンパイラがこのコードに遭遇したとき、裏で何をやっているかを知る必要がある。
C言語や一般的なポインター操作の感覚でいえば、「指定したアドレス(メモオフセット)に直接データを上書きしている」ように思えるかもしれない。しかし、PL/Iの言語仕様と、文字ストリング(String)のデータ属性の厳密さを思い出してほしい。
もし、操作対象の変数が可変長(VARYING)であったり、あるいはアライメントや記述子(Descriptor)の整合性を保つ必要がある場合、コンパイラは安全のために次のような処理を裏で実行する。
1. 一時領域(Temporary Descriptor / Work Area)の自動生成
代入の成否やデータ長の不一致によるメモリ破壊を防ぐため、ランタイム(LE: Language Environment)が裏でこっそりヒープまたはスタック上に作業用の仮領域を確保する。
2. 元データの退避と部分的な書き換え
元のストリングから必要な部分を一時領域にコピーし、指定された文字列を埋め込む。
3. 実変数への再配置(ストレージへの転記)
最終的に、一時領域で完成したデータを元の変数領域へ「丸ごとコピー」し直す。
……お気づきだろうか?
たった数バイトの文字を書き換えるために、コンパイラとランタイムが「見えないメモリの切り出し、コピー、再配置」の三段跳びを裏でやってのけているのだ。これが、いわゆる「メモリコピーのオーバーヘッド」の正体である。
—
3. 実践コード:VSAMレコード処理におけるアンチパターンと対策
百聞は一見にしかず。実際の基幹系バッチプログラムを想定したコードを見てみよう。
以下の例は、VSAM KSDS(キー順データセット)から読み込んだレコードの一部を、`SUBSTR`の左辺値を使って書き換えている典型的な「重い」コードと、それを回避するスマートな実装だ。
1
—————————————————————-
- MODULE NAME: SUBSTR_TEST
- DESCR: SUBSTR左辺値使用の是非とパフォーマンス改善の例
—————————————————————-
SUBSTR_TEST: PROC OPTIONS(MAIN);
/ VSAM入出力用レコード定義 /
DCL MASTER_KSDS FILE RECORD SEQUENTIAL UPDATE;
DCL 1 MASTER_REC,
5 REC_KEY CHAR(8),
5 REC_STATUS CHAR(2),
5 REC_FILLER CHAR(190);
DCL W_NEW_STATUS CHAR(2) INIT(‘OK’);
DCL I FIXED BIN(31) INIT(0);
ON ERROR
begin;
PUT SKIP LIST(‘ UNEXPECTED ERROR OCCURRED ‘);
GOTO ERROR_EXIT;
end;
OPEN FILE(MASTER_KSDS) UPDATE;
/ 大規模ループ(数百万件のトランザクション処理を想定) /
DO WHILE(TRUE);
READ FILE(MASTER_KSDS) INTO(MASTER_REC);
IF ENDFILE(MASTER_KSDS) THEN LEAVE;
/ ————————————————– /
/ 【アンチパターン】 SUBSTRを左辺値として使用 /
/ ————————————————– /
/ 高頻度で呼ばれるループ内では、この記述が毎回一時領域の /
/ 生成とメモリコピーを引き起こし、バッチを遅延させる /
/ ————————————————– /
/ SUBSTR(REC_STATUS, 1, 2) = W_NEW_STATUS; /
/ ————————————————– /
/ 【推奨アプローチ】 構造体メンバへの直接代入 /
/ ————————————————– /
/ フィールドが固定長であり、構造体で正しくレイアウトが /
/ 定義されているなら、直接代入する方が圧倒的に高速 /
/ ————————————————– /
REC_STATUS = W_NEW_STATUS;
REWRITE FILE(MASTER_KSDS) FROM(MASTER_REC);
END;
CLOSE FILE(MASTER_KSDS);
RETURN;
ERROR_EXIT:
CLOSE FILE(MASTER_KSDS);
SIGNAL FINISH;
END SUBSTR_TEST;
なぜ直接代入が良いのか?
上記のコードで、もし`REC_STATUS`が単なる固定長フィールドであれば、`REC_STATUS = W_NEW_STATUS;` と書くことで、コンパイラは余計な一時領域を一切作らず、機械語レベルの単純な`MVC`(Move Character)命令一発で処理を完結させられる。
一方、これを無理に `SUBSTR(MASTER_REC, 9, 2) = W_NEW_STATUS;` などと書こうものなら、コンパイラは親構造体全体のオフセット計算と動的な領域切り出しの最適化を諦め、ランタイムルーチンを呼び出すコードを生成してしまう。これがメインフレームのCPUを無駄に食いつぶす元凶なのだ。
—
4. ONユニット制御と例外処理における注意点
もう一つ、現場でやりがちなミスとして、可変長ストリング(VARYING)に対して`SUBSTR`の左辺値操作を行い、文字列の長さが溢れた場合の挙動がある。
PL/Iでは、ストリングの長さを超えた範囲に`SUBSTR`で代入を行おうとすると、条件名(Condition)である `STRINGRANGE` が発生する。
1
/ 開発環境やコンパイルオプション(STGIO等)によっては重いペナルティがある /
DCL V_DATA CHAR(10) VARYING INIT(‘ABC’);
/ 宣言された現在長を超える位置への代入はSTRINGRANGE例外のトリガーに /
SUBSTR(V_DATA, 15, 2) = ‘XY’;
もし本番稼働中のバッチプログラムで、このような意図しない`SUBSTR`の範囲外アクセスが頻発し、かつ適切な `ON STRINGRANGE` ユニットが記述されていなかったり、無駄なエラーハンドリングが走ったりすると、システム全体のパフォーマンスがガタ落ちする。いや、最悪の場合はアベンド(ABEND)だ。
プロダクションコードを書く上での鉄則として、以下の2点を肝に銘じておいてほしい。
1. 固定長データに対するストリングの切り出しは、極力構造体のオフセット(メンバ定義)で解決する。
2. どうしても`SUBSTR`を左辺に使わざるを得ない場合(不規則なパディングや動的な文字列構築など)は、ループの外で処理するか、あるいはオーバーヘッドを許容できるだけの頻度であるかをプロファイラ(IBM Fault AnalyzerやApplication Performance Analyzerなど)で必ず検証する。
—
シニアからのメッセージ
レガシーシステムと呼ばれるメインフレームの現場では、「動けばいいや」で書かれた何十年も前のコードが山のように眠っている。そして、マイグレーションやリファエンジニアリングの際に、そういう「なんとなく書かれた`SUBSTR`の左辺値」が、新しいCPU環境やクラウド上のエミュレータ上で思わぬボトルネックとして牙を剥くんだ。
PL/Iは非常に強力で、コンパイラが賢く最適化してくれる言語ではある。しかし、「コンパイラに余計な苦労をさせないコードを書くこと」こそが、われわれメインフレーム・エンジニアの腕の見せ所なのだよ。
さて、理論はここまでだ。次は実際のソースコードレビューで、この知見をどう活かすか、自分の目で確かめてみるといい。頼んだぞ!
