【実務・中級編】FIXED BINARYの内部表現とストレージ占有量 – PL/Iの基本構文とデータ制御実践ガイド

メインフレームのPL/Iエンジニアの諸君、ご苦労!
今日もまた、基幹システムの健全な稼働のために、日夜奮闘してくれていることだろう。

さて、今日はPL/Iの根幹をなすデータ型の一つ、「`FIXED BINARY`」について、その深淵を覗き込むとしよう。お前たちの中には「なんだ、`FIXED BINARY`なんて基本中の基本じゃないですか」と思う者もいるかもしれない。だがな、メインフレームの現場では、この「基本」にこそ、システムの安定性やパフォーマンスを左右する重大な秘密が隠されているんだ。

単なる言語仕様の解説書を読むだけじゃ分からない、血と汗と涙の詰まった現場の知見を、今日はお前たちに伝授する。しっかりと耳を傾けろよ。

PL/I FIXED BINARYの深淵:内部表現とアライメントが織りなすメインフレームの真実

1. 「`FIXED BINARY`」は、なぜ奥が深いのか?

お前たちもよく知っているように、`FIXED BINARY`はPL/Iにおける符号付き整数値を扱うためのデータ型だ。`FIXED BINARY(p)`と宣言することで、`p`で指定された精度(桁数)を持つバイナリ整数として変数を定義する。

だがな、これが単なる整数型だと甘く見てはいけない。メインフレームの世界では、この`FIXED BINARY`の内部表現、特にストレージの占有量メモリ上でのアライメント(配置)を理解しているかどうかが、後々大きなトラブルを回避できるかどうかの分かれ目になるんだ。

  • パフォーマンス: 正しいアライメントはCPUがデータを効率的にアクセスするために不可欠だ。
  • ストレージ効率: 無駄なパディングを避け、限られたメモリを有効活用する。
  • 他言語連携: COBOLやアセンブラ、あるいは外部のDBとの連携時に、データのレイアウト齟齬を防ぐ。
  • デバッグ: 問題発生時に、メモリダンプを読んだり、`HEX`関数で内部表現を確認したりする上で、この知識は必須中の必須だ。

いいか、お前たちが将来、基幹システムの重責を担うときに、必ずこの知識が役に立つ。今日、ここでその基礎を徹底的に叩き込んでやるから、覚悟しろ。

2. `FIXED BINARY(p)`の基本:符号と2の補数

まず、`FIXED BINARY`がどのように数値を表現するかを再確認しよう。

`FIXED BINARY`は、2の補数表現を用いる。これは正の数、負の数、そしてゼロを効率的に表現するための標準的な手法だ。

  • 正の数: 最上位ビット(符号ビット)が`0`で、残りのビットで絶対値を表現する。
  • 負の数: 最上位ビット(符号ビット)が`1`で、絶対値のビットを反転し、`1`を加えることで表現する。これにより、負の数と正の数を加算するとゼロになる(オーバーフローを無視すれば)という便利な性質を持つ。

この2の補数表現こそが、`FIXED BINARY`が持つ、最大値と最小値を理解する鍵となる。

3. ストレージ占有量とアライメントの核心

さあ、ここからが本題だ。`FIXED BINARY(p)`の`p`の値によって、変数が占めるストレージのバイト長と、メモリ上でのアライメントが変わってくる。

IBMメインフレームのPL/Iでは、通常、以下のルールに従う。

  • `FIXED BINARY(p)` で `p` が 1~15の場合:
  • 2バイト(ハーフワード)を占有する。
  • ハーフワード境界(2バイト単位)にアライメントされる。
  • 表現可能な値の範囲: -32768 ~ +32767
  • `FIXED BINARY(p)` で `p` が 16~31の場合:
  • 4バイト(フルワード)を占有する。
  • フルワード境界(4バイト単位)にアライメントされる。
  • 表現可能な値の範囲: -2,147,483,648 ~ +2,147,483,647
  • `FIXED BINARY(p)` で `p` が 32~63の場合:
  • 8バイト(ダブルワード)を占有する。
  • ダブルワード境界(8バイト単位)にアライメントされる。
  • 表現可能な値の範囲: 約 -9 x 10^18 ~ +9 x 10^18

