算術演算の最適化と固定小数点数:`OPTIMIZE`が暴くメインフレームの深淵
基幹システムの現場において、深夜のバッチウィンドウが予定時刻を過ぎても終わらない、あるいは原因不明のS0C7アベンドが突如として本番稼働中のオンラインを直撃する――。そんな絶望的な状況に直面したとき、あなたは何を疑うだろうか。JCLのステップか、DB2のアクセスパスか。
しかし、真犯人は多くの場合、私たちが何気なく記述したPL/Iの算術データ定義と、コンパイラが吐き出した機械語命令のミスマッチにある。
今回は、PL/Iにおける固定小数点数(`FIXED BINARY` および `FIXED DECIMAL`)の精度設計が、コンパイラオプション(特に `OPTIMIZE`)によっていかに機械語レベルの挙動を変え、時としてシステムを奈落の底へ突き落とすのかについて、アーキテクトの視点から徹底的に紐解いていこう。
—
1. `FIXED BINARY` vs `FIXED DECIMAL` とコンパイラの機械語生成
PL/Iの最大の強みであり、同時に移行エンジニアの頭を悩ませる原因は、その極めて柔軟なデータ型定義にある。特に金融系システムで多用される `FIXED DECIMAL (p, q)`(パック十進数)と、内部演算で高速に処理される `FIXED BINARY (p)`(2進整数)の混在は、コンパイラに複雑な型変換コードを生成させる。
ここで問題になるのが、IBM Enterprise PL/Iコンパイラが提供する `OPTIMIZE(0)`、`OPTIMIZE(FULL)`(または `OPTIMIZE(1)` / `OPTIMIZE(2)`)といった最適化レベルの違いだ。
CVBとCVDのコスト:見えないオーバーヘッド
固定小数点演算において、`FIXED DECIMAL`(パッデッド・デシマル)同士の演算には専用の十進演算命令(`AP`、`SP`、`MP`、`DP` など)が使われる。しかし、これが `FIXED BINARY`(レジスタ演算に適した2進数)やポインタ算術、あるいはループ制御変数と絡み合うと、コンパイラはメモリ上のパック十進数を汎用レジスタで扱える2進数へ変換する `CVB`(Convert to Binary)や、その逆の `CVD`(Convert to Decimal)命令を挿入せざるを得なくなる。
DCL W_DEC_AMT FIXED DECIMAL(11,2) INIT(0);
DCL W_BIN_CNT FIXED BIN(31) INIT(0);
/ この単純な加算ですら、内部では型変換のドラマが起きている /
W_BIN_CNT = W_BIN_CNT + W_DEC_AMT;
`OPTIMIZE(0)`(最適化なし)でコンパイルした場合、コンパイラは忠実にソースコードの行を機械語に落とし込むため、無駄な `CVD` / `CVB` の往復や、メモリ(ストレージ)とレジスタ間でのデータロード/ストアが頻発する。これがバッチ処理全体のスループットをじわじわと蝕む。
一方、`OPTIMIZE(FULL)` を指定すると、コンパイラは変数のライフサイクルやレジスタの生存期間を徹底的に解析し、ループ内での不要な `CVB` / `CVD` の呼び出しをレジスタアロケーションの魔法によって排除、あるいはループ外へホイスティング(外出し)する。
だが、この「賢すぎる最適化」が、レガシー移行やエッジケースにおいて牙をむくことがあるのだ。
—
2. 移行プロジェクトの罠:パックデシマルの内部符号反転バグ
JavaやC#、あるいはクラウド環境へのマイグレーションにおいて、最もエンジニアを絶望させるのが「データ不整合」だ。PL/Iから他言語への移行時、あるいはPL/Iプログラム単体でのモダナイゼーション(コンパイラバージョンのアップグレード含む)において、`OPTIMIZE` レベルを上げた途端に計算結果が狂う現象に遭遇したことはないだろうか。
原因の多くは、未初期化領域や不正なゾーン/パック符号(S0C7の原因となるデータ)に対するコンパイラの最適化挙動の変化にある。
/ 危険なデータ定義の例:不適切な精度とスケール /
DCL 1 ACCOUNT_REC,
5 AC_ID FIXED BIN(15),
5 AC_BALANCE FIXED DECIMAL(7,2);
/ 最適化によってレジスタにキャッシュされた値が、
非同期なストレージ書き換え(埋め込みSQLや不安全なPOINTER操作)を
見落とすケースがある /
ポインタとベース変数を用いた動的メモリ操作における魔物
メインフレームの基幹系では、リンケージセクションや動的ストレージ(`ALLOCATE`)において、`POINTER` と `BASED` 変数を用いた凄まじいテクニックが使われる。
DCL P_RAW_DATA POINTER;
DCL 1 MAP_DATA BASED(P_RAW_DATA),
3 D_TYPE CHAR(1),
3 D_VAL FIXED DECIMAL(5,0);
/ 動的に割り当てられた領域をキャスト的に扱う /
P_RAW_DATA = GET_BUFFER_ADDRESS();
IF D_VAL > 1000 THEN DO;
/ 処理 /
END;
ここで `OPTIMIZE(FULL)` が効いていると、コンパイラは `D_VAL` の値が「このコードブロック内で変化しない」と勝手に仮定し、レジスタ上にロードした値を使い回す。しかし、もし別の非同期ルーチンやCICSの共通ワークエリア(CWA)、あるいはDB2のフェッチによってそのストレージ領域が書き換えられていた場合、プログラムは古いキャッシュ値を参照し続け、致命的なロジックバグを引き起こす。
PL/Iにおける `REORDER` / `NOREORDER` コンパイラオプションの指定ミスは、まさにこの悪夢を引き起こす引き金となるのだ。
—
3. アベンド(ABEND)発生時のダンプ解析とエッジケース
本番稼働中の夜間バッチで `S0C1`、`S0C4`、あるいはお馴染みの `S0C7` が発生した際、システムアーキテクトとしての真価が問われる。
`OPTIMIZE(FULL)`環境下でのストレージダンプの読み解き
最適化が有効な状態でビルドされたロードモジュールのアドレス空間をIPCS(Interactive Problem Control System)で覗くと、ソースコードの変数が「どのレジスタに割り当てられているか」を追うという苦行が待っている。
コンパイラリスト(Listing)の OFFSET マップと照らし合わせ、プログラムチェック発生時の PSW(Program Status Word)のインストラクション・アドレスから逆引きしなければならない。
特に `FIXED BINARY(31)` のオーバーフロー(固定小数点演算例外:ABEND `S0C8`)が発生した場合、`OPTIMIZE` によって命令の順序が入れ替わっているため、ダンプ上のブレークポイントや例外発生位置が、ソースコードの記述行から数行ズレて見えることがある。これは若手エンジニアをパニックに陥れる十分な理由になるが、ベテランならプロローグ/エピローグコードとレジスタ退避の状況から正確に真の発生源を割り出す。
埋め込みSQL(DB2)およびCICSオンライン処理のエッジケース
CICS環境やDB2を伴うオンライン処理では、タスクのライフサイクルが極めて短く、パフォーマンスが命だ。
ここで `FIXED DECIMAL` と `FIXED BINARY` の暗黙の型変換が埋め込みSQLのホスト変数(Host Variable)との間で発生すると、DB2プリコンパイラとPL/Iコンパイラの双方の最適化が複雑に絡み合う。
例えば、DB2のコラム定義が `DECIMAL(9,0)` であるのに対し、PL/I側で `FIXED BIN(31)` として受けている変数を `WHERE` 句の条件に使うとする。
`OPTIMIZE(0)` では毎回確実にパック十進数への変換が行われていたものが、`OPTIMIZE(FULL)` によって不適切な定数畳み込み(Constant Folding)やアグレッシブなレジスタ最適化が行われ、意図しないオーバーフローや、DB2側のインデックススキャンが外れてフルテーブルスキャンに化けるという、性能上の「エッジケース」を踏み抜くことがある。
—
4. モダナイゼーション・移行期におけるアーキテクトの指針
JavaやC#へのマイグレーション、あるいは最新の z/OS 上での Enterprise PL/I へのバージョンアップを行う際、私たちは以下の鉄則を遵守しなければならない。
1. コンパイラオプションの安易な全体変更の禁止
「とにかく最新にして `OPTIMIZE(FULL)` をつければ速くなる」という幻想を捨てること。特に古い世代からコンパイルし直す際は、まず `OPTIMIZE(0)` または `OPTIMIZE(2) NOREORDER` でリグレッションテストを通過させ、動作の完全な等価性を担保した上で、段階的に最適化レベルを引き上げるべきだ。
2. データ型の厳密なタイトネス(Tightness)の確保
無駄な `CVB`/`CVD` を発生させないために、ループカウンタや配列インデックスは迷わず `FIXED BIN(31)` または `FIXED BIN(63)` に統一し、金額や数量といったビジネス上の正確性が求められる項目のみ `FIXED DECIMAL` を割り当てる。中途半端な `FIXED BIN(15)` の乱用は、かえってコンパイラに不要な符号拡張(Sign Extension)命令を生成させる原因となる。
3. ポインタ操作とベース変数のカプセル化
レガシーなポインタ演算をそのまま他言語(Java等)に移行することは不可能に近い。PL/Iの段階で、ポインタによるメモリ直叩きを行っている箇所を特定し、構造体(`STRUCTURE`)の明確なオフセット定義や、明確なアクセサ・ルーチンへリファクタリングしておくことが、将来の移行成功率を劇的に跳ね上げる。
—
結びにかえて
メインフレームのPL/Iは、ハードウェアの進化とコンパイラの最適化技術の歴史そのものと共にある。
`OPTIMIZE` オプションが生成する機械語の裏側を理解し、`FIXED BINARY` と `FIXED DECIMAL` の鼓動を聞き分けることができれば、もはやコンパイルエラーや不可解なアベンドは恐怖の対象ではなく、システムと対話するための興味深い手がかりへと変わるはずだ。
基幹システムの命脈を預かるアーキテクトとして、コードの1行、オプションの1文字に宿る意図を、これからも妥協なく追求し続けてほしい。
