CEILとFLOORの罠:浮動小数点レジスタとマイグレーションの暗部
メインフレームの現場で長く生きていると、「たかが数値の切り上げ・切り捨て」という油断が、夜間バッチの致命的なアベンドや、数円単位の金額ズレを生む瞬間を幾度となく目撃してきた。特に、JavaやC#といったモダンな言語へのマイグレーション設計を任されたテックリードが、PL/Iの `CEIL` や `FLOOR` 関数の挙動を甘く見た結果、本番稼働後に痛い目をみるケースは後を絶たない。
PL/Iには、C言語のような厳格な「キーワード(予約語)」の概念が薄く、識別子の命名自由度が高い反面、コンパイラの最適化やハードウェアの浮動小数点レジスタの仕様が密に絡み合う。今回は、基幹システムの極限の信頼性を担保するため、`CEIL` と `FLOOR` が内部で何を行っているのか、そしてレガシー移行時にどのような地雷を踏み抜くのかを、アーキテクトの視点から徹底的に紐解いていこう。
—
1. 予約語を持たないPL/Iの呪縛と「組み込み関数」のオーバーライド
PL/Iの言語仕様における最大の特徴であり、時に凶器となるのが「予約語(Keywords)をほとんど持たない」という設計思想だ。`IF` や `DO` でさえも文脈キーワードであり、極端な話、変数名として使うことすら(推奨はしないが)コンパイラは許容する。
この仕様は `CEIL` や `FLOOR` といった組み込み関数(Built-in Functions)の扱いにも影を落とす。
1
/ 危険なプログラミング例:組み込み関数の再定義 /
TEST_PROG: PROC OPTIONS(MAIN);
DCL CEIL FIXED DEC(5,2) INIT(123.45); / CEILを変数名として宣言 /
DCL X FLOAT DEC(16);
/ ここでコンパイラはどう解釈するか? /
X = CEIL(10.5); / 変数CEILへの代入なのか、関数呼び出しなのか? /
END TEST_PROG;
PL/Iコンパイラは、同一スコープ内に同名の識別子(変数)が存在する場合、組み込み関数よりもローカル変数を優先して解決しようとする(あるいは文脈エラー、未定義参照を引き起こす)。近代的なJavaやC#の厳格な名前空間管理に慣れた若手エンジニアからすると悪夢のような仕様だが、何十年も前の古い基幹系コピーブロックを流用したバッチプログラムには、こうした隠し地雷が平然と埋まっている。
マイグレーション時にJava等へ自動変換ツール(トランスレータ)を通す際、この文脈依存の曖昧さが原因で、メソッド呼び出しと変数参照の誤認によるコンパイルエラーや、予期せぬロジック崩壊が頻発する原因となる。
—
2. 浮動小数点レジスタと内部ビット操作のリアル
さて、本題の `CEIL`(切り上げ)と `FLOOR`(切り捨て)の内部挙動だ。これらは単なる算術演算ではなく、IBM Zアーキテクチャの浮動小数点レジスタ(FPR)と丸めモード(Rounding Mode)を直接刺激する。
PL/Iで `FLOAT` 型に対してこれらを適用すると、ハードウェアレベルの浮動小数点命令(例:`AEBR` や `CXFBR` など、16進浮動小数点数(HFP)から2進浮動小数点数(BFP)、あるいはその逆の変換)が実行される。ここで問題になるのが、「IEEE 754規格の丸めモード」と「古いIBMハードウェアの16進浮動小数点(HFP)」の精度の差異である。
基幹システムでは、金融系の金利計算などで `FLOAT` ではなく `FIXED DECIMAL`(パックデシマル)を使うのが鉄則だが、パフォーマンスチューニングの一環で `FLOAT` へ一時的にキャストされ、これらの関数に放り込まれることがある。
浮動小数点演算における精度の罠
1
DCL V_FLOAT FLOAT DEC(16) INIT(1000000000.0000001);
DCL V_CEIL FLOAT DEC(16);
/ 内部の浮動小数点レジスタでの保持精度の限界による丸め誤差 /
V_CEIL = CEIL(V_FLOAT);
浮動小数点は2進数で小数を表現するため、`0.1` のような値ですでに無限小数となり、誤差を孕む。`CEIL` や `FLOOR` が実行される際、この微小な誤差が境界値(例: `1000000001.0` になるべきところが `1000000000.9999999` になり、`CEIL` で切り上げられても想定外の桁あふれやズレを起こす)を跨いでしまうアベンドや論理不整合が、月次・年次バッチの極限負荷時に発生する。
—
3. 実践:ポインタとベース変数によるメモリ直接操作時のエッジケース
システムアーキテクトとして最も警戒すべきは、動的メモリ管理(`ALLOCATE`)や、ストレージの直接マップ(` BASED` 変数)を行う領域で `CEIL`/`FLOOR` を使って境界調整を行うコードだ。
例えば、可変長レコードのオフセット計算や、CICS通信領域(COMMAREA)のパディング計算において、整数境界(4バイト境界や8バイト境界)にアライメントを合わせるために以下のようなコードが書かれることがある。
1
Dcl 1 COMM_AREA Based(Ptr_Comm),
3 Data_Len Fixed Bin(31),
3 Payload Char(0) Based(Ptr_Payload);
Dcl Ptr_Comm Pointer;
Dcl Ptr_Payload Pointer;
Dcl Raw_Offset Float Dec(16);
Dcl Aligned_Off Fixed Bin(31);
/ オフセットを4バイト境界に切り上げる計算 /
Raw_Offset = (Ptr_Comm -> Data_Len + 3) / 4;
Aligned_Off = CEIL(Raw_Offset) 4;
/ ポインタ演算の実行 /
Ptr_Payload = Addrel(Ptr_Comm, Aligned_Off);
ここで `CEIL` の入力に `FLOAT` を介している点に注目してほしい。もしコンパイラオプションで `STG(FLOAT)` の最適化レベルがアグレッシブに設定されていると、中間レジスタの丸め処理が最適化の波に飲まれ、意図したビットシフトや整数化が行われないことがある。
結果として、ポインタの指すアドレスが奇数番地や不正な境界を指すことになり、ハードウェア例外(S0C4アベンド:ストレージ保護例外 / 不正アドレス参照)を引き起こして夜間バッチが盛大にクラッシュする。
—
4. 埋め込みSQL(DB2)およびCICS環境におけるエッジケース
オンライン処理(CICS)やDB2のホスト変数と組み合わせる場合、話はさらに複雑になる。
DB2のストアドプロシージャやSQL内で `CEIL` / `FLOOR` を使う場合と、PL/I側のアプリケーション層でこれらを処理する場合では、データの丸めに対する「お墨付き」が異なる。
1. DB2のDECIMAL型に対するPL/I側の受け受け
DB2からフェッチした `DECIMAL(15,2)` のデータを、PL/I側で一度 `FLOAT` にキャストして `CEIL` をかけ、再び `DECIMAL` に戻してDB2にUPDATEをかけるような処理があったとする。
2. パックデシマルの内部符号反転バグ
古いコンパイラや最適化オプションの不具合、あるいはポインタ操作の誤りにより、パックデシマル(`COMP-3` 相当)の末尾の符号ニブル(`C`, `D`, `F` など)が破壊された状態で算術関数(またはそれに準ずる変換)を通すと、S0C7アベンド(データ例外:不当なパック十進数)が発生する。`CEIL` 自体はパックデシマルを直接引数にとる場合、内部で一時的にレジスタ形式へアンパック・変換を行うため、この符号異常の直撃を受ける。
1
/ DB2ホスト変数を絡めた安全な切り上げ処理の鉄則 /
DCL 1 WS_VARS,
3 INPUT_AMT DECIMAL(11,2),
3 CEIL_AMT DECIMAL(11,2);
/ NG例: FLOATを経由した無謀な演算 /
/ WS_VARS.CEIL_AMT = CEIL(WS_VARS.INPUT_AMT); /
/ 推奨:FIXED DECIMALのままで完結させるか、明示的な整数演算による安全な切り上げ /
/ 小数点以下が存在する場合の切り上げロジック(整数演算によるエミュレーション) /
商用環境のマイグレーションにおいて、Java側(`java.math.BigDecimal` の `setScale(0, RoundingMode.CEIL)` など)へ移行する際、PL/I側で浮動小数点レジスタの暗黙の丸めによって「救われていた(あるいは歪められていた)」微小な端数処理が、Javaの厳格な丸めモードによって異なった結果を生み出し、監査ログの金額不一致を引き起こすトラブルが後を絶たない。移行設計時には、PL/Iが内部で行っていた浮動小数点制御のビット単位の挙動を完全に再現、あるいはモダナイゼーションの機会として正しい丸め仕様へ定義し直す必要がある。
—
5. まとめ:レガシーの挙動を制する者が移行を制する
PL/Iの `CEIL` や `FLOOR` は、単なる数学関数ではない。そこにはIBMメインフレームのハードウェアアーキテクチャ、浮動小数点レジスタの歴史、そして予約語を持たない自由奔放な言語仕様が複雑に絡み合っている。
レガシーマイグレーションを成功させるための要諦は、移行元のコードを機械的に別言語へ置き換えることではない。「そのコードが、メインフレームのどのレジスタとメモリ空間をどう歪ませて結果を出力していたか」を解体し、モダンなプラットフォーム上で完全にコントロール下に置くことだ。
次にS0C7やS0C4のダンプに直面したとき、あるいはマイグレーション後の金額検算で1円のズレを発見したときは、コンパイラの最適化オプションと、背後にある浮動小数点演算の暗部を思い出してほしい。コードの行間を読むのではなく、マシンの息づかいを読むこと――それこそが、真のシステムアーキテクトの仕事である。