いいか、ここが肝心だ。PL/Iの変数は、特に構造体(`STRUCTURE`)のメンバーとして宣言された場合、デフォルトで`ALIGNED`属性を持つ。これは、コンパイラが自動的に、そのデータ型が要求する自然な境界に合うようにメモリを配置してくれるということだ。

なぜアライメントが重要なのか?

1. CPUの効率: メインフレームのCPUは、ハーフワードはハーフワード境界、フルワードはフルワード境界に配置されているデータを一度に読み込むのが最も効率が良い。もしデータが境界をまたいで配置されていると(`UNALIGNED`)、CPUは複数回に分けて読み込み、結合する処理が必要になり、パフォーマンスが著しく低下する。
2. データ整合性: 特に`RECORD` I/Oで外部ファイルやDBから直接レコードを読み込む場合、レコードの物理的なレイアウトとPL/Iの構造体定義が一致している必要がある。アライメントのルールを理解していなければ、レコードのフィールドがずれてしまい、全く異なる値を読み込んでしまうことになる。
3. 他言語連携: COBOLの`COMP`や`COMP-5`データ項目との連携では、バイト長だけでなくアライメントも一致させる必要がある。

例えば、こんな構造体を考えてみよう。

DCL 1 MY_RECORD,
2 FIELD_A FIXED BINARY(31), / フルワード (4バイト) /
2 FIELD_B FIXED BINARY(15), / ハーフワード (2バイト) /
2 FIELD_C FIXED BINARY(31); / フルワード (4バイト) /

この構造体は、メモリ上ではおそらく以下のように配置されるだろう。

| オフセット | 長さ (バイト) | データ項目 | 備考 |
| :——— | :———— | :——— | :— |
| 0 | 4 | `FIELD_A` | フルワード境界 (0, 4, 8…) |
| 4 | 2 | `FIELD_B` | ハーフワード境界 (4, 6, 8…) |
| 6 | 2 | (パディング) | `FIELD_C`がフルワード境界にアライメントされるためのパディング |
| 8 | 4 | `FIELD_C` | フルワード境界 (8, 12…) |

`FIELD_B`の後に2バイトのパディングが挿入されているのがわかるか? これは`FIELD_C`がフルワードであり、その次のフルワード境界(オフセット8)に配置される必要があるからだ。もしこのパディングを考慮せずに、この構造体を基に外部ファイルレコードを定義したり、COBOLプログラムと連携させたりすると、`FIELD_C`以降のデータがすべてずれてしまい、とんでもないことになる。

「PL/Iコンパイラが勝手にやってくれるから大丈夫」ではない。その「勝手にやってくれる」メカニズムを理解していなければ、いざという時にデバッグのしようがないんだ。

4. 現場での実践とトラブルシューティングのコツ

4.1. オーバーフローの監視と対処 (`SIZE`条件とONユニット)

`FIXED BINARY`で最もよく遭遇する問題の一つがオーバーフローだ。例えば`FIXED BINARY(15)`の変数に`32768`を代入しようとすれば、それは表現可能な範囲を超えるため、オーバーフローが発生する。

このとき、PL/Iは`SIZE`条件を発生させる。デフォルトでは、`SIZE`条件はプログラムを異常終了させるが、これを`ON UNIT`で捕捉し、適切にハンドリングすることが非常に重要だ。

/ 悪い例:オーバーフローで異常終了の危険性 /
DCL SHORT_BIN FIXED BINARY(15);
SHORT_BIN = 32767;
SHORT_BIN = SHORT_BIN + 1; / ここでSIZE条件が発生し、デフォルトではABEND /

/ 良い例:ON UNITでオーバーフローをハンドリング /
PACKAGE MYPKG;

MYPROG: PROCEDURE OPTIONS(MAIN);

DCL SHORT_BIN FIXED BINARY(15);
DCL LONG_BIN FIXED BINARY(31);
DCL ERROR_FLAG BIT(1) INIT(‘0’B);

