【テクニカル・上級編】MOD関数とREM関数の除算命令展開の違い – PL/Iの基本構文とデータ制御実践ガイド

MODとREMの残酷な差異:汎用機における剰余計算の罠とマイグレーションの処方箋

メインフレームの現場において、数値を割り、その「余り」を求めるという極めてプリミティブな処理ほど、システム移行時やバッチ改修時に開発者を絶望の淵に追い込むものはない。特に、JavaやC#などのモダン言語に慣れ親しんだエンジニアが、長年稼働してきたPL/I(Programming Language One)のコードベースに挑む時、そこにはコンパイラ固有の数学的定義とハードウェア(S/390・z/Architecture)の命令セットが織りなす「深い罠」が待ち受けている。

今回は、PL/Iにおける `MOD` 組み込み関数と `REM` 組み込み関数の挙動の違いに焦点を当てる。単なる関数の仕様解説にとどまらず、ハードウェアの `DR`(Divide Register)命令レベルの挙動、符号反転に起因するパックデシマルのバグ、さらにはDB2やCICSが絡むエッジケース、そしてJavaへの安全なマイグレーション設計まで、実務の最前線で培った知見を余すところなく共有しよう。

—

1. 識別子の自由度と「予約語を持たない」PL/Iの哲学

本題に入る前に、PL/Iという言語の根本的な思想について触れておきたい。C言語やJavaには数十個の「キーワード(予約語)」が存在し、変数名として使うことは許されない。しかし、PL/Iには厳密な意味での「予約語」が存在しない。

1
/ PL/Iにおける恐るべき変数宣言の例 /
DECLARE IF FIXED BIN(31);
DECLARE THEN FIXED BIN(31);
DECLARE MOD FIXED BIN(31);

IF = 10;
THEN = 20;
MOD = IF + THEN;

PL/Iコンパイラは、文脈(Context)からそれがキーワードなのかユーザー定義の識別子なのかを完璧に判別する。この柔軟性は強力だが、裏を返せば、組み込み関数名である `MOD` や `REM` すら変数名として上書き定義できてしまうことを意味する。
レガシーなソースコードを解析する際、先人が不注意にも `DECLARE MOD …` などと定義しているコード片に出くわすと、コンパイルエラーではなく、意図しない数学的計算の崩壊を引き起こす。モダナイゼーションの初期段階で、コードの静的解析ツールを用いてこの手の「コンテキスト汚染」を洗い出すことが、システムアーキテクトとしての最初の仕事となる。

—

2. MOD関数とREM関数の数学的定義の差異

剰余(Remainder / Modulo)を計算する際、被除数(Dividend)や除数(Divisor)に負数が含まれる場合、言語や処理系によって結果が異なることはよく知られている。PL/I(IBM Enterprise PL/Iコンパイラ)における `MOD` と `REM` の仕様は以下の通りである。

  • `REM(x, y)`: 数学的な「剰余(Remainder)」。結果の符号は被除数(x)の符号に一致する。C言語の `%` 演算子に近い挙動を示す。
  • `MOD(x, y)`: 数学的な「合同(Modulo)」。結果の符号は除数(y)の符号に一致する。金融計算や循環インデックスの算出で好まれる。

計算式で表すと以下の関係が成り立つ。
> `REM(x, y) = x – (y TRUNC(x / y))`
> `MOD(x, y) = x – (y FLOOR(x / y))`

この差異が、バッチ処理の突合ロジックや日次・月次の端数処理において、致命的な金額のズレを生み出す原因となる。

—

3. ハードウェア命令(DR/DRS)とコンパイラ最適化の闇

メインフレームのCPUアーキテクチャにおいて、整数除算は `DR`(Divide Register)や `DS`(Divide Single)といった機械語命令によって実行される。これらは商と余りを同時にレジスタに格納するため非常に効率が良い。

しかし、マイコンやメインフレームのハードウェアレベルの割り算は、基本的に「切り捨て(Truncation towards zero)」を行う。
IBM Enterprise PL/Iコンパイラは、このハードウェアの特性と言語仕様のギャップを埋めるため、負数が絡む剰余計算に対してインラインで補正コード(条件分岐や符号反転処理)を展開する。

ここで問題になるのが、パックデシマル(COMP-3)や固定小数点バイナリ(FIXED DECIMAL / FIXED BINARY)を混用した際の内部符号反転バグである。

以下の実務に近いPL/Iコードを見てほしい。

1
//
/ MOD と REM の挙動検証プログラム /
//
TEST_REMAINDER: PROC OPTIONS(MAIN);

DCL WS_DIVIDEND FIXED BIN(31) INIT(-10);
DCL WS_DIVISOR FIXED BIN(31) INIT( 3);
DCL WS_RES_REM FIXED BIN(31);
DCL WS_RES_MOD FIXED BIN(31);

DCL WS_MSG_BUF CHAR(80);

/ REMの計算:結果の符号は被除数(-10)に合わせてマイナスになる /
WS_RES_REM = REM(WS_DIVIDEND, WS_DIVISOR);

/ MODの計算:結果の符号は除数(3)に合わせてプラスになる /
WS_RES_MOD = MOD(WS_DIVIDEND, WS_DIVISOR);

