【実務・中級編】BUFFERED属性によるI/O効率化とバッファ管理 – PL/Iの基本構文とデータ制御実践ガイド

PL/IファイルI/Oの神髄:BUFFERED属性でバッチ処理を爆速化する裏技

おい、諸君。PL/Iのバッチ処理で、なんかI/Oがボトルネックになってねぇか? nocturnes.ai の連中みたいに、最新技術で「API叩きまくり!」とか言ってる連中には理解できねぇ、あの地味だけど超重要なファイルI/Oの話だ。特に、BUFFERED属性の使いこなしは、メインフレーム屋としての腕の見せ所なんだぜ。今日は、そのBUFFERED属性の基本から、どうすればメモリ消費を抑えつつI/Oを効率化できるのか、現場のリアルな経験を交えて語ってやろう。

なぜBUFFERED属性が重要なのか? I/Oの現実とPL/Iの設計思想

まず、なんでBUFFERED属性なんてものがPL/Iにあるのか、その背景を理解しとく必要がある。メインフレームのI/Oってのは、CPU処理と違って、ディスクやテープといった物理的なデバイスとのやり取りだから、どうしても時間がかかる。CPUがピュンピュン回ってる間に、I/O待ちで何十ミリ秒も待たされたら、そりゃシステム全体のパフォーマンスはガタ落ちだ。

そこで登場するのが「バッファリング」という考え方だ。これは、プログラムが直接ディスクにアクセスするのではなく、メモリ上に用意した「バッファ」という一時的な置き場を経由してデータを受け渡しする仕組みだ。

  • UNBUFFERED属性: プログラムがREAD/WRITE命令を発行するたびに、直接ディスクとの間でデータのやり取りが発生する。1レコード処理するごとに、1回の物理I/Oが発生する可能性がある。これは、小規模なデータ処理や、リアルタイム性が求められる処理には向いている場合があるが、大量のレコードを扱うバッチ処理では、I/O回数が膨大になってしまう。
  • BUFFERED属性: PL/IのコンパイラやOSが、ディスクとプログラムの間にバッファ領域を用意してくれる。READ命令を発行しても、すぐにはディスクから読み込まず、バッファにデータがあればそこからプログラムに渡す。同様に、WRITE命令も、すぐにディスクに書き込まず、バッファがいっぱいになるまで溜め込んでから、まとめてディスクに書き出す。これにより、物理的なI/O回数を大幅に削減できるんだ。

PL/IがこのBUFFERED属性を標準で提供しているのは、まさにバッチ処理での効率化を強く意識した設計思想の表れなんだ。メインフレームで動く基幹システムは、夜間に大量のデータを処理するバッチ処理が主軸だからな。

PACKAGE、PROCEDURE、そしてOPTIONS(MAIN) – PL/Iプログラムの骨格

さて、BUFFERED属性を語る上で、PL/Iプログラムの基本構造もおさらいしておこう。

MYPROG: PROCEDURE OPTIONS(MAIN) EXTERNAL;
/ ここからプログラム本体 /

DECLARE …

/ ファイル定義 /
DECLARE INPUT_FILE FILE RECORD SEQUENTIAL ENV(REGIONAL(1) …); / 例 /
DECLARE OUTPUT_FILE FILE RECORD SEQUENTIAL ENV(REGIONAL(1) …); / 例 /

/ ファイル定義にBUFFERED属性を付与 /
DECLARE BUFFERED_FILE FILE RECORD SEQUENTIAL ENV(REGIONAL(1)) BUFFERED; / 例 /

/ バッファ管理用の変数 /
DECLARE BUFFER_VAR CHAR(XXXX); / レコード長に合わせる /

ON ENDFILE(INPUT_FILE) BEGIN;
/ ファイル終了時の処理 /
END;

ON ERROR … / エラーハンドリング /

/ ファイルオープン /
OPEN FILE(BUFFERED_FILE) INPUT STREAM; / STREAMはシーケンシャルアクセスの場合 /