/ SIZE条件が発生した場合のON UNITを定義 /
ON SIZE BEGIN;
PUT SKIP LIST(‘ ERROR: SIZE CONDITION DETECTED! ‘);
PUT SKIP LIST(‘ Variable causing error: SHORT_BIN’);
PUT SKIP LIST(‘ Current value (before overflow): ‘ || SHORT_BIN);
/ ここでエラーフラグを立てるか、GOTOでエラー処理ルーチンに飛ぶ /
ERROR_FLAG = ‘1’B;
REVERT SIZE; / ON UNITを終了し、SIZE条件が再発生しないようにする /
END;

SHORT_BIN = 32767;
PUT SKIP LIST(‘SHORT_BIN (Initial) = ‘ || SHORT_BIN); / 32767 /

SHORT_BIN = SHORT_BIN + 1; / SIZE条件発生!ON UNITが実行される /

IF ERROR_FLAG THEN DO;
PUT SKIP LIST(‘ Processing stopped due to overflow.’);
/ エラー処理、ログ出力、プログラム終了などのロジック /
GOTO END_OF_PROGRAM;
END;

/ オーバーフローが発生しなかった場合の処理 /
PUT SKIP LIST(‘SHORT_BIN (After increment) = ‘ || SHORT_BIN);

/ 別の変数でオーバーフローしない例 /
LONG_BIN = 2147483647;
PUT SKIP LIST(‘LONG_BIN (Initial) = ‘ || LONG_BIN);
LONG_BIN = LONG_BIN + 1; / ここではSIZE条件は発生しない(2の補数の最大値を超えていない) /
PUT SKIP LIST(‘LONG_BIN (After increment) = ‘ || LONG_BIN); / 表現可能な最小値 -2147483648 になる /
/ これはSIZE条件ではなく、ラップアラウンド動作 /

/ BUILTIN関数を使った事前チェック /
IF (SHORT_BIN > MAXVAL(FIXED BINARY(15)) – 1) THEN DO; / +1でオーバーフローするかどうかを事前にチェック /
PUT SKIP LIST(‘ WARNING: SHORT_BIN will overflow if incremented! ‘);
END;

END_OF_PROGRAM:;
PUT SKIP LIST(‘Program completed.’);

END MYPROG;
END MYPKG;

`MAXVAL`や`MINVAL`といった`BUILTIN`関数を活用すれば、計算前にオーバーフローの可能性をチェックすることも可能だ。現場では、これらの防御策を幾重にも張り巡らせることが、安定稼働への道となる。

4.2. 他言語(COBOL)連携時の注意

COBOLプログラムとのデータ連携は、メインフレーム開発の日常茶飯事だろう。特に`FIXED BINARY`に相当するCOBOLのデータ項目とのマッピングは重要だ。

  • `FIXED BINARY(15)` は、COBOLの `PIC S9(4) COMP` または `PIC S9(n) COMP-5` で、長さ2バイトのものに相当する。
  • `FIXED BINARY(31)` は、COBOLの `PIC S9(9) COMP` または `PIC S9(n) COMP-5` で、長さ4バイトのものに相当する。

`COMP` は通常、システムが指定する自然なバイナリ表現(IBM環境ではビッグエンディアンの2の補数)だが、`PIC S9(n)`の`n`の値によってハーフワードかフルワードかが決まる。
`COMP-5` は、より直接的にバイナリ表現を指定するもので、`n`の桁数ではなく、バイト長でデータ表現が決まる。

どちらにせよ、PL/Iの`FIXED BINARY`と連携する際は、バイト長とアライメントが完全に一致しているかを、必ず双方のコンパイラオプションやデータ定義マニュアルで確認しろ。アライメントがずれていると、読み込む値が全く異なってしまう。

4.3. レコード入出力とアライメントの齟齬

`RECORD` I/Oを用いてファイルを扱う際、外部ファイルのレコードレイアウトとPL/Iの構造体(`STRUCTURE`)定義は鏡のように一致していなければならない。アライメントの知識がここで活きてくる。

