モジュロ演算とは 定義と余りの扱い プログラミングとハードウェアの違い
数学では、モジュロ演算の結果は、算術除算の余りである。よく知られているように、2つの整数の除算は商と余りを生成する。
しかし、他の規約も可能である。コンピュータや電卓には、数字を格納し表現する様々な方法があります。モジュロ演算の定義は、プログラミング言語やハードウェアに依存します。
モジュロ演算の数学的定義
数学での典型的な定義(ユークリッドの除法)は次の通りです。整数 a と正の整数 n に対して、ある一意の商 q と 標準的な余り r が存在して
a = q·n + r, ただし 0 ≤ r < n。
この r を a mod n(a を n で割ったあまり)とするのが一般的です。負の数に対しても「0 から n−1 の範囲に入る代表」を取るのが数学的には自然です(例:7 mod 3 = 1、−1 mod 3 = 2)。また合同式 a ≡ b (mod n) は a と b が n で割った余りが同じ、あるいは差が n の倍数であることを意味します。
プログラミングにおける挙動の違い
一方でプログラミング言語や実装では、割り算の切り捨て規則や符号の扱いにより余りの符号や範囲が変わります。代表的な違い:
- ユークリッド(非負)型:余りが 0 ≤ r < |n| となる(Python の % は除数が正のときにこの性質を満たす)。
- 切り捨て(ゼロ方向)型:商をゼロ方向に切り捨てる方式では、余りは被除数(分子、a)と同じ符号を持つことが多い(例:C、C++、Java の整数除算/剰余は商を 0 に向かって切り捨てるため、余りは被除数と同符号になる)。
具体例:
- 数学(標準): −7 mod 3 = 2(−7 = (−3)·3 + 2)
- C / Java(一般的): −7 % 3 = −1(商 = −2、余り = −1)
- Python: −7 % 3 = 2(Python の規則により余りは除数と同符号)
したがって、言語間で % 演算子の結果を期待通りに扱うには注意が必要です。
ハードウェアと実装上の注意点
コンピュータの内部では整数は有限幅(例:32 ビット、64 ビット)で表現され、算術はしばしば 2^n に対する合同(モジュロ 2^n)として扱われます。特に:
- 符号なし整数(unsigned):ハードウェア演算は明確に 2^k による剰余演算と同等であり、加算や乗算はオーバーフローで自動的にラップアラウンドします。
- 符号付き整数:多くのマシンは二の補数表現を使い、ハードウェアはビット列として演算するため結果はビット上でラップしますが、言語仕様上の振る舞い(未定義、実装依存、あるいは明確に定義)は言語によって異なります。たとえば C 言語では有符号整数のオーバーフローは未定義動作ですが、実機では二の補数でラップすることがほとんどです。
- 性能:任意の除数に対する剰余演算は比較的コストが高く、定数で 2 の累乗のときはコンパイラやハードウェアがビットマスク(x & (2^k−1))やシフトで高速化します。コンパイラは一般定数除算も乗算とシフトに置き換える最適化を行います。
実用的なトリックと推奨事項
- 言語仕様を確認する:使用する言語が剰余の符号についてどう定義しているかを確認してください(特に負の被除数・除数の場合)。
- 常に非負の余りが欲しいとき:多くの言語で次のように正規化できます。r = ((a % n) + n) % n 。これにより負の結果でも 0 ≤ r < n にマップされます。
- 2 の累乗で高速化:除数が 2^k の場合は x & (2^k - 1)(符号なし)を使うと高速で安全ですが、負の値や符号付き型では注意が必要です。
- ゼロ除算に注意:どの定義でも除数が 0 の場合は未定義または例外になります。必ず除数が 0 でないことをチェックしてください。
- テストを書く:負の入力や境界(最小/最大整数)についての単体テストを用意し、実装が期待通りの振る舞いをするか確認してください。
まとめ
モジュロ演算は数学的には余りを返す操作であり、標準的には 0 ≤ r < n の余りを扱いますが、コンピュータの世界では表現方法や言語仕様により挙動が変わります。プログラミングやハードウェアの文脈では、符号やビット幅、パフォーマンス上の最適化、言語仕様(未定義動作や符号の取り扱い)に注意して実装・利用することが重要です。