/ データ処理ループ /
DO WHILE ( ‘1’B ); / 無限ループ /
GET FILE(BUFFERED_FILE) EDIT(BUFFER_VAR) ( … ); / EDITはストリームアクセス用 /
/ または /
READ FILE(BUFFERED_FILE) SET(BUFFER_VAR); / レコードアクセス用 /

/ バッファから読み込んだデータを処理 /
/ … /

/ ファイル終了判定(GET/READの後にENDFILE ON UNITが発火する) /
END;

/ ファイルクローズ /
CLOSE FILE(BUFFERED_FILE);

/ プログラム終了 /
RETURN;

MYPROG: END MYPROG;

  • `MYPROG: PROCEDURE OPTIONS(MAIN) EXTERNAL;`: プログラムのエントリーポイントを定義する。`OPTIONS(MAIN)`がないと、この手続きはメインプログラムとして実行されない。`EXTERNAL`は、他のプログラムから呼び出せることを示す(必須ではない場合もある)。
  • `DECLARE …`: 変数やファイルなどの宣言部分だ。
  • `DECLARE FILE … BUFFERED;`: ここが今日の主役。`FILE`宣言で、`BUFFERED`属性を指定することで、そのファイルに対するI/Oがバッファリングされるようになる。
  • `ON ENDFILE(…)`: ファイルの終端に達したときに実行されるONユニット。バッチ処理では、このONユニットでループを抜けるのが一般的だ。
  • `OPEN FILE(…)`: ファイルを使用する前にオープンする。`INPUT`や`OUTPUT`、`UPDATE`といったモードを指定する。`STREAM`はシーケンシャルファイル、`RECORD`はレコードファイルを指定する。
  • `GET` / `READ`: ファイルからデータを読み込む命令。`GET`はストリームアクセス(編集・コピー)、`READ`はレコードアクセス(構造化データ)でよく使われる。
  • `CLOSE FILE(…)`: ファイルの使用が終わったら必ずクローズする。

BUFFERED属性によるI/O効率化とバッファ管理の実際

BUFFERED属性を指定すると、PL/IコンパイラやOSのI/Oサポート機能が、指定されたバッファ領域を使ってディスクI/Oを管理してくれる。具体的には、以下のような動作になる。

  • READ時:

1. プログラムが`READ`命令を発行する。
2. まず、プログラムがアクセスできるバッファ領域にデータが格納されているか確認される。
3. バッファにデータがあれば、そこからプログラム変数にコピーされる。物理I/Oは発生しない。
4. バッファが空、または必要なデータがバッファにない場合、OSはディスクからバッファ領域にデータを読み込む。この時、一度に複数のレコード(またはブロック)が読み込まれることが多い(バッファサイズによる)。これが物理I/Oだ。
5. 読み込まれたデータから、プログラム変数にコピーされる。

  • WRITE時:

1. プログラムが`WRITE`命令を発行する。
2. プログラム変数のデータが、まずバッファ領域にコピーされる。
3. バッファがいっぱいになるまで、WRITE命令はディスクへの物理I/Oを発行しない。
4. バッファがいっぱいになったり、`CLOSE`命令が発行されたり、あるいは明示的に`FLUSH`(後述)が発行されたりすると、バッファの内容がまとめてディスクに書き込まれる。これが物理I/Oだ。

メリット:物理I/O回数の激減

BUFFERED属性の最大のメリットは、物理I/O回数を劇的に減らせることだ。例えば、1レコードあたり100バイトのファイルで、バッファサイズが8KB(8192バイト)だとすると、

  • UNBUFFEREDの場合: 100レコード処理するごとに、100回の物理I/Oが発生する可能性がある(ディスクのブロックサイズやOSの制御による)。
  • BUFFEREDの場合: 8192バイト ÷ 100バイト/レコード ≒ 81レコード。つまり、約81レコードを読み込む(または書き込む)ごとに、1回の物理I/Oで済むことになる。これは、I/O処理のオーバーヘッドが100倍以上削減されることを意味する!