もし外部ファイルが`UNALIGNED`なデータ項目を含んでいたり、PL/Iの自然なアライメントルールと異なるレイアウトを持っていたりする場合、PL/I側で`UNALIGNED`属性を明示的に指定したり、`UNION`を使ってオーバーレイ構造を作成したりする必要が出てくることもある。

/ 外部ファイルがアライメントされていないレコードの場合の対応例 /
DCL 1 MY_UNALIGNED_RECORD UNALIGNED,
2 FIELD_X CHAR(3),
2 FIELD_Y FIXED BINARY(15), / UNALIGNEDのため、バイト境界に配置される /
2 FIELD_Z CHAR(1);

/ この場合、FIELD_YはFIELD_Xの直後、オフセット3から配置され、 /
/ ハーフワード境界ではないオフセット3に配置されることになる。 /
/ UNALIGNED属性はパフォーマンスに影響を与えるため、可能な限り避けるべきだが、 /
/ 外部ファイルの制約がある場合は必要となる。 /

このようなケースは稀だが、レガシーシステム改修では遭遇することがある。その時に慌てないためにも、基本のアライメントルールを理解しておくことは必須だ。

5. 実践コードで学ぶ`FIXED BINARY`:内部表現を暴く

では、実際にコードを書き、`FIXED BINARY`の内部表現とアライメントを確かめてみよう。
今回はVSAM KSDSファイルからレコードを読み込み、内部の`FIXED BINARY`項目をデバッグするシナリオだ。

PACKAGE MY_FIXED_BINARY_DEMO;

/ メインプロシージャ /
FIXBIN_MAIN: PROCEDURE OPTIONS(MAIN);

/ — 変数宣言部 — /
DCL MY_VSAM_FILE FILE RECORD INPUT ENVIRONMENT(VSAM KSDS); / VSAM KSDSファイル定義 /
DCL MY_VSAM_STATUS FIXED BINARY(31) INIT(0); / VSAMファイル操作結果ステータス /

/ VSAMレコードの構造体定義 /
/ ここでFIXED BINARYのバイト長とアライメントが適用される /
DCL 1 VSAM_REC ALIGNED, / デフォルトはALIGNEDだが明示的に指定 /
2 REC_KEY FIXED BINARY(31), / フルワード (4バイト) /
2 REC_SEQ_NO FIXED BINARY(15), / ハーフワード (2バイト) /
2 FILLER_1 CHAR(2), / FILLER_1 と FILLER_2 は、REC_AMOUNT のアライメント用 /
2 REC_AMOUNT FIXED BINARY(31), / フルワード (4バイト) /
2 REC_DATE CHAR(8),
2 REC_TIME CHAR(6),
2 REC_DESC CHAR(20);

DCL WS_RECORD_COUNT FIXED BINARY(31) INIT(0); / 処理レコード数カウンタ /
DCL WS_CALC_VALUE FIXED BINARY(31); / 演算用変数 /

/ — ON UNIT定義部 — /
/ VSAMファイルエラー発生時のON UNIT /
ON ENDFILE(MY_VSAM_FILE) BEGIN;
PUT SKIP LIST(‘ INFO: End of VSAM file reached. Total records processed: ‘ || WS_RECORD_COUNT);
GO TO END_PROGRAM; / ファイル終端に達したらプログラム終了へ /
END;

/ SIZE条件発生時のON UNIT /
ON SIZE BEGIN;
PUT SKIP LIST(‘ CRITICAL ERROR: SIZE CONDITION DETECTED for REC_AMOUNT! ‘);
PUT SKIP LIST(‘ Current REC_AMOUNT value (before overflow): ‘ || REC_AMOUNT);
PUT SKIP LIST(‘ The operation tried to store a value too large for FIXED BINARY(31).’);
PUT SKIP LIST(‘ Processing will be terminated.’);
GO TO ERROR_HANDLER; / エラー処理ルーチンへ /
END;

/ — メイン処理部 — /
PUT SKIP LIST(‘ Starting FIXED BINARY Demo Program ‘);

/ VSAMファイルオープン /
OPEN FILE(MY_VSAM_FILE) INPUT;
PUT SKIP LIST(‘VSAM file opened successfully.’);

