おい、最近配属された若手が「STREAM入出力のバッファなんて、どっちでも動くんだから一緒っしょ?」なんて軽口を叩いているのを聞いてね、思わず冷や汗が出ちまったよ。
いいかい? 24時間365止まらない勘定系や、夜間バッチで何千万件ものデータをぶん回すメインフレームの世界じゃ、その「どっちでもいいや」という甘い認識が、一晩でCPU使用率を天井知らずに跳ね上げさせ、ジョブをABEND(異常終了)に追い込む最大の凶器になるんだ。
今回は、PL/IのSTREAM入出力における `BUFFERED` と `UNBUFFERED` 属性の性能差とバッファ管理の泥臭い実態 について、現場のノウハウを余すところなく叩き込んでやる。耳の穴かっぽじってよく聞きな。
—
1. なぜPL/IのSTREAM入出力でバッファが命取りになるのか
そもそもPL/IのSTREAM(ストリーム)入出力は、人間にとって読みやすい文字形式(キャラクター)としてデータを扱う仕組みだ。レコード入出力(RECORD I/O)が物理的なレコード構造をそのまま扱うのに対し、STREAMは文字のストリーム(流れ)として読み書きするため、内部で膨大な編集・変換処理が発生する。
ここで問題になるのが バッファリング戦略 だ。
`BUFFERED` 属性:システム任せの省エネ優等生
`BUFFERED`(デフォルトまたは明示的指定)を指定すると、OS(BSAMやQSAMなどのアクセス法)がメモリ上にバッファプールを確保し、複数レコードをまとめてI/O処理する。
- メリット: 物理的なI/O回数が激減するため、チャネルビジー率が下がり、結果としてCPUの待ち時間が減る。
- デメリット: メモリを消費する。また、バッファがフラッシュされるタイミングを意識しないと、障害時のデータロスト範囲が広がる。
`UNBUFFERED` 属性:一途だが燃費の悪い暴れ馬
対して `UNBUFFERED` を指定すると、OSのバッファリング機構をバイパスするか、あるいは極めて限定的な制御となり、1回の `PUT` / `GET` ステートメントが、そのまま(あるいは極めてダイレクトに)物理的なI/O要求に直結しやすくなる。
- メリット: バッファ溢れや制御の複雑さから解放される…と言いたいところだが、実務でこれをメリットに感じる場面はまずない。
- デメリット: 圧倒的なI/Oネック。 数万件のファイルを処理させようものなら、EXCP(入出力実行回数)が跳ね上がり、あっという間にCPU時間を食いつぶしてオペレータから怒りの連絡が飛んでくる。
—
2. VSAMやQSAMファイルとの絡みと実務上の罠
メインフレームのバッチ処理では、順次ファイル(QSAM)やVSAM(KSDS/ESDS)を相手にすることが多い。ここで勘違いしやすいのが、「VSAMだからSTREAMではなくRECORDを使わなきゃいけない」という思い込みだ。
ログ出力や簡単なテキストレポートの生成などでは、依然としてSTREAMファイルがバリバリ現役で使われている。
特に、夜間バッチの集計結果をテキストとして吐き出す際、無駄に `UNBUFFERED`(あるいはそれに近い暗黙の挙動)を引き起こすコードを書いていると、データ量が増加した途端に性能劣化のボトルネックになる。
さらに恐ろしいのが、バッファ溢れ(Buffer Overflow) だ。
レコード長を超えるデータを無理やりストリームに流し込もうとしたり、レコード・フォーマット(F/V/VBなど)の定義とバッファサイズがミスマッチを起こすと、コンパイラやOSレベルで致命的な例外が発生する。これをいかにハンドリングするか、それがプログラマの腕の見せ所というわけだ。
—
3. 実践!PL/Iストリーム入出力とONユニット制御のコード例
百聞は一見に如かずだ。実際に現場で使える、バッファ制御と例外処理(ONユニット)を考慮した堅牢なPL/Iプログラムのサンプルを見せておこう。
すべて大文字、適切なインデント、そして `SUBSTR` などの `BUILTIN` 関数を駆使した、保守性の高いコードだ。
STRP01: PROC OPTIONS(MAIN);
/————————————————————–/
/ 修正履歴: 202X.10.15 新規作成 /
/ 概要 : 大量ストリーム出力におけるBUFFERED属性の活用と /
/ 入出力エラー(ENDFILE / ERROR)の制御サンプル /
/————————————————————–/
DCL OUT_FILE FILE STREAM OUTPUT
BUFFERED; / 明示的にBUFFEREDを指定しI/O効率を最大化 /
DCL 1 W_DATA_REC,
5 W_ID CHAR(8),
5 W_NAME CHAR(30),
5 W_AMOUNT FIXED DEC(9,2);
DCL W_LINE_BUF CHAR(100);
DCL W_COUNT FIXED BIN(31) INIT(0);
/ 異常系および終了時のONユニット定義 /
ON ENDFILE(OUT_FILE) BEGIN;
PUT SKIP LIST(‘ 正常終了: すべてのデータ書き込みが完了しました ‘);
GOTO CLOSE_RTN;
END;
ON ERROR BEGIN;
PUT SKIP LIST(‘ 致命的エラー発生: 処理を中断します ‘);
/ ここでダンプ取得やエラーログ出力のルーチンを呼び出す /
EXIT;
END;
/ ファイルのオープン(BUFFEREDの恩恵を受ける準備) /
OPEN FILE(OUT_FILE) TITLE(‘RPTFILE’) OUTPUT;
PUT SKIP LIST(‘— 大量データ・ストリーム出力処理 開始 —‘);
/ メインのループ処理(ダミーデータ生成と書き込み) /
DO WHILE(W_COUNT < 50000);
W_COUNT = W_COUNT + 1;
/ BUILTIN関数や編集機能を活用したデータ整形 /
W_ID = 'ID' || REPEAT('0', 5 - LENGTH(TRIM(CHAR(W_COUNT))))
|| CHAR(W_COUNT);
W_NAME = 'CUSTOMER_' || CHAR(W_COUNT);
W_AMOUNT = W_COUNT 123.45;
/ 文字列の組み立て(ストリーム出力用バッファへ蓄積される) /
W_LINE_BUF = W_ID || ',' || W_NAME || ',' ||
PUT_STRING(W_AMOUNT); / 実際にはEDIT等を使用 /
/ STREAM出力: BUFFEREDのおかげで物理I/Oは極小化される /
PUT FILE(OUT_FILE) EDIT (W_LINE_BUF) (A);
/ 1万件ごとに進捗を出力(バッファのフラッシュポイントを意識) /
IF MOD(W_COUNT, 10000) = 0 THEN
PUT SKIP EDIT('処理件数: ', W_COUNT) (A, F(10));
END;
CLOSE_RTN:
CLOSE FILE(OUT_FILE);
PUT SKIP LIST('--- 処理正常終了 ---');
RETURN;
END STRP01;
---
4. ベテランからの現場の教訓(まとめ)
このコードを見て、「なんだ、当たり前のことじゃないか」と思ったかい?
違うんだよ。実際のレガシーシステム改修現場では、前任者が適当に書いたコードをコピペし回すうちに、本来 `BUFFERED` で処理されるべき巨大なCSV/テキスト出力ストリームに、余計な制御コードや非効率なフォーマット指定が混ざり込み、パフォーマンスがガタガタになっているケースが後を絶たない。
特に注意すべきポイントをもう一度おさらいしておこう。
1. 基本は `BUFFERED` を死守せよ
特別な理由(リアルタイムの同期書き込みがどうしても必要な場合など)がない限り、STREAM出力には `BUFFERED` を信頼し、OSのバッファ管理能力を最大限に引き出せ。
2. ONユニットでI/O異常をねじ伏せろ
バッファ溢れやディスクフル、その他の入出力エラーはいつだって突然やってくる。`ON ERROR` や `ON ENDFILE` を適切に配置し、異常時にシステムが暴走するのを防ぐのがプロの仕事だ。
3. CPU使用率が高いときはEXCPとバッファを疑え
もしお前が担当しているバッチジョブが「CPUバウンドなのに妙に時間がかかる」「I/O待ちが多い」と感じたら、まずはPL/Iのファイル定義とJCLのDCBパラメータ(BLKSIZEなど)、そして今回話したバッファリング属性を真っ先に疑うんだな。
基本に忠実であれ。そして、コードの裏側でメインフレームのハードウェアとOSがどう動いているのかを常に想像しながらキーボードを叩くこと。それができれば、お前も一人前のメインフレーム・アーキテクトに近づけるはずだ。さて、コーヒーでも飲んで次のタスクに取り掛かるとしようか。
