【テクニカル・上級編】FIXED DECIMALのパック10進数形式(COMP-3) – PL/Iの基本構文とデータ制御実践ガイド

FIXED DECIMALの深層:パック10進数(COMP-3)のメモリ構造と、Java/C#マイグレーションの罠

メインフレームの現場において、勘定系システムのコアを支えるデータ型といえば、何と言っても `FIXED DECIMAL(p, q)` である。COBOLerであればお馴染みの `COMP-3`(パック10進数)、PL/Iの世界では単に `FIXED DEC` または `DECIMAL` として定義されるこのデータ型は、金融計算における丸め誤差を完全に排除するために不可欠な存在だ。

しかし、現代のオープン系言語(Javaの `BigDecimal` や C#の `decimal`)へのマイグレーションや、深夜バッチの異常終了(S0C7アベンド)の解析において、このパック10進数の内部表現と符号処理のメカニズムを正確に理解しているエンジニアは驚くほど少ない。

今回は、IBM Z(z/Architecture)のハードウェアレベルでのストレージ構造から、PL/Iコンパイラの最適化挙動、さらにはDB2やCICSを絡めた実務でのエッジケースまで、システムアーキテクトの視点で深く掘り下げていこう。

—

1. 内部メモリ構造:なぜ 奇数桁(p) なのか?

まず、`FIXED DECIMAL(p, q)` がメモリ上でどのように表現されるかのおさらいだ。
パラメータの `p` は全桁数(精度)、`q` は小数点以下の桁数(スケール)を指す。

IBMメインフレームにおいて、パック10進数は 1バイトあたり2桁の数字 を格納し、最後の4ビット(下位ニブル)に 符号(Sign) を保持する。この仕様から導き出される重要な公式がある。

> 必要バイト数 = 整数部(p – q) と 小数点部(q) の和の桁数に 1 を加えたものを 2 で割って切り上げ
> ついでに言えば、定義上の有効桁数 `p` は 奇数 になることが多い(偶数で定義しても、コンパイラは内部的に1桁増やして奇数として扱う)。

例えば、`DCL WS-AMT FIXED DEC(9, 2);` という変数を定義した場合:

  • 全桁数 $p = 9$
  • 整数部 $9 – 2 = 7$ 桁
  • 小数点部 $q = 2$ 桁
  • 総桁数 = $7 + 2 = 9$ 桁
  • バイト数 = $(9 + 1) / 2 = 5$ バイト

この5バイトのメモリ上では、以下のようにデータが並ぶ。

[ 0 ] [ 1 ] [ 2 ] [ 3 ] [ 4 ] <-- バイトオフセット +-----+-----+-----+-----+-----+ | 12 | 34 | 56 | 78 | 9C | <-- 16進数表現(例: +123456789) +-----+-----+-----+-----+-----+ ↑ ↑ ↑ ↑ | +-- 下位ニブル 'C' は正符号(Positive) | | | | +----- 上位ニブル '9' +---- 1バイトに2桁のBCD(Binary-Coded Decimal) 最後のバイトの下位ニブルに格納される符号の標準的なIBMハードウェア表現は以下の通りだ。

  • 正符号 (Positive): `C`, `A`, `F`, `E` (通常は生成・出力時に `C` が使われる)
  • 負符号 (Negative): `D`, `B` (通常は `D` が使われる)
  • 符号なし (Unsigned): `F`

—

2. ベース変数とポインタによる動的メモリ操作のエッジケース

レガシーなPL/Iプログラム、特に大規模な共通ルーチンや外部インターフェース(BTAM/VTAMや独自のファイル構造)を扱うコードでは、`POINTER` と `BASED` 変数を用いたストレージの直接操作が頻繁に行われる。

ここで発生しやすいのが、「パック10進数のアラインメントおよび符号不正によるS0C7アベンド」 である。以下のコード例を見てほしい。

—————————————————————–

  • BASED変数を用いたパック10進数の動的マッピングと符号検証の例

—————————————————————–
RAW_DATA_AREA: PROC OPTIONS(MAIN);

DCL P_BUF POINTER;
DCL 1 TARGET_RECORD BASED(P_BUF),
3 REC_ID CHAR(4),
3 REC_AMT FIXED DEC(7,2); セールス金額 (4バイト領域)

DCL WORK_HEX CHAR(1) LOCAL;
DCL SIGN_NIBBLE BIT(4);
DCL DIGIT_NIBBLE BIT(4);

— 外部から取得したストレージのアドレスをポインタにセット(省略)

  • P_BUF = GET_STORAGE_ADDRESS();

— 【罠】もし外部I/Fのデータが不正(例:ゾーン混入や符号ビット破壊)の場合

  • ここでそのまま演算を行うと、ハードウェア例外(S0C7)が発生する。
  • 最後のバイトの符号ニブルを安全に検査する処理

SIGN_NIBBLE = SUBSTR(UNSPEC(REC_AMT), 33, 4); / 4バイトの最後の4ビット /

SELECT(SIGN_NIBBLE);
WHEN(‘1100’B, ‘1111’B, ‘1010’B, ‘1110’B) / C, F, A, E /
PUT SKIP LIST(‘正の符号は正常です’);
WHEN(‘1101’B, ‘1011’B) / D, B /
PUT SKIP LIST(‘負の符号を検出しました’);
OTHER
PUT SKIP EDIT (‘【致命的エラー】無効な符号ビット:’, UNSPEC(REC_AMT)) (A, X(1), B);
SIGNAL ERROR; / 意図的なアベンドまたは例外処理へ誘導 /
END;

END RAW_DATA_AREA;

アーキテクトとして特筆すべきは、`UNSPEC` 組み込み関数を用いたビットレベルの検査だ。PL/Iはデータ型の安全性をコンパイル時に厳しくチェックするが、`BASED` 変数と組み合わされたポインタ操作は、C言語の `void` と同様の危険なパワーを秘めている。外部から流れ込んできた生データ(Raw Data)が文字コード(EBCDIC)のスペース(`X’40’`)のまま `FIXED DEC` としてロードされ、それを加算命令(AP命令など)にかけた瞬間に、容赦なく `S0C7(Data Exception)` が飛ぶ。

—

3. コンパイラオプションによる最適化とパック演算の挙動

IBM Enterprise PL/Iコンパイラを使用する際、演算のパフォーマンスと整合性を左右するのが最適化オプション(`OPT(2)` や `OPT(3)`)である。

パック10進数同士の演算(加算・減算・乗算・除算)は、S/390およびz/Architectureの十進演算命令(AP, SP, MP, DPなど)に直接翻訳される。しかし、コンパイラは最適化レベルに応じて、以下のようなコード生成の最適化を行う。

1. インライン展開とレジスタ退避の最小化:
小規模な `FIXED DEC` 同士の四則演算は、ベースレジスタからのオフセット計算を効率化し、余分なロード/ストア命令を省く。
2. 中間精度の自動拡張:
PL/Iの言語仕様では、乗算や除算の中間結果の精度(p, q)は厳密に規定されている。例えば `FIXED DEC(5,2) FIXED DEC(5,2)` の場合、中間結果は `FIXED DEC(10,4)` として扱われ、オーバーフローを防ぐためのコードが自動生成される。

マイグレーション時のリスク:ゼロサプレスとパディング

JavaやC#へロジックを移行する際、PL/I側で暗黙的に行われている「桁あふれ(Overflow)時の丸め処理」や「符号の正規化(Normalizing)」が、移行先言語でそのまま再現されないケースが多い。特に、古いメインフレームのアプリケーションでは、`ON OVERFLOW` ブロックを記述して例外をトラップしているケースがあり、これを看過して移行すると、金額の切り捨てや誤った丸めによる「致命的な金融データの不一致」を引き起こす。

—

4. DB2(埋め込みSQL)および CICS エッジケース対策

基幹系PL/Iプログラムは、多くの場合、DB2とCICSの海原の中で泳いでいる。ここでのパック10進数の扱いは、一歩間違えるとシステム全体を巻き込む障害に直結する。

DB2ホスト変数におけるエッジケース

DB2の列定義が `DECIMAL(9,2)` である場合、PL/I側のホスト変数も厳密に `DCL HV-AMT FIXED DEC(9,2);` と定義する必要がある。
もし、これを誤って `FIXED BINARY`(COMP / COMP-5)や `CHAR` で受けてしまうと、DB2プリコンパイラ(あるいはランタイム)で暗黙のデータ型変換が発生し、CPUサイクルの無駄遣いだけでなく、高精度な小数点の切り捨てリスクが生じる。

特に注意すべきは NULL値の扱い である。
PL/Iでは `FIXED DEC` 自体はNULLを持てないため、DB2と連携する際は必ず インジケータ変数(Indicator Variable) をセットで用意しなければならない。

DCL 1 CUST_REC,
3 SQL-AMT FIXED DEC(9,2),
3 IND-AMT FIXED BIN(15); インジケータ変数 (-1ならNULL)

— SQL実行
EXEC SQL
SELECT AMOUNT INTO :SQL-AMT :IND-AMT
FROM SALES_TABLE
WHERE CUST_ID = :W-CUST-ID;

IF IND-AMT < 0 THEN -- NULLの場合はビジネス上のデフォルト値(0等)を設定 SQL-AMT = 0; END; もしインジケータ変数を忘れていて、DB2からNULLが返却された場合、ランタイムエラー(SQLCODE -305等)を引き起こすか、最悪の場合メモリー破壊に繋がる危険性がある。

CICSオンラインにおける通信域(COMMAREA)の罠

CICSの画面間トランザクションで渡される `DFHCOMMAREA` 内に `FIXED DEC` を配置する場合、フロントエンド(例えばWebフロントやオープン系API)とのインターフェース設計で地雷を踏むことがある。
オープン系のJSON/XMLパーサーは、基本的にパック10進数(COMP-3)をそのまま解釈できない。そのため、CICSのコンバージョン機能や、PL/I側で一度 `PICTURE` 編集項目や `CHAR` に変換(EDIT/UNEDIT)してから渡す必要がある。
この変換をおろそかにすると、通信電文の文字化けや、フィールドズレによる全トランザクションの異常終了(ASRAアベンドなど)の温床となる。

—

5. アーキテクトとしての提言:レガシー移行へ向けて

メインフレームからJavaやC#へのマイグレーションプロジェクトにおいて、`FIXED DEC(p, q)` は単なる「数値型への置き換え」では済まされない。

1. データストアの移行: DB2 on z/OS から PostgreSQL / Oracle へ移行する場合、データベース側は `NUMERIC(p, q)` や `DECIMAL(p, q)` でそのまま受け止められるが、ファイル(VSAMなど)をフラットファイル(S/390フォーマットのまま)で移行先のNAS等に置く場合、オープン系プログラムから直接読むことは不可能である。必ずコンバーター(EBCDICからASCII/UTF-8、およびCOMP-3のアンパック処理を行うETLツール)を挟む設計が必要となる。
2. 演算精度の担保: Javaへ移行する場合は、`float` や `double` などの浮動小数点数は絶対に使ってはならない(金融計算における10進小数誤差のため)。必ず `java.math.BigDecimal` を使用し、丸めモード(`RoundingMode`)までPL/Iの挙動と一致させる必要がある。

パック10進数(COMP-3)は、半世紀近くの歴史を持つ枯れた技術ではあるが、その裏にはハードウェアの制約と極限の効率化の知恵が詰まっている。その構造をミリ単位で把握している者だけが、安全で確実なシステム保守およびモダナイゼーションを成し遂げることができるのだ。

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