/ レコード読み込みループ /
DO WHILE(‘1’B); / 無限ループ (ON ENDFILEで脱出) /
ON CODE(MY_VSAM_FILE) BEGIN;
/ VSAMファイルI/Oエラー発生時のON UNIT /
MY_VSAM_STATUS = ONCODE;
PUT SKIP LIST(‘ ERROR: VSAM I/O Error occurred! ONCODE = ‘ || MY_VSAM_STATUS);
PUT SKIP LIST(‘ Please check VSAM file status and JCL.’);
GO TO ERROR_HANDLER;
END;

READ FILE(MY_VSAM_FILE) INTO(VSAM_REC); / VSAMからレコードを読み込む /
WS_RECORD_COUNT = WS_RECORD_COUNT + 1;

/ 読み込んだレコードの内容を表示 (特にFIXED BINARY項目に注目) /
PUT SKIP DATA(WS_RECORD_COUNT);
PUT SKIP LIST(‘ REC_KEY (DEC) = ‘ || REC_KEY);
PUT SKIP LIST(‘ REC_KEY (HEX) = ‘ || HEX(REC_KEY)); / 内部表現を16進数で確認 /
PUT SKIP LIST(‘ REC_KEY (ADDR)= ‘ || ADDR(REC_KEY)); / メモリ上のアドレスを確認 /
PUT SKIP LIST(‘ REC_KEY (LEN) = ‘ || LENGTH(REC_KEY)); / 占有バイト数を確認 (4) /

PUT SKIP LIST(‘ REC_SEQ_NO (DEC) = ‘ || REC_SEQ_NO);
PUT SKIP LIST(‘ REC_SEQ_NO (HEX) = ‘ || HEX(REC_SEQ_NO)); / 内部表現を16進数で確認 /
PUT SKIP LIST(‘ REC_SEQ_NO (ADDR)= ‘ || ADDR(REC_SEQ_NO)); / メモリ上のアドレスを確認 /
PUT SKIP LIST(‘ REC_SEQ_NO (LEN) = ‘ || LENGTH(REC_SEQ_NO)); / 占有バイト数を確認 (2) /

PUT SKIP LIST(‘ REC_AMOUNT (DEC) = ‘ || REC_AMOUNT);
PUT SKIP LIST(‘ REC_AMOUNT (HEX) = ‘ || HEX(REC_AMOUNT)); / 内部表現を16進数で確認 /
PUT SKIP LIST(‘ REC_AMOUNT (ADDR)= ‘ || ADDR(REC_AMOUNT)); / メモリ上のアドレスを確認 /
PUT SKIP LIST(‘ REC_AMOUNT (LEN) = ‘ || LENGTH(REC_AMOUNT)); / 占有バイト数を確認 (4) /

/ REC_AMOUNTに対して計算を行い、SIZE条件を発生させる可能性のある処理 /
WS_CALC_VALUE = REC_AMOUNT 100; / 例として100倍する /

/ ここで意図的にオーバーフローを発生させてみる /
IF WS_RECORD_COUNT = 2 THEN DO; / 2番目のレコードで強制的にオーバーフロー /
/ FIXED BINARY(31)の最大値に非常に近い値を設定し、次の加算でオーバーフローさせる /
REC_AMOUNT = 2147483647 – 10; / 最大値より少し小さい値 /
PUT SKIP LIST(‘ INFO: Forcing REC_AMOUNT near max value for overflow test.’);
PUT SKIP LIST(‘ REC_AMOUNT (Forced) = ‘ || REC_AMOUNT);
REC_AMOUNT = REC_AMOUNT + 20; / ここでSIZE条件が発生するはず /
PUT SKIP LIST(‘ REC_AMOUNT (After forced add) = ‘ || REC_AMOUNT);
END;

PUT SKIP LIST(‘ Calculated value (WS_CALC_VALUE) = ‘ || WS_CALC_VALUE);
PUT SKIP LIST(‘—————————————————-‘);
END;

