こんにちは。夜な夜な巨大なJCLと格闘し、ABENDのSYSUDUMPを睨みつけている全国のメインフレーム・エンジニアの皆さん。
今回は、PL/Iの基本中の基本でありながら、その内部挙動を理解している人が意外と少ない「INDEX関数」について話をしよう。
「おい、新人の頃に習った文字列検索だろ? 今さら何を語ることがある」と思ったそこのあなた。
ちょっと待ってほしい。マイグレーション案件や、膨大なトランザクションを処理する大規模バッチの性能チューニング現場において、「なんとなく動くから」と適当に文字列操作を書いていると、生産稼働後にCPU使用率(サービスタスク単位のELAPSED TIME)が高騰して運用部門から呼び出しを食らうことになる。
PL/Iの美しさは、高水準言語でありながら、生成されるマシン語(アセンブラレベル)の挙動をプログラマが手に取るように想像できる点にある。特に今回取り上げる`INDEX`関数が、System/390やz/Architectureのハードウェア命令であるTRT(Translate and Test)命令とどのように結びついているかを知ることで、君の書くコードの質は一段も二段も跳ね上がるはずだ。
さあ、現場のシニアアーキテクトが長年培ってきた「泥くさくも美しい」メインフレームの極意を授けよう。
—
1. 識別子の自由度と「予約語を持たない」PL/Iの思想
本題に入る前に、PL/Iの根底にある強力な構文規則について少し触れておこう。
C言語やJava、COBOLとは異なり、PL/Iには厳密な意味での「予約語(Reserved Words)」が存在しない。
例えば、COBOLでは `INDEX` や `SEARCH`、`READ` といった語句は予約語であり、変数名として使うことはご法度だ。しかし、PL/Iでは以下のようなコードを書くことすら構文上は許容される。
1
/ 予約語を持たないPL/Iの変態的な(しかし合法な)例 /
DECLARE INDEX FIXED BINARY(31);
DECLARE READ CHARACTER(10);
コンパイラは前後の文脈(Context)から、それがキーワードなのか、プログラマが定義した識別子(変数名)なのかを完璧に判別する。この柔軟性は、プログラミング言語の歴史において極めて異例かつ強力な設計思想だ。
しかし、実務の現場では、コンパイラを混乱させないため、そして何より後から保守する開発者のメンタルを守るため、組み込み関数(BUILTIN)と同名の変数名を定義することは厳に慎むべきである。これから解説する `INDEX` も、当然システムが提供する強力な組み込み関数として活用する。
—
2. INDEX関数とハードウェア命令「TRT(Translate and Test)」の密な関係
さて、本日のメインテーマだ。
私たちがPL/Iのコードで何気なく記述する以下の一文。
1
POSITION = INDEX(TARGET_STR, SEARCH_CHAR);
この時、コンパイラ(IBM Enterprise PL/I Compilerなど)は、これをただのループ処理(文字を1文字ずつ比較するような愚かな処理)としてはコンパイルしない。ターゲット文字列の長さや検索パターンの長さに応じて、S/390アーキテクチャの真骨頂であるTRT(Translate and Test:翻訳してテスト)命令、あるいは文字長に応じた最適化されたハードウェア命令群へと機械語レベルでトランスレートする。
TRT命令のメカニズム
TRT命令は、2つのオペランド(引数とテーブル)を取る。
1. 第1オペランド(検査対象の文字列): メモリ上のアドレスと長さ。
2. 第2オペランド(256バイトのトランスレーション・テーブル): 検索対象の文字バイトに対応する値を格納したテーブル。
CPUは、第1オペランドの文字列を1バイトずつスキャンし、第2-テーブルをインデックスとして参照する。もしテーブルの値がゼロであればスキャンを続け、非ゼロの値を見つけた瞬間に処理を中断し、見つかったメモリアドレスと、テーブルから引いた非ゼロの値を汎用レジスタに格納して制御を返す。
つまり、PL/Iの `INDEX` 関数は、アセンブラで職人が手組みしたような極限まで最適化された高速スキャンを、バックグラウンドのハードウェア支援によって一撃でやってのけているのだ。これを自前で `DO` ループを回して一文字ずつ比較するようなコードを書いたら、コンパイラの最適化をフイにするだけでなく、CPU時間をドブに捨てるようなものである。
—
3. 実践:VSAMレコード処理におけるINDEX関数の活用とONユニット制御
では、実際のメインフレーム開発現場を想定したサンプルコードを見ていこう。
ここでは、VSAM(KSDS)から読み込んだレコードの可変長データ領域から、特定のデリミタやキーワードを `INDEX` 関数で高速に検出し、例外処理(ONユニット)の制御フローと組み合わせた堅牢なバッチ処理の例を示す。
1
/ ================================================================= /
/ PROGRAM-ID: VSRCH01 /
/ REMARKS : VSAMレコードを読み込み、INDEX関数とTRTの高速性を活かして /
/ 特定キーワードを検出・処理する実戦形式のPL/Iサンプル /
/ ================================================================= /
VSRCH01: PROC OPTIONS(MAIN);
/ 組み込み関数の明示的宣言(保守性向上のため常に記述を推奨) /
DECLARE INDEX BUILTIN;
DECLARE LENGTH BUILTIN;
/ VSAMファイル定義 /
DECLARE MASTER_VSAM FILE RECORD SEQUENTIAL INPUT;
/ レコードエリア定義 /
DECLARE 1 IN_RECORD,
5 REC_LEN FIXED BINARY(15), / レコード長 /
5 REC_DATA CHARACTER(1000); / データ本体 /
/ 制御変数 /
DECLARE SEARCH_KEY CHARACTER(10) INITIAL(‘ERROR-LOG’);
DECLARE POS FIXED BINARY(31);
DECLARE EOF_FLAG BIT(1) INITIAL(‘0’B);
/ ファイル終了(ENDFILE)のONユニット制御フロー /
ON ENDFILE(MASTER_VSAM)
BEGIN;
DISPLAY(‘ INFO: VSAM FILE END-OF-FILE REACHED. ‘);
EOF_FLAG = ‘1’B;
END;
/ I/Oエラー時のONユニット(基幹システムの必須作法) /
ON ERROR
BEGIN;
DISPLAY(‘ SEVERE ERROR: UNEXPECTED ABEND CAUGHT IN ON-UNIT. ‘);
/ 必要に応じてダンプ取得や異常終了処理を記述 /
SIGNAL ERROR;
END;
/ VSAMファイル オープン /
OPEN FILE(MASTER_VSAM);
DISPLAY(‘ INFO: BATCH PROCESSING STARTED. ‘);
/ メインループ /
DO WHILE(^EOF_FLAG);
READ FILE(MASTER_VSAM) INTO(IN_RECORD);
/ 読み込み成功かつファイルエンドでなければ処理継続 /
IF ^EOF_FLAG THEN DO;
/ ★ここでINDEX関数がハードウェア命令(TRT等)を活用して高速検索を行う /
POS = INDEX(IN_RECORD.REC_DATA, SEARCH_KEY);
IF POS > 0 THEN DO;
/ キーワードが見つかった場合の処理 /
DISPLAY(‘MATCH FOUND AT POSITION: ‘ || TRIM(CHAR(POS)));
/ TODO: 該当レコードに対する詳細なビジネスロジックやフラグ更新 /
END;
END;
END;
/ クローズ処理 /
CLOSE FILE(MASTER_VSAM);
DISPLAY(‘ INFO: BATCH PROCESSING NORMALLY TERMINATED. ‘);
RETURN;
END VSRCH01;
—
4. シニアアーキテクトからの実践的なデバッグとコーディングのコツ
現場でこの手のコードをレビューしていると、たまに次のような「アンチパターン」に遭遇する。後輩のコードを直すときは、以下のポイントを必ず指導してほしい。
1. 不要な部分文字列(Substring)の切り出しをしない
`IF INDEX(SUBSTR(IN_RECORD.REC_DATA, 1, 500), SEARCH_KEY) > 0 THEN …`
このような書き方をすると、コンパイラが余計な一時変数(ワークエリア)を生成し、メモリコピーのオーバーヘッドが発生する。`INDEX(IN_RECORD.REC_DATA, SEARCH_KEY)` のように、対象文字列をそのまま渡せば、コンパイラとハードウェアが最も効率の良いメモリアドレスのポインタ操作を行ってくれる。
2. 文字型変数のアライメントと長さ(CHARACTER vs VARCHAR)
固定長文字列(`CHARACTER(n)`)と可変長文字列(`VARYING`)では、内部の保持構造が異なる。`INDEX` 関数に渡す変数は、可能な限り宣言時の属性を一致させ、コンパイラに暗黙の型変換(データ変換に伴うCPUサイクルの消費)をさせないことが、チューニングの基本中の基本である。
3. ONユニットは「隠れたジャンプ先」であることを意識する
サンプルコードで見せた `ON ENDFILE` や `ON ERROR` は、構造化プログラミングのきれいな見た目とは裏腹に、内部的には一種の割り込み処理(例外ハンドリング)である。無闇に複雑なロジックをONユニットの中に書くと、デバッグ時に制御フローが追いにくくなるため、フラグの変更やロギングに留めるのがプロの作法だ。
—
おわりに
メインフレームの世界はレガシーと呼ばれることが多いが、その裏側にあるアーキテクチャの合理性とパフォーマンスの追求は、現代のクラウドネイティブな環境であっても学ぶべき点が非常に多い。
「なぜこの関数は速いのか」
「このソースコードは、マシン語に落ちたときにCPUにどのような負荷をかけるのか」
そういった視点を持ち続けることで、単なる「コードを動かすオペレータ」から、真の意味での「システムアーキテクト」へとステップアップできる。
次のバッチ改修の夜には、ぜひコンパイルリストの機械語命令(オフセット)や、今回解説した `INDEX` 関数の背後にあるハードウェアの息吹に思いを馳せてみてほしい。
健闘を祈る。複雑なジョブネットの海で迷子にならないよう、今日も堅牢なコードを書こう。
