はじめに:STREAM入出力の「バッファリング」を舐めてはいけない
メインフレームの現場で何十年も稼働し続けている基幹系バッチプログラム。その心臓部であるPL/Iコードを眺めていると、時として `STREAM` 環境における `BUFFERED` と `UNBUFFERED` の違いを見落としたがために、夜間バッチのウィンドウを盛大に突き破るという、古典的かつ致命的な悲劇に遭遇する。
JavaやC#といったモダン言語の抽象化されたI/Oに慣れ親しんだ若いエンジニアや、単なる構文の置き換えしか頭にないマイグレーションツールは、この違いを軽視しがちだ。しかし、z/OSのアクセス方式(BSAMやQSAM)の挙動、そしてチャネル・プログラムの仕組みまで踏み込むならば、この属性選択がCPU使用率とI/O待ちのバランスを完全に支配していることが痛いほどわかるはずだ。
今回は、PL/Iの `STREAM` ファイルにおける `BUFFERED` と `UNBUFFERED` の性能差と、バッファ管理の深層、さらにはエッジケースにおけるトラブルシューティングについて、レガシーの極みを極めたアーキテクトの視点から徹底的に紐解いていこう。
—
1. 識別子の自由度と、その裏腹にあるコンパイラ最適化の罠
まず大前提として、PL/Iという言語の特異な設計思想に触れておかねばならない。PL/Iには、いわゆる「予約語(Reserved Words)」というものが厳密には存在しない。`BUFFERED` や `UNBUFFERED`、さらには `OPEN` や `CLOSE` でさえも、文脈によってはプログラマが自由な変数名(識別子)として定義できてしまう。
/ 狂気の変数宣言例:絶対に真似してはいけません /
DECLARE
BUFFERED FIXED BIN(31),
UNBUFFERED CHARACTER(10);
/ これでもコンパイルエラーにならないのがPL/Iの恐ろしさ /
BUFFERED = 100;
この仕様は言語の柔軟性を示す一方で、コンパイラがソースコードを解析する際のトークナイザ(語句解析器)に絶大な負荷をかける。コンテキスト依存の解析を行っているため、誤ったコーディングスタイルの下では、コンパイラが最適化の機会を逸失し、生成される機械語命令(Object Code)が冗長になる傾向がある。
STREAM入出力における属性指定も、このコンパイラの解釈と密接に関わっている。次に、本題であるバッファリングのメカニズムへ踏り込もう。
—
2. BUFFERED vs UNBUFFERED:アーキテクチャレベルでの性能差
`STREAM INPUT` または `STREAM OUTPUT` ファイルを定義する際、属性として `BUFFERED`(デフォルト)あるいは `UNBUFFERED` を指定する。この数文字の違いが、チャネル・プログラム(EXCPマクロの発行頻度)とOSのバッファ管理に絶大な影響を与える。
BUFFERED 属性の挙動(デフォルト)
- 機構: レコード単位ではなく、論理レコードがブロック単位(BLKSZ)でOSのバッファプール(VSAMやQSAMのバッファ)に読み込まれ、プログラムはそのバッファ上のアドレスを透過的に操作する。
- 利点: 物理的なI/O(EXCPの回数)が劇的に削減される。CPUのオーバーヘッドとI/O待機時間が最小化され、大量データを扱う基幹バッチにおいては必須の選択肢となる。
UNBUFFERED 属性の挙動
- 機構: ファイル操作のたびに、ワーキングストレージと外部記憶装置の間で直接データ転送が行われる(あるいは極小のバッファを介して即座に物理IOを誘発する)。
- 用途: 端末やコンソール、あるいは厳密なリアルタイム性が要求される一部の特殊なファイル(ただしSTREAMでこれをやること自体がアーキテクチャ上のアンチパターンに近い)を除き、通常のデータセットに対して指定すべきではない。
もし、数百万件のマスターファイルを処理するバッチで `UNBUFFERED` を誤指定しようものなら、z/OSのアクセスメソッドが毎回物理I/Oを直撃させ、チャネルビジー率が跳ね上がり、システム全体のスループットが崩壊する。これこそが、移行設計時に絶対に見落としてはいけないポイントである。
—
3. 実践コード:動的メモリとSTREAM入出力の安全な共存
基幹系システムでは、可変長レコードや巨大な構造体を扱うために、ベース変数(Based Variable)とポインタ(Pointer)を用いた動的メモリ管理が多用される。以下に、`BUFFERED` なSTREAMファイルからデータを安全に読み込み、ストレージ上で処理する堅牢なPL/Iコードの実例を示す。
/ ========================================================== /
/ STREAM出力ファイルにおけるバッファ管理と動的処理のサンプル /
/ ========================================================== /
TEST_BATCH: PROC OPTIONS(MAIN);
/ 構造体の定義(ベース変数用) /
DECLARE 1 DTL_RECORD BASED(P_DTL),
2 REC_ID CHAR(4),
2 REC_AMT FIXED DEC(15,2),
2 REC_DESC CHAR(100);
DECLARE P_DTL POINTER;
/ ファイル定義:BUFFEREDを明示的に指定(デフォルトだが可視化) /
DECLARE SYSIN FILE STREAM INPUT BUFFERED;
DECLARE SYSPRINT FILE STREAM OUTPUT BUFFERED;
DECLARE EOF_FLG BIT(1) INIT(‘0’B);
DECLARE ERR_CNT FIXED BIN(31) INIT(0);
/ 終了条件の監視 /
ON ENDFILE(SYSIN) EOF_FLG = ‘1’B;
/ エラー発生時の異常系ハンドリング /
ON ERROR
BEGIN;
DISPLAY(‘ CRITICAL ERROR OCCURRED IN STREAM PROCESSING ‘);
/ ダンプ採取を誘導するための強制アベンド発行 /
SIGNAL ERROR;
END;
OPEN FILE(SYSIN) INPUT, FILE(SYSPRINT) OUTPUT;
DO WHILE(^EOF_FLG);
/ 動的メモリの割り当て(ALLOCATE) /
ALLOCATE DTL_RECORD;
/ STREAMからのデータ取得(実際にはGET LIST / EDITを使用) /
/ ここでは簡略化のためイメージとして記述 /
GET FILE(SYSIN) EDIT (REC_ID, REC_AMT, REC_DESC)
(A(4), F(15,2), A(100));
IF EOF_FLG THEN DO;
FREE DTL_RECORD;
LEAVE;
END;
/ パックデシマルの符号反転やビジネスロジックの適用 /
IF REC_AMT < 0 THEN DO;
ERR_CNT = ERR_CNT + 1;
/ エラーログへの出力 /
PUT FILE(SYSPRINT) EDIT ('MINUS AMOUNT DETECTED: ', REC_ID, REC_AMT)
(A, A(4), F(15,2));
END;
/ 処理終了後のメモリ解放(メモリリーク防止の鉄則) /
FREE DTL_RECORD;
END;
CLOSE FILE(SYSIN), FILE(SYSPRINT);
DISPLAY('BATCH NORMALLY TERMINATED. ERROR COUNT: ' || ERR_CNT);
RETURN;
END TEST_BATCH;
---
4. エッジケースの解析:アベンド、符号反転、そしてマイグレーションの罠
レガシーシステムをモダナイゼーションする際、あるいは既存のバッチが突如として `S0C4` や `S0C7` などのシステムアベンド(ABEND)を起こした際、アーキテクチャの深い理解がなければ原因究明に途方もない時間がかかる。
① パックデシマル(COMP-3)の内部符号反転バグとバッファ溢れ
PL/Iの `FIXED DECIMAL` は、内部的にPacked Decimal形式(EBCDICのゾーン・数値表現)で保持される。外部ファイルから `STREAM` でデータをインポート・エクスポートする際、コードページの変換ミスや `UNBUFFERED` による不整合が生じると、最下位ニブル(符号部分:通常 `C`, `D`, `F` など)が破損する。
これが原因で、演算命令実行時に有名な S0C7(データ例外: Data Exception) が発生する。ダンプリスト(CEEDUMP)を覗き、レジスタ内の16進数データが `0C7` のアベンドアドレス周辺でどのように崩れているかを見極めるスキルは、シニアアーキテクチャの必須素養である。
② 埋め込みSQL(DB2)やCICSオンラインとの混在におけるバッファ矛盾
STREAMファイル(特に `BUFFERED`)を使用している最中に、同一タスク内でDB2のコミットポイント(`EXEC SQL COMMIT`)やCICSの同期点(`EXEC CICS SYNCPOINT`)を跨ぐ場合、OSレベルのバッファフラッシュとDB2のログバッファの整合性が問題になる。
中途半端なバッファリング状態のままタスク異常終了(ABEND)が発生すると、ファイル側のバッファに残っていたデータが書き出されず、データベース側との不整合(データのロストや二重更新)を引き起こす。これを防ぐためには、適切なタイミングでの `ENDFILE` 処理や、必要に応じた `CLOSE` / `OPEN` によるバッファの強制フラッシュ(Synchronize)設計が不可欠となる。
—
おわりに:レガシーの知見を未来のアーキテクチャへ
PL/Iの `BUFFERED`/`UNBUFFERED` 属性に代表される、バッファ管理とI/O制御の哲学は、単なる「古い汎用機の作法」ではない。現代のクラウド環境や分散システムにおける非同期I/O、ブロッキング制御、そしてメモリプーリングの概念と本質的に何ら変わらない。
JavaやC#、あるいはクラウドネイティブなマイクロサービスへシステムを移行(マイグレーション)する際、「どう書き換えるか」という表面的な構文変換にとどまってはならない。元々のレガシープログラムがどのようなハードウェア制約、どのようなOSのI/O最適化の恩恵を受けてそのコードになっていたのか。その背景にあるアーキテクチャの文脈を深く理解し尽くしてこそ、真に堅牢で高パフォーマンスな次期システムを設計することができる。
レガシーを制する者は、モダナイゼーションを制す。コードの奥底に宿るエンジニアリングの魂を、次の世代へと確実に継承していこう。