/ — エラーハンドリング部 — /
ERROR_HANDLER:
PUT SKIP LIST(‘!!! Program terminated due to an error. !!!’);
/ ここで適切なエラーログ出力やリソース解放を行う /
CLOSE FILE(MY_VSAM_FILE);
RETURN; / プログラムを異常終了させる /

/ — プログラム終了部 — /
END_PROGRAM:
CLOSE FILE(MY_VSAM_FILE); / ファイルを閉じる /
PUT SKIP LIST(‘ Program Finished Normally ‘);

END FIXBIN_MAIN;
END MY_FIXED_BINARY_DEMO;

コード解説のポイント:

1. `DCL 1 VSAM_REC ALIGNED`: 構造体全体が`ALIGNED`であることを明示している。これにより、各メンバーはそれぞれのデータ型が要求する自然な境界に配置される。
2. `REC_KEY FIXED BINARY(31)`: フルワード(4バイト)で宣言。`HEX(REC_KEY)`でダンプを見ると、4バイトの16進数表現が確認できるだろう。`ADDR(REC_KEY)`で表示されるアドレスも、4の倍数になるはずだ。
3. `REC_SEQ_NO FIXED BINARY(15)`: ハーフワード(2バイト)で宣言。`HEX(REC_SEQ_NO)`は2バイトの16進数表現、`ADDR(REC_SEQ_NO)`は2の倍数になる。
4. `FILLER_1 CHAR(2)`: ここがポイントだ。`REC_SEQ_NO`が2バイト、その後の`REC_AMOUNT`が4バイトのフルワードだ。`REC_AMOUNT`がフルワード境界にアライメントされるためには、`REC_SEQ_NO`の直後(オフセット6)から`REC_AMOUNT`が始まることはできない。オフセット8から始まる必要があるため、間に2バイトのパディングが必要になる。この`FILLER_1`はそのパディングを明示的に表現しているものだ。もし`FILLER_1`がなければ、PL/Iコンパイラが自動的にパディングを挿入するが、その事実を知るためには`ADDR`関数などで確認する必要がある。
5. `ON SIZE`ユニット: `REC_AMOUNT`を意図的にオーバーフローさせる処理を入れ、`ON SIZE`ユニットがどのように機能するかを示している。現場ではこのような防御策が不可欠だ。
6. `HEX()`, `ADDR()`, `LENGTH()` BUILTIN関数: これらはデバッグの三種の神器と言っても過言ではない。

  • `HEX(変数)`: 変数の内容を16進数でダンプ表示する。メモリ上の実際のバイト列を確認できる。
  • `ADDR(変数)`: 変数のメモリ上の開始アドレスを取得する。これでアライメントされているかを確認できる。
  • `LENGTH(変数)`: 変数のバイト長を取得する。これで占有ストレージが正しいかを確認できる。

このコードを実行し、出力される`HEX`や`ADDR`の値を見れば、`FIXED BINARY`がどのようにメモリ上で「生きているか」が手に取るようにわかるだろう。特に`REC_SEQ_NO`と`REC_AMOUNT`のアドレスの差を注意深く見てみろ。`FILLER_1`の存在意義が理解できるはずだ。

6. 結び:未来のメインフレームを頼んだぞ

どうだ、`FIXED BINARY`が単なる整数型ではなかったことが理解できたか?

この知識は、単にPL/Iの文法を覚える以上の意味を持つ。メインフレームという限られたリソースの中で、最大限のパフォーマンスを引き出し、堅牢なシステムを構築し、そして何よりも未来へと引き継がれる基幹システムの品質を保証するために不可欠な知見なんだ。

お前たちが今後、大規模なバッチ改修やマイグレーション案件に携わる際、この`FIXED BINARY`の内部表現とアライメントの知識は、必ずや強力な武器となるだろう。メモリダンプとにらめっこしたり、他言語とのインターフェースで頭を抱えたりする時に、今日の話が頭の片隅にでもあれば、きっと解決への糸口が見つかるはずだ。

これからも、メインフレームの深い知識を貪欲に吸収し、頼れるエンジニアへと成長してくれ。
期待しているぞ、後輩たち!

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