デメリット:メモリ消費量とリアルタイム性

もちろん、いいことばかりではない。BUFFERED属性を使うと、その分メモリ上にバッファ領域を確保する必要がある。大量のファイルをBUFFEREDで開いたり、バッファサイズを極端に大きくしたりすると、プログラム全体のメモリ消費量が増加する。

また、 BUFFERED属性は「遅延書き込み」という性質を持つため、プログラムが異常終了した場合などに、バッファに残っていたデータがディスクに書き込まれずに失われるリスクがある。UNBUFFERED属性は、WRITE命令発行と同時にディスクに書き込むため、このリスクは低い。

VSAMファイルとBUFFERED属性

VSAM(Virtual Storage Access Method)ファイルは、メインフレームでよく使われるファイル組織の一つだ。特に、KSDS(Key Sequenced Data Set)やRRDS(Relative Record Data Set)など、インデックスや相対レコード番号でアクセスするファイルでは、BUFFERED属性の使い方がパフォーマンスに大きく影響する。

VSAMファイルの場合、BUFFERED属性を指定すると、OSはKSDSのキー領域、レコード領域、およびインデックスブロックなどをまとめてバッファリングしようとする。これにより、ディスクアクセスの回数が減り、レコードの検索や更新が高速化される。

ただし、VSAMファイルでBUFFERED属性を使う場合、バッファサイズ(OSのJCLやDDステートメントで指定されることが多い)と、レコード長、インデックス構造などを考慮して、最適なチューニングが必要になる。闇雲にBUFFEREDにするだけでは、かえってパフォーマンスが悪化することもあるので注意が必要だ。

ONユニットによるバッファ管理とエラーハンドリング

BUFFERED属性を使う場合、ONユニットの制御はさらに重要になる。

`ENDFILE` ONユニット

これは、ファイル終端に達したことを検知するためのものだ。BUFFERED属性の場合、バッファにデータが残っている状態で`GET`や`READ`命令が発行され、EOF(End Of File)に達すると、ON ENDFILEユニットが実行される。

DCL INPUT_FILE FILE RECORD SEQUENTIAL BUFFERED;
DCL IN_REC CHAR(100);

OPEN FILE(INPUT_FILE) INPUT;

ON ENDFILE(INPUT_FILE) BEGIN;
PUT SKIP LIST(‘ファイル終端に到達しました。’);
/ ここでループを抜けるためのフラグを立てたり、処理を終了させたりする /
GO TO END_PROCESSING; / 例 /
END;

DO WHILE(‘1’B);
READ FILE(INPUT_FILE) SET(IN_REC);
/ IN_RECのデータを処理 /
PUT SKIP LIST(‘処理レコード: ‘, IN_REC);
END;

END_PROCESSING:
CLOSE FILE(INPUT_FILE);

`ERROR` ONユニット

ファイルI/Oでエラーが発生した場合に実行されるONユニットだ。BUFFERED属性の場合、エラーがディスクI/Oそのものに起因するのか、バッファ管理に起因するのかを切り分けるのが難しい場合がある。

DCL OUTPUT_FILE FILE RECORD SEQUENTIAL BUFFERED;
DCL OUT_REC CHAR(80);

OPEN FILE(OUTPUT_FILE) OUTPUT;

ON ERROR BEGIN;
PUT SKIP LIST(‘ファイルI/Oエラーが発生しました!’);
PUT SKIP LIST(‘SYSPRINTにエラー情報を出力します。’);
/ 発生したエラーコードなどを取得してログに出力する /
/ 例: SYSPRINTへの出力 /
PUT FILE(SYSPRINT) SKIP LIST(‘ERROR: ‘, PLIRETC); / PLIRETCはエラーコード /
/ プログラムを異常終了させるか、リカバリを試みる /
SIGNAL ERROR; / エラーを再通知 /
END;