PUT SKIP LIST (‘— 剰余計算の比較 (-10 / 3) —‘);
PUT SKIP EDIT (‘REM Result : ‘, WS_RES_REM) (A, F(4)); / 出力: -1 /
PUT SKIP EDIT (‘MOD Result : ‘, WS_RES_MOD) (A, F(4)); / 出力: 2 /

END TEST_REMAINDER;

このコード自体は意図通りに動くが、これがDB2の埋め込みSQLやCICSの通信エリア(DFHCOMMAREA)を介したデータ、あるいは極端な最適化オプション(`OPT(2)` や `OPT(3)`)が指定された環境下でコンパイルされると、コンパイラのレジスタ割り付け最適化のバグ、あるいはパックデシマルの符号ニブル(Zone/Sign部分の `C`, `D`, `F`)の解釈ミスにより、稀にアベンド(S0C7 10進数データ例外など)やサイレントな計算汚染を引き起こすことがある。

特に、コンパイルオプションで `TRUNC(BIN)` がどう設定されているかによって、ハードウェアの境界値(例: 31ビットの最大値を超える演算)における挙動が劇的に変わるため、レガシー移行時にはコンパイラオプションの解析が不可欠となる。

—

4. 埋め込みSQL(DB2)およびCICSオンラインにおけるエッジケース

基幹システムでは、単体の計算ロジックだけでなく、周辺ミドルウェアとの連携においてこの剰余計算が牙をむく。

1. DB2(SQL)との演算結果の乖離

DB2のSQL内にも `MOD` 関数が存在するが、その実態はデータベース製品やバージョンによって挙動が異なる場合がある。PL/I側で `REM` を使って計算した結果をDB2のホスト変数に渡し、SQL側の `MOD` 条件と比較・突合するようなバッチ処理がある場合、負数を含むデータが流れた瞬間にヒット漏れ(データ不整合)が発生する。
対策: データベース側にロジックを丸投げせず、PL/I側で事前に絶対値化するか、あるいは両者で同一の演算結果になるようビジネスロジック層でラップ関数を挟む設計にすべきである。

2. CICSオンラインにおけるポインタ操作とストレージ上書き

高速性を追求するCICSの領域外メモリ(GETMAINで取得した動的ストレージ)をポインタ(`POINTER`)で直接指し示し、構造体(`BASED` 変数)をマッピングして剰余計算を行う設計が見られる。
このとき、ポインタのアライメント違反や、演算結果を格納する領域の定義ミス(例:`FIXED DEC(5,0)` に `REM` の結果で溢れたデータを突っ込む)が発生すると、瞬時にS0C4アベンド(ストレージ保護例外)を引き起こし、CICSリージョン全体を巻き込む障害に発展する。

—

5. Java/C#へのマイグレーション設計:決定的な処方箋

レガシーマイグレーションの現場において、PL/Iの `MOD` と `REM` をJavaへ移行する際、最も多くのエンジニアが犯すミスは、Javaの剰余演算子 `%` を無思考にそのまま適用することである。

Javaの `%` 演算子は、実は数学的な「Modulo」ではなく、C言語を踏襲した「Truncated Division(切り捨て除算)に対するRemainder(剰余)」である。つまり、Javaの `x % y` は、PL/Iの `REM(x, y)` と同じ挙動を示す。

もし移行元のPL/Iコードが `MOD(x, y)`(除数の符号に合わせる数学的モジュロ)を使用していた場合、Javaで単純に `%` を使うと、負数が絡んだ瞬間に計算結果が逆転し、基幹システムの業務ロジックが破壊される。

Javaにおける安全な移行実装例

JavaでPL/Iの `MOD` と `REM` を完全に再現するためのヘルパーメソッドを以下に提示する。移行設計書のコードレビューにおいて、これと同等の安全策が講じられているか必ず確認してほしい。

public class Pl1MathUtils {

/

  • PL/Iの REM関数相当(被除数の符号に一致する剰余)
  • Javaの % 演算子と同等。

/
public static int rem(int x, int y) {
return x % y;
}

/

  • PL/Iの MOD関数相当(除数の符号に一致する数学的モジュロ)
  • 負数が絡む計算の正確性を担保する。

/
public static int mod(int x, int y) {
if (y == 0) {
throw new ArithmeticException(“/ by zero”);
}
int result = x % y;
if (result != 0 && ((x < 0) ^ (y < 0))) { result += y; } return result; } } ---

結びにかえて

たかが「余りを求める計算」と侮ってはならない。メインフレームの歴史とハードウェアの制約、そしてPL/Iという言語の自由度が複雑に絡み合った領域において、浮動小数点やパックデシマルの誤差、そして符号の扱いは、数億円規模の金融計算の正確性を左右する急所である。

テックリードやアーキテクトとして移行プロジェクトに臨む際は、ソースコードの表面的な構文置換にとどまらず、コンパイラが生成する機械語の挙動、ミドルウェア(DB2/CICS)とのデータ連携の妙、そして言語間の数学的定義の差異を完全に見極め、堅牢な設計を貫き通してほしい。

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