メインフレームの奥底でうごめく数学関数:PL/Iにおける`LOG`および`EXP`の内部構造とマイグレーションの罠
基幹システムの現場において、長年稼働し続ける勘定系やリスク計算バッチのコアロジックを覗くと、突如として現れる数学関数がある。`LOG`(自然対数)や`EXP`(指数関数)だ。
JavaやC#といったモダンな言語であれば、これらは標準ライブラリが提供する単なるAPIに過ぎない。しかし、IBMメインフレーム(z/OS)上で稼働するPL/I(Enterprise PL/I)において、これらの超越関数を呼び出すという行為は、実行時ライブラリ(`SCEELKED`など)の深部、さらにはハードウェアアーキテクチャの浮動小数点演算機構(FPU)との緻密なダンスを意味する。
今回は、テックリードやモダナイゼーションを率いるアーキテクトに向けて、PL/Iにおける`LOG`および`EXP`の内部挙動、精度担保の仕組み、そしてJava/C#等へのマイグレーション時に必ず直面する「エッジケースの罠」について、現場の知見を総動員して解説する。
—
1. 実行時ライブラリ(SCEELKED)とハードウェア命令の協調
PL/Iプログラムで`LOG(X)`や`EXP(X)`を実行したとき、コンパイラはそれを単なるインラインコードには展開しない。通常、これらはIBM Language Environment(LE)の実行時ライブラリである `SCEELKED` に含まれる数学ルーチンへの外部サブルーチン呼び出し(あるいはバインド)として解決される。
z/OSのアーキテクチャ(z/Architecture)において、浮動小数点演算は長年の歴史を持つロング・プレシジョン(倍精度 64ビット)またはエクステンデッド・プレシジョン(拡張精度 128ビット)の浮動小数点レジスタ(FPR)を使用して行われる。
内部での演算フロー
1. 引数の型チェックと正規化: 渡された変数が固定小数点(`FIXED`)やパックデシマル(`DECIMAL FIXED`)である場合、コンパイラは暗黙的な型変換コードを生成し、一時的なロング浮動小数点(`FLOAT DEC(16)` または `FLOAT BIN(53)`)へ昇格させる。
2. LE数学ルーチンの呼び出し: `CEESxLOG` や `CEESxEXP` といったLEの数学サービスが呼び出される(`x`には精度の属性が入る)。
3. ハードウェア命令の活用: 近年の z14 や z15 などのプロセッサでは、ベクトル・ファシリティー(Vector Facility)を活用した高速なSIMD演算や、ハードウェア支援による超越関数命令が組み込まれているが、精度の境界値付近では依然としてソフトウェアによるテイラー展開やポリノミアル(多項式)近似の補正ルーチンが実行される。
ここで重要なのは、「PL/Iが保証する精度」と「Java/C#のIEEE 754標準仕様との微妙な挙動の差異」である。
—
2. PL/Iコードの実装例とメモリ・精度の制御
まずは、実務で遭遇するような、ポインタとベース変数を用いた動的メモリ操作と数学関数を組み合わせたコードを見てみよう。大量の市場データ(金利や確率密度)を扱う数理バッチでは、ストレージの効率化のためにベース変数(Based Variable)が多用される。
1
/ —————————————————————- /
/ プログラム名: MTHVAL01 /
/ 概要: ポインタとベース変数を用いた高精度対数・指数計算バッチ /
/ —————————————————————- /
MTHVAL01: PROC OPTIONS(MAIN);
DCL 1 RATE_DATA BASED(P_RATE),
3 RATE_ID CHAR(4),
3 RAW_VAL FLOAT DEC(16), / 入生データ(倍精度浮動小数点) /
3 LOG_VAL FLOAT DEC(16), / 計算結果: LOG(RAW_VAL) /
3 EXP_VAL FLOAT DEC(16); / 計算結果: EXP(RAW_VAL) /
DCL P_RATE POINTER;
DCL STORAGE_SZ FIXED BIN(31) INIT(32);
DCL RET_CODE FIXED BIN(31) INIT(0);
DCL I FIXED BIN(31);
/ 動的ストレージの獲得(GET STORAGE) /
P_RATE = ALLOCATE(STORAGE_SZ);
IF P_RATE = NULL() THEN DO;
PUT SKIP LIST(‘CRITICAL ERROR: STORAGE ALLOCATION FAILED.’);
SIGNAL ERROR;
END;
/ テストデータの初期化 /
RATE_ID = ‘IR01’;
RAW_VAL = 2.718281828459045; / 自然対数の底 e に近い値 /
/ —————————————————————- /
/ LOGおよびEXP関数の実行 /
/ ※ 引数が0以下の場合のシグナル処理(ZDCV等)に注意 /
/ —————————————————————- /
ON ERROR BEGIN;
PUT SKIP LIST(‘ERROR: MATHEMATICAL EXCEPTION OCCURRED.’);
/ アベンドを回避するためのフォールバック処理 /
GOTO ERROR_EXIT;
END;
IF RAW_VAL > 0 THEN DO;
LOG_VAL = LOG(RAW_VAL);
EXP_VAL = EXP(RAW_VAL);
END;
ELSE DO;
PUT SKIP LIST(‘ERROR: INVALID ARGUMENT FOR LOG FUNCTION.’);
GOTO ERROR_EXIT;
END;
/ 結果の出力 /
PUT SKIP EDIT (‘ID: ‘, RATE_ID,
‘ LOG: ‘, LOG_VAL,
‘ EXP: ‘, EXP_VAL)
(A, A, A, E(20,10), A, E(20,10));
GOTO NORMAL_EXIT;
ERROR_EXIT:
RET_CODE = 8;
NORMAL_EXIT:
/ ストレージの解放 /
FREE P_RATE;
RETURN;
END MTHVAL01;
このコードにおいて、`FLOAT DEC(16)` を使用している点は非常に重要だ。IBMメインフレームのPL/Iでは、デフォルトの `FLOAT` は単精度(6桁、`FLOAT DEC(6)` または `FLOAT BIN(21)`)になることがある。金融計算や統計モデルのマイグレーションでは、この暗黙の精度低下が致命的な端数誤差を生む原因となる。
—
3. アベンド(ABEND)と例外処理の深部
メインフレームで数学関数を扱う際の最大の恐怖は、浮動小数点例外によるアベンド(代表例:AS9CやAEI0など、あるいはLEのU4038など)である。
よくあるアベンドシナリオ
1. ドメインエラー(Domain Error): `LOG(0)` や `LOG(-5.2)` のように、定義域外の値を渡した場合。
2. レンジエラー(Range Error): `EXP(750.0)` のように、倍精度浮動小数点の表現上限(約 $10^{308}$)を超える値を生成しようとしてオーバーフローを起こした場合。
PL/Iでは、コンパイルオプションで `CHECK(UNDERFLOW, OVERFLOW, ZERODIVIDE)` などを指定し、さらに `ON` 条件文でトラップを張るのが定石だ。
しかし、レガシーシステムの中には、この `ON` 条件が適切に記述されておらず、レガシーコードが無言で異常値を伝播させ、最終的なDB2の更新処理で制約違反を引き起こすか、突然のU4038アベンドで夜間バッチを停止させるという事故が後を絶たない。
—
4. マイグレーション(Java/C#等への移行)における致命的なエッジケース
レガシーマイグレーションの現場で、PL/Iの `LOG` / `EXP` をJavaの `Math.log()` / `Math.exp()` や C#の `Math.Log()` / `Math.Exp()` に機械的に置き換えると、「テスト結果が数ペンス、あるいは数円ズレる」という現象に直面する。アーキテクトが頭を抱える瞬間だ。
その主な原因と対策を以下に挙げる。
① 浮動小数点の丸めモード(Rounding Mode)の差異
メインフレームのハードウェア演算(Hexadecimal Floating-Point: HFP、あるいは近年の Binary Floating-Point: BFP)と、x86/ARMプロセッサ上で動くJava仮想マシン(JVM)のIEEE 754演算では、端数の丸め処理(Toward zero, Nearest等)に微妙な差異が生じることがある。特に長大なお金の複利計算のループ内では、この誤差が蓄積する。
- 対策: マイグレーション先のJavaコードでも、必要に応じて `BigDecimal` を用いた多倍長演算への書き換えを検討する。ただし、超越関数を `BigDecimal` で実装する場合は独自にテイラー展開を書くか、信頼できるサードパーティ製ライブラリ(Apache Commons Math等)を導入し、精度を検証(キャリブレーション)しなければならない。
② アンダーフロー・オーバーフロー時の挙動
PL/Iではアンダーフロー(非常に小さな値がゼロとみなされる現象)が発生した際、コンパイラオプションやLEの環境変数(`ABPERC` など)によって、プログラムを強制終了させるか、単に「0.0」とみなして続行するかを厳密に制御できる。
一方、Javaの `Math.exp()` は、極端に小さな負の数に対して下限を越えると `0.0` を返し、例外は投げない。このエラーハンドリングの暗黙の仕様違いが、移行後のバグの温床となる。
—
5. アーキテクトとしての総括
PL/Iにおける `LOG` および `EXP` は、単なる数学的計算ルーチンではない。それは、IBMメインフレームのハードウェア・レジスタ、実行時ライブラリ(`SCEELKED`)、そして何十年も積み上げられてきた例外処理の歴史が緻密に噛み合った「巨大な歯車の一部」である。
モダナイゼーションを進める際、「言語仕様の関数が同じだから移行は容易だ」という安易な判断を下すことは、システムアーキテクトとしての自殺行為に等しい。
ソースコードの字面を追うだけでなく、「元のPL/Iプログラムがどのような精度(`FLOAT DEC`の桁数)でデータを保持し、LEの環境下でどのような例外シグナルを想定していたか」を徹底的にリバースエンジニアリングすること。それこそが、レガシー移行を成功裏に導く唯一にして最大の王道である。