/ … データ書き込み処理 … /

/ 例: バッファがいっぱいになる前に強制的に書き出す(FLUSH) /
/ PL/Iの標準機能には直接的なFLUSH命令はないが、 /
/ READ命令を発行することで、バッファの内容が書き出される場合がある。 /
/ または、CLOSE命令で強制的に書き出される。 /

CLOSE FILE(OUTPUT_FILE);

`FLUSH` について

PL/Iの標準言語仕様には、明示的な`FLUSH`命令は存在しない。しかし、`WRITE`命令で書き込んだデータがバッファに溜まった状態でも、次に`READ`命令を発行すると、OSはバッファの内容をディスクに書き出してから、新しいデータをバッファに読み込む挙動をとることが多い。また、`CLOSE`命令を発行した際には、バッファに残っているデータは必ずディスクに書き出される。

バッチ処理の途中で、どうしてもバッファの内容をディスクに確定させたい、という稀なケースでは、ダミーの`READ`命令を発行したり、あるいは`CLOSE`して再度`OPEN`するという荒業を使うこともあるが、これはシステムへの負荷も大きいため、通常は避けるべきだ。

実践的なコード例:レコード入力と集計処理

ここでは、BUFFERED属性を使って、入力ファイルを読み込み、簡単な集計を行うPL/Iプログラムの例を示す。

BATCHJOB: PROCEDURE OPTIONS(MAIN) EXTERNAL;
/

  • このプログラムは、BUFFERED属性を指定した入力ファイルから
  • レコードを読み込み、指定された項目を集計する。
  • I/O効率化のためにBUFFERED属性を活用する。

/

DCL 1 IN_RECORD,
2 ITEM_CODE CHAR(10), / 商品コード /
2 ITEM_NAME CHAR(30), / 商品名 /
2 QUANTITY PIC ‘9(5)’, / 数量 /
2 PRICE PIC ‘9(7)V99’; / 価格 (小数点以下2桁) /

DCL TOTAL_QUANTITY PIC ‘9(10)V9’; / 総数量 (小数点以下1桁で桁上がり対応) /
DCL TOTAL_AMOUNT PIC ‘9(15)V99’; / 総金額 /
DCL RECORD_COUNT PIC ‘9(9)’; / 処理レコード数 /

/ 入力ファイル定義 (BUFFERED属性を指定) /
DCL INPUT_FILE FILE RECORD SEQUENTIAL ENV(REGIONAL(1)) BUFFERED;

/ 出力ファイル定義 (SYSPRINTを使用) /
DCL SYSPRINT FILE STREAM OUTPUT;

/ ファイル終端検知用フラグ /
DCL EOF_FLAG BIT(1) INIT(‘0’B);

/ 初期化 /
TOTAL_QUANTITY = 0;
TOTAL_AMOUNT = 0;
RECORD_COUNT = 0;

/ ファイルオープン /
OPEN FILE(INPUT_FILE) INPUT;
PUT FILE(SYSPRINT) SKIP(2) LIST(‘— バッチ処理開始 —‘);

/ ファイル終端時の処理を定義 /
ON ENDFILE(INPUT_FILE)
BEGIN;
EOF_FLAG = ‘1’B; / フラグを立てる /
/ ここでバッファに残っているデータを処理する必要がある場合もあるが、 /
/ READ命令でEOFに達した場合は、通常、バッファのデータはすべて読み込まれている。 /
END;

/ データ処理ループ /
DO WHILE ( EOF_FLAG = ‘0’B );

/ ファイルからレコードを読み込む (BUFFEREDなのでI/Oは効率化されている) /
READ FILE(INPUT_FILE) SET(IN_RECORD);

/ ON ENDFILEユニットが発火した場合、ループを抜ける /
IF EOF_FLAG THEN
LEAVE;
END;

