おい、最近夜間バッチのウィンドウがじわじわ延びてきて、運用管理部からお叱りを受けてないか? 「なんか最近、勘定系の月次集計バッチが終わるの遅いんだけど、コード何か変えた?」なんて言われて、冷や汗をかいている若手も多いんじゃないかと思う。
こういう時、大抵犯人はデータベースのインデックス設計ミスか、あるいはアホみたいに非効率な文字列走査ループだ。特に、何百万件も流れるレコードに対して、自前で `DO` ループを組んで1文字ずつ文字比較をやっているコードを見つけると、俺は思わずそのエンジニアの肩を叩きたくなる。「おいおい、ここは昭和のオフコンじゃないんだ。IBMメインフレームのハードウェア特性を思い出してくれ」ってな。
今回は、PL/Iの真骨頂である組み込み関数 `INDEX` と `VERIFY` を取り上げる。これらが内部でどう動き、なぜCPUサイクルを劇的に節約できるのか。メインフレームの鉄板であるVSAM処理やONユニットのトラップ制御も絡めながら、現場で即効性のある話をしていこう。
—
1. 予約語を持たないPL/Iの美学と `INDEX` / `VERIFY` の正体
まず、PL/Iの根本的な言語思想に触れておこう。C言語やJavaなんかとは違い、PL/Iには厳密な予約語(Reserved Words)というものが存在しない。`IF` や `DO` でさえも、コンテキストによっては変数名として使えてしまうという、懐の深い(というか狂気すらはらんだ)言語仕様だ。
この設計思想は、組み込み関数(BUILTIN)にも表れている。`INDEX` や `VERIFY` は、コンパイラに対して「これは関数だ」と明示しない限り、ただの識別子として扱われる余地さえある。だからこそ、確実に最適化の恩恵を受けるためには、以下のように明確に宣言しておくことが鉄則だ。
1
DCL INDEX BUILTIN;
DCL VERIFY BUILTIN;
これをサボると、コンパイラが「おっ、自作のサブルーチンを呼ぶんだな?」と勘違いして、無駄なリンケージ(CALL)を生成したり、インライン展開のチャンスを逃したりする。たった1行の `DCL … BUILTIN` が、CPUの命令キャッシュ効率を救うこともあるんだ。忘れずに書いておくように。
—
2. ハードウェアの咆哮:`TRT` 命令と内部実装の秘密
さて、本題のパフォーマンスの話だ。なぜ自前の `DO` ループによる文字検索がダメで、`INDEX` や `VERIFY` が速いのか。
それは、これらがSystem z(IBMメインフレーム)のハードウェア命令(マシン語)を直接、あるいは極限まで最適化されたランタイムルーチン経由で叩いているからだ。
例えば、指定した文字が文字列のどこにあるかを探す `INDEX` や、逆に「指定した文字セット以外の文字が最初に出現する位置」を探す `VERIFY` は、内部的に `TRT`(Translate and Test:翻訳してテスト)命令 や、近年の z14 / z15 / z16 で強化されたベクトル命令(Packed DecimalやVector String Instructions)をフル活用している。
昔からのメインフレームエンジニアなら `TRT` の名を聞いてピンと来るはずだ。256バイトのトランスレート・テーブルを用意し、1回の命令でメモリブロックを高速スキャンする、あの伝説的な命令だ。
自前のPL/Iコードで `IF STRING(I:I) = ‘X’` なんてやってループを回すと、1文字ごとにブランチ命令(条件分岐)が発生し、パイプラインが乱れまくる。これがCPUサイクルの無駄遣い、すなわち「CPU時間の高騰」の正体というわけだ。
—
3. 実践:VSAMレコード処理における最適化コード
口で言うだけでは納得いかないだろう。実際のバッチプログラムを想定したサンプルコードを見せよう。
ここでは、KSDS(Key-Sequenced Data Set)のVSAMファイルを順次読み込み、可変長レコード(VB)内の特定の制御文字や不正なデータパターンを `INDEX` と `VERIFY` で高速に検知・処理する例を組んでみた。エラー時の異常終了を避けるための `ON` ユニットの作法にも注目してほしい。
1
/ ================================================================= /
/ プログラムID : SRTOPT01 /
/ テーマ : INDEX/VERIFY関数を活用した高効率文字列検索 /
/ ================================================================= /
SRTOPT01: PROC OPTIONS(MAIN);
/ 組み込み関数の明示的宣言(最適化の基本) /
DCL INDEX BUILTIN;
DCL VERIFY BUILTIN;
DCL LENGTH BUILTIN;
/ ファイル定義(VSAM KSDS) /
DCL CUST_VSAM FILE RECORD SEQUENTIAL INPUT
ENVIRONMENT(VSAM);
/ レコード構造体(可変長を想定) /
DCL 1 CUST_REC,
5 REC_LEN BIN FIXED(15),
5 REC_BODY CHAR(500);
/ ワーク変数 /
DCL W_EOF_FLG CHAR(1) INIT(‘0’);
DCL W_POS BIN FIXED(31);
DCL W_ERR_CNT BIN FIXED(31) INIT(0);
/ 検証用定数:英数字およびスペースのみを許可する文字セット /
DCL VALID_CHARS CHAR(62) STATIC
INIT(‘ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ‘);
/ — ONユニット定義:ファイル終了(ENDFILE)の捕捉 — /
ON ENDFILE(CUST_VSAM) W_EOF_FLG = ‘1’;
/ — ONユニット定義:データ例外等の安全策 — /
ON ERROR BEGIN;
DISPLAY(‘【SEVERE】予期せぬエラーが発生しました。処理を中断します。’);
CLOSE FILE(CUST_VSAM);
SIGNAL FINISH;
END;
DISPLAY(‘— 顧客マスタ文字列検証処理 開始 —‘);
OPEN FILE(CUST_VSAM);
/ メインループ /
DO WHILE(W_EOF_FLG = ‘0’);
READ FILE(CUST_VSAM) INTO(CUST_REC);
/ 読み込み成功時の処理 /
IF W_EOF_FLG = ‘0’ THEN DO;
/ 1. INDEX関数の活用:特定の制御コード(例: X’00’)の混入チェック /
/ 文字列中にヌル文字が含まれていないかを一撃で探す /
W_POS = INDEX(REC_BODY, ’00’X);
IF W_POS > 0 THEN DO;
DISPLAY(‘警告: 制御文字(X”00”)を検知 位置 = ‘ || TRIM(W_POS));
W_ERR_CNT = W_ERR_CNT + 1;
ITERATE; / 次のレコードへスキップ /
END;
/ 2. VERIFY関数の活用:許可されていない文字が含まれていないか一網打尽 /
/ VALID_CHARS 以外の文字が最初に出現する位置を返す(0なら全て正常) /
W_POS = VERIFY(REC_BODY, VALID_CHARS);
IF W_POS > 0 THEN DO;
DISPLAY(‘警告: 許可外の文字を検知 位置 = ‘ || TRIM(W_POS)
|| ‘ 文字 = ‘ || SUBSTR(REC_BODY, W_POS, 1));
W_ERR_CNT = W_ERR_CNT + 1;
END;
END;
END;
CLOSE FILE(CUST_VSAM);
DISPLAY(‘— 顧客マスタ文字列検証処理 終了 検出件数: ‘ || TRIM(W_ERR_CNT) || ‘ —‘);
END SRTOPT01;
—
4. コードの解説と現場のテクニック
このコードのポイントをいくつか、現場の視点から解説しておこう。
① `INDEX(REC_BODY, ’00’X)` の強み
レガシーシステム移行時によくあるのが、他機種(オープン系など)からコンバートしてきたデータに混ざるゴミデータだ。これを自前ループで探すと遅い上にバグの元になる。`INDEX` に第2引数としてバイナリや1バイト文字を渡すことで、コンパイラは即座にハードウェアの高速スキャン命令を生成する。文字の長さが最大長(この場合は500バイト)固定であるため、余計なオーバーヘッドがない。
② `VERIFY` によるホワイトリスト検証
セキュリティやデータクレンジングの観点でも `VERIFY` は最強の武器だ。「この文字しか入っちゃいけない」というホワイトリスト方式のバリデーションをやる場合、通常の `DO` ループで1文字ずつ比較テーブルと照合していたら、それだけでバッチのCPU時間をゴリゴリ削られる。`VERIFY(対象文字列, 許可文字セット)` は、許可されていない文字が混ざった瞬間にその位置(オフセット)を返すため、不正データの早期発見に絶大な効果を発揮する。
③ 適切なONユニットの配置
VSAMの `ENDFILE` や、万が一のデータ切り捨て・領域外参照に対する `ERROR` 条件を `BEGIN…END` ブロックで捕捉している。レガシーバッチにおいて、エラーハンドリングを怠るとアブノーマルエンド(ABEND)時にジョブ管理システム(A-AUTOやJP1など)が盛大にアラートを上げ、オペレータを深夜に叩き起こす羽目になる。丁寧なメッセージを出してクローズ処理を入れるのが、デキるアーキテクトの作法だ。
—
5. おわりに:CPU時間を制する者がメインフレームを制す
クラウド全盛の現代であっても、メガバンクや大手生損保、流通の基幹システムを支えるIBMメインフレームにおいて、「1命令の重み」は微塵も変わっていない。クラウドならリバースを増やしてスケールアウトでゴリ押しできるかもしれないが、メインフレームの世界では、CPU使用量(MIPs課金)はそのまま直結するコストなのだ。
今回紹介した `INDEX` や `VERIFY` のような組み込み関数を適切に選び、ハードウェア命令の特性を意識したコーディングを行うこと。それこそが、レガシーシステムの寿命を延ばし、夜間バッチを定刻通りに終わらせるための最大の秘訣である。
さて、理屈は分かったはずだ。次に自社のバッチソースで `DO I = 1 TO 100` なんて汚い文字比較ループを見つけたら、ドヤ顔でこの書き方にリファクタリングしてやるといい。後輩からの見る目が変わること間違いなしだ。
