はじめに:PL/Iの「予約語を持たない自由度」と、コンパイラの裏側
ようこそ、メインフレームの深淵へ。
現代のモダンな言語、例えばJavaやC#、あるいはPythonに慣れ親しんだエンジニアが初めてPL/Iのコードベースに触れたとき、彼らは決まってこう驚く。「なぜ、この言語には厳密な予約語(Reserved Words)が存在しないのか?」と。
PL/Iの仕様において、`IF`や`THEN`、さらには組み込み関数の`INDEX`ですらも、コンテキストによってはただの識別子(変数名)として再定義することが理論上可能です。コンパイラは、字句解析の段階で文脈を推論し、それがキーワードなのかユーザー定義の変数なのかを完璧に識別しています。この「何でもあり」の柔軟性の裏で、IBMのPL/Iコンパイラ(Enterprise PL/Iなど)は、生成する機械語レベルにおいて凄まじい最適化を行っています。
今回は、その中でも特にバッチ処理のデータナレーションや文字列加工で頻繁に酷使される `INDEX`関数 に焦点を当てます。この関数が、IBM System/zのハードウェア命令である `TRT`(Translate and Test)命令 をどのようにハックし、極限のパフォーマンスを引き出しているのか。その内部挙動から、動的メモリ操作、アベンド解析、さらにはJavaやC#へのマイグレーション設計におけるリスクヘッジまで、システムアーキテクトの視点で徹底的に紐解いていきましょう。
—
1. `INDEX`関数の正体:ハードウェア命令 `TRT` との融合
基幹システムのオンライン処理や大規模バッチで、数百万件のレコードから特定の部分文字列を高速に検索する必要があるとき、私たちは何気なく `WK_POS = INDEX(IN_RECORD, ‘ERROR’);` と記述します。
この高レベルな関数呼び出しが、コンパイル時にどのように機械語に翻訳されるかご存知でしょうか?
素朴なアルゴリズムであれば、ポインタを1バイトずつインクリメントしながら文字比較のループ($O(N \times M)$ のオーダー)を回すところですが、IBMメインフレームのPL/Iコンパイラは、そんな非効率なコードは吐き出しません。
256バイトのトランスレートテーブルの構築
`INDEX`関数のターゲット(検索対象文字列)とサーチパターンが静的、あるいはそれに準じる条件を満たす場合、コンパイラおよびランタイムは、内部的に `TRT`(Translate and Test)機械語命令 を効率的に発行するための構造を裏で組み立てます。
`TRT`命令は、第1オペランドの文字列を1バイトずつスキャンし、第2オペランドで指定された256バイトのテーブル(トランスレート・アンド・テスト・テーブル)を参照します。
- テーブルの値が `X’00’` の場合:次のバイトへ進む。
- テーブルの値が `X’00’` 以外の場合:スキャンを即座に停止し、汎用レジスタに一致した位置のアドレスと、テーブルから引いた値を設定して制御を返す。
このハードウェア支援により、メインフレームはメモリスキャンをC言語の `memchr` やアセンブラの最適化ループを凌駕するハードウェア・パイプラインの最高速で実行します。
—
2. 実践:ポインタとベース変数によるメモリ直叩きと `INDEX` の最適化
マイグレーションやレガシーシステムのチューニング現場では、定義済みの固定長構造体だけでなく、動的に割り当てられた巨大なストレージ(ストレージ・クラス `BASED` 変数)を対象に `INDEX` を実行するケースが多々あります。
以下のPL/Iコードは、バッチプログラム内で動的領域(ゲッタウェイ・バッファ)をポインタで制御しつつ、`INDEX` 関数を用いて高速に区切り文字を検知する実務的なパターンです。
1
/ ======================================================= /
/ 動的メモリ上の高速パターンマッチング処理サブルーチン /
/ ======================================================= /
FAST_PARSER: PROC(P_BUF_PTR, P_BUF_LEN) OPTIONS(COBOL);
DCL P_BUF_PTR POINTER VALUE; / 入力バッファのポインタ /
DCL P_BUF_LEN FIXED BIN(31) VALUE; / バッファ長 /
/ BASED変数による動的ストラクチャのマッピング /
DCL 1 BUF_MAP BASED(P_BUF_PTR),
2 DATA_STR CHAR(P_BUF_LEN0 REFER(P_BUF_LEN));
DCL P_BUF_LEN0 FIXED BIN(31) INIT(0);
DCL WK_POS FIXED BIN(31) INIT(0);
DCL TARGET_POS FIXED BIN(31) INIT(1);
/ メモリダンプや異常値に備えたセーフティガード /
IF P_BUF_PTR = NULL() | P_BUF_LEN <= 0 THEN DO;
SIGNAL ERROR;
RETURN;
END;
/ TRT命令の恩恵を受けるINDEX関数の実行 /
/ 実際のコンパイル後コードでは、バッファ長に応じた最適化コードが展開される /
DO WHILE (TARGET_POS <= P_BUF_LEN);
WK_POS = INDEX(SUBSTR(BUF_MAP.DATA_STR, TARGET_POS), X'25'); / '%'を検索 /
IF WK_POS = 0 THEN
LEAVE; / 見つからなければループ抜け /
/ 見つかった位置のオフセット計算と処理 /
CALL PROCESS_MATCHING(TARGET_POS + WK_POS - 1);
TARGET_POS = TARGET_POS + WK_POS;
END;
RETURN;
END FAST_PARSER;
ここで重要なのは、`SUBSTR` と `INDEX` を組み合わせる際、コンパイラ最適化レベル(`OPTIMIZE(2)` 以上)が有効になっていないと、不要な一時ワークエリアへのデータ転送(MVC命令の乱発)が発生し、`TRT` の恩恵が台無しになる点です。アーキテクトとしては、必ずコンパイルリスト(Cross-Reference and Attributes)を検分し、インライン展開やレジスタ割り当てが意図通りに行われているかを確認する義務があります。
---
3. 現場の罠:アベンド解析とエッジケース
高パフォーマンスを誇る `INDEX` ですが、メインフレーム特有のデータ構造の闇に足を踏み入れると、突然の S0C4アベンド(Protection Exception) や予期せぬロジック崩壊を引き起こします。
エッジケース1:パックデシマル(COMP-3)の内部符号反転バグと文字列関数
たまに見る悪夢が、パックデシマル項目やバイナリ項目(`FIXED BIN`)を、型を無視して `CHAR` 型の変数にオーバーレイし、そのまま `INDEX` で値を探そうとするコードです。
例えば、パックデシマルの末尾ニブル(符号部分)が不正なゾーンであったり、符号反転バグによって意図しないバイナリデータ(例: `X’00’` や `X’FF’`)が文字列の途中に紛り込んだ場合、`INDEX` が誤った位置でヒットするか、あるいは文字列の終端を見失ってストレージの境界外までスキャンを続けようとし、S0C4アベンドを誘発します。
エッジケース2:CICSおよびDB2環境下でのストリング長オーバー
CICSオンラインの通信領域(COMMAREA)や、DB2の可変長文字列(VARCHAR:2バイトの長さプレフィックス+実データ)をそのまま `INDEX` に渡す際、プレフィックスの長さを誤って計算し、ゴミデータをスキャンしてしまうトラブルが後を絶ちません。
特にDB2の埋め込みSQLで取得した `VARCHAR` は、PL/I側では以下のような構造体として展開されます。
1
DCL 1 DB2_VSTR,
2 LEN FIXED BIN(15), / 実際の長さ /
2 TEXT CHAR(200); / 文字列本体 /
この `TEXT` に対して、`LEN` を無視して全体(200バイト)を `INDEX` で検索してしまうと、以前のトランザクションのゴミクズまで検索対象に含まれ、セキュリティ上の重大な脆弱性や誤作動を引き起こします。検索前には必ず `SUBSTR(TEXT, 1, LEN)` で範囲を厳密に切り出すべきです。
—
4. レガシー移行(Java / C#)へのアーキテクチャ設計上の警鐘
さて、私たちアーキテクトが最も頭を悩ませる「メインフレームからの脱却(マイグレーション)」の文脈において、この `INDEX` 関数と `TRT` 命令の挙動はどう扱われるべきでしょうか。
Javaの `String.indexOf()` や C#の `string.IndexOf()` は、モダンなCPUのSIMD命令(AVX-512やNEONなど)を利用して十分に高速化されています。しかし、セマンティクス(意味論)の差異 に起因するバグには細心の注意が必要です。
1. 文字コード(EBCDIC vs ASCII/UTF-8)の差異
メインフレーム上で動いていた `INDEX(RECORD, ‘A’)` は、EBCDICコード体系における `’A’`(`X’C1’`)の位置を検索しています。これを単純にJavaの `indexOf(“A”)`(Unicodeの `U+0041`)に置き換える場合、コード変換(CCSID変換)のタイミングや、シフトJIS/EBCDIC特有の漢字混じりデータ(2バイト文字の扱い)でオフセットのズレが必ず発生します。
2. パディング(空白埋め)の仕様
PL/Iの `CHAR` 型は固定長であり、足りない部分は右側にスペース(`X’40’`)が自動パディングされます。一方、Javaの `String` は可変長です。レガシーコードが「末尾のスペースを含めた固定長の位置」を前提に `INDEX` の結果を利用していた場合、移行後のJavaコードで同様のロジックを書くと、文字数が可変になった瞬間にオフセットが狂い、致命的なバグを生みます。
マイグレーション設計においては、単に構文を機械的に置き換えるのではなく、「元のコードがハードウェアのどの命令とメモリレイアウトを前提に書かれていたか」を逆算し、移行先のランタイム(JavaのByteBufferやカスタム文字検索ユーティリティ)で同等以上のスキャン性能と正確性を担保する設計が不可欠です。
—
おわりに
PL/Iの基本構文や組み込み関数は、一見すると古臭いレガシーな記述に見えますが、その背後にはIBMメインフレームのハードウェアアーキテクチャ(S/370、ESA/390、Zアーキテクチャ)と直結した、息を飲むほど洗練された最適化の歴史が詰まっています。
「予約語がない」という極限の自由度を許された言語で書かれたコードを紐解き、機械語レベルの挙動まで見通すこと。それこそが、真のレガシー移行スペシャリスト、そしてシステムアーキテクトに求められる矜持です。
基幹システムの荒海を航海するエンジニア諸君、次に見るコンパイルリストやダンプには、今日の知見をぜひ役立ててください。