/ 読み込んだレコードの処理 /
RECORD_COUNT = RECORD_COUNT + 1;

/ 数量と金額を集計 /
TOTAL_QUANTITY = TOTAL_QUANTITY + QUANTITY;
TOTAL_AMOUNT = TOTAL_AMOUNT + (QUANTITY PRICE); / 四則演算はPL/Iが得意 /

/ 処理中のレコード数を表示 (デバッグ用、実際は不要な場合も) /
IF MOD(RECORD_COUNT, 1000) = 0 THEN / 1000レコードごとに表示 /
PUT FILE(SYSPRINT) SKIP LIST(‘処理レコード数: ‘, RECORD_COUNT);
END;

END; / WHILEループ終了 /

/ ファイルクローズ (バッファに残っているデータはここでディスクに書き出される) /
CLOSE FILE(INPUT_FILE);

/ 集計結果の出力 /
PUT FILE(SYSPRINT) SKIP(2) LIST(‘— 集計結果 —‘);
PUT FILE(SYSPRINT) SKIP LIST(‘総処理レコード数: ‘, RECORD_COUNT);
PUT FILE(SYSPRINT) SKIP LIST(‘総数量: ‘, TOTAL_QUANTITY);
PUT FILE(SYSPRINT) SKIP LIST(‘総金額: ‘, TOTAL_AMOUNT);

PUT FILE(SYSPRINT) SKIP LIST(‘— バッチ処理終了 —‘);

/ プログラム終了 /
RETURN;

END BATCHJOB;

このコード例では、

  • `DCL INPUT_FILE FILE RECORD SEQUENTIAL ENV(REGIONAL(1)) BUFFERED;` で、入力ファイルをBUFFERED属性付きで定義しています。`SEQUENTIAL`はシーケンシャルアクセス、`ENV(REGIONAL(1))`はVSAMのRD(Regional)データセットを想定していますが、通常のPS(Physical Sequential)データセットでも構いません。
  • `READ FILE(INPUT_FILE) SET(IN_RECORD);` でレコードを読み込んでいます。BUFFERED属性のおかげで、この`READ`命令の発行回数よりも、実際のディスクI/Oの発生回数は格段に少なくなっています。
  • `ON ENDFILE(INPUT_FILE)` でファイルの終端を検知し、`EOF_FLAG`をセットしてループを抜けるようにしています。
  • `PUT FILE(SYSPRINT)` で、処理の進捗や最終結果を標準出力(SYSPRINT)に出力しています。

まとめ:BUFFERED属性は諸刃の剣、賢く使え!

BUFFERED属性は、PL/Iのバッチ処理においてI/Oパフォーマンスを劇的に向上させる強力な武器だ。しかし、その反面、メモリ消費量の増加や、異常終了時のデータロスリスクといったデメリットも存在する。

  • 大量のレコードを処理するバッチ処理: BUFFERED属性はほぼ必須。
  • リアルタイム性が求められる処理、またはメモリリソースが極端に制限されている場合: UNBUFFERED属性や、より細かいI/O制御が必要になる場合がある。
  • VSAMファイル: バッファリングは効果的だが、チューニングが重要。

今回の解説を参考に、君たちの担当するバッチ処理のパフォーマンスチューニングに、ぜひBUFFERED属性を効果的に活用してほしい。で、もし「いや、うちのシステムではBUFFEREDにすると逆に遅くなるんだけど?」なんていう疑問が出てきたら、それはきっと、バッファサイズ、レコード長、VSAMのAC(Allocate Control)パラメータ、あるいはJCLでのDCBパラメータ設定なんか、もっと深いレベルでのチューニングが必要なケースだろう。その辺の話は、また別の機会にでも語ってやろう。

現場の諸君、PL/Iのコードは、単に動けばいいってもんじゃない。効率と堅牢性を両立させてこそ、真のメインフレームエンジニアだ。頑張ってくれよ!

タイトルとURLをコピーしました