Go言語 big.Int.Mul()徹底解説:巨大整数計算の基本と応用
big.Int.Mul()
の基本的な使い方
Mul
メソッドのシグネチャは以下のようになっています。
func (z *Int) Mul(x, y *Int) *Int
これは、次のように解釈できます。
x, y *Int
: 掛け算を行う2つのbig.Int
型のポインタです。x
とy
を掛け合わせます。z *Int
: 演算結果を格納するbig.Int
型のポインタです。Mul
メソッドは、x
とy
の積を計算し、その結果をz
に代入します。そして、そのz
自身を返します。
重要なポイント
- 結果はレシーバに格納される
Mul
メソッドは、結果をレシーバ(この場合はz
)に直接書き込みます。そのため、z
は結果を格納するためのbig.Int
型の変数である必要があります。 - ポインタで渡す
big.Int
型の値を扱う際は、常にポインタ(*big.Int
)で渡す必要があります。これは、Goの標準の整数型とは異なる点です。 - 通常の演算子(*)は使えない
big.Int
型に対しては、通常の*
演算子(アスタリスク)を使った掛け算はできません。必ずMul
メソッドを使用する必要があります。
具体例
package main
import (
"fmt"
"math/big"
)
func main() {
// big.Int の初期化
a := big.NewInt(1234567890123456789) // 非常に大きな整数
b := big.NewInt(9876543210987654321) // 別の非常に大きな整数
// 結果を格納するための big.Int を用意
result := new(big.Int)
// a と b の積を計算し、result に格納
result.Mul(a, b)
fmt.Printf("a = %s\n", a.String())
fmt.Printf("b = %s\n", b.String())
fmt.Printf("a * b = %s\n", result.String())
// 別な例:同じ big.Int を再利用して計算
c := big.NewInt(5)
d := big.NewInt(10)
e := big.NewInt(20)
// c = c * d (cの値が上書きされる)
c.Mul(c, d)
fmt.Printf("c (5*10) = %s\n", c.String()) // 結果: 50
// c = c * e (cの値が再度上書きされる)
c.Mul(c, e)
fmt.Printf("c (50*20) = %s\n", c.String()) // 結果: 1000
}
Goの組み込みの整数型(int
, int64
など)は、固定のビット数で表現されるため、扱える数値の範囲に制限があります。例えば、int64
型は最大で約9×1018までしか表現できません。
しかし、暗号通貨、科学技術計算、非常に大きな数を扱うアルゴリズムなど、この範囲を超える巨大な整数を扱う必要がある場合があります。そのような場合に、math/big
パッケージが提供するbig.Int
型が役立ちます。big.Int
は、メモリが許す限り任意の精度の整数を表現できます。
nilポインタのデアファレンス (nil dereference)
これは最も頻繁に発生するエラーの一つです。big.Int
型の変数を初期化せずにMul
メソッドを呼び出そうとすると発生します。
エラーの例
package main
import (
"fmt"
"math/big"
)
func main() {
var a *big.Int // 初期化されていない (nil)
b := big.NewInt(10)
c := big.NewInt(5)
// ここでパニックが発生する可能性が高い (a は nil なので)
a.Mul(b, c) // runtime error: invalid memory address or nil pointer dereference
fmt.Printf("Result: %s\n", a.String())
}
原因
big.Int
型はポインタとして扱われるため、使用する前にnew(big.Int)
またはbig.NewInt()
などで初期化する必要があります。var a *big.Int
としただけでは、a
はnil
ポインタのままです。
トラブルシューティング/解決策
結果を格納するbig.Int
も、引数として渡すbig.Int
も、必ず使用前に初期化します。
package main
import (
"fmt"
"math/big"
)
func main() {
// 解決策1: new(big.Int) で初期化
var a = new(big.Int) // 結果を格納するための big.Int を初期化
b := big.NewInt(10)
c := big.NewInt(5)
a.Mul(b, c) // 正しく動作する
fmt.Printf("Result: %s\n", a.String()) // Result: 50
// 解決策2: big.NewInt() で初期化 (初期値も設定できる)
d := big.NewInt(0) // または big.NewInt(10) など
e := big.NewInt(20)
f := big.NewInt(3)
d.Mul(e, f) // 正しく動作する
fmt.Printf("Result: %s\n", d.String()) // Result: 60
}
結果のレシーバ(z)と引数(x, y)が同じインスタンスの場合
Mul
メソッドは結果をレシーバに上書きします。この性質を理解していないと、意図しない結果になることがあります。
問題の例
package main
import (
"fmt"
"math/big"
)
func main() {
x := big.NewInt(10)
y := big.NewInt(5)
// x = x * y を意図しているが、結果が異なる可能性がある
// 実際には y の値が計算中に変更されてしまう可能性がある
x.Mul(x, y) // 問題ではないが、意図しない結果になることがある
fmt.Printf("Result: %s\n", x.String())
}
この例自体は動作しますが、より複雑な計算式で同じbig.Int
インスタンスを複数の操作で使い回す場合に、予期せぬ中間結果の上書きにより最終結果がおかしくなることがあります。特に、big.Int.Mul(x, x)
のような自己乗算を行う場合や、複数の操作をチェインする場合に注意が必要です。
トラブルシューティング/解決策
Mul
メソッドは、レシーバと引数が同じbig.Int
インスタンスであっても正しく動作するように設計されています(つまり、x.Mul(x, y)
やx.Mul(y, x)
、x.Mul(x, x)
は安全です)。
ただし、読みやすさや意図の明確化のため、中間結果を別のbig.Int
変数に格納することをお勧めします。特に、複雑な式を評価する際には、結果を格納するための新しいbig.Int
インスタンスを準備するのが安全です。
package main
import (
"fmt"
"math/big"
)
func main() {
x := big.NewInt(10)
y := big.NewInt(5)
z := big.NewInt(2)
// (x * y) + z を計算したい場合
tempResult := new(big.Int) // 中間結果を格納するための新しい big.Int
tempResult.Mul(x, y) // tempResult = x * y
finalResult := new(big.Int)
finalResult.Add(tempResult, z) // finalResult = tempResult + z
fmt.Printf("Result: %s\n", finalResult.String()) // Result: 52
}
通常の演算子 (*) の使用
big.Int
型に対しては、Goの組み込み型のように*
演算子を使って直接掛け算することはできません。
エラーの例
package main
import (
"fmt"
"math/big"
)
func main() {
a := big.NewInt(10)
b := big.NewInt(5)
// コンパイルエラー: invalid operation: a * b (operator * not defined on big.Int)
// result := a * b
// コンパイルエラー: invalid operation: a * 5 (mismatched types *big.Int and int)
// result := a * 5
}
原因
Goの演算子はオーバーロードできないため、big.Int
のようなカスタム型に対しては、専用のメソッド(この場合はMul()
)を使用する必要があります。
トラブルシューティング/解決策
常にbig.Int.Mul()
メソッドを使用します。
package main
import (
"fmt"
"math/big"
)
func main() {
a := big.NewInt(10)
b := big.NewInt(5)
result := new(big.Int)
result.Mul(a, b) // 正しい使い方
fmt.Printf("Result: %s\n", result.String()) // Result: 50
}
型変換の不足
big.Int
は他の数値型(int64
、float64
など)と直接演算できません。必要に応じて型変換を行う必要があります。
問題の例
package main
import (
"fmt"
"math/big"
)
func main() {
a := big.NewInt(10)
b := 5 // int 型
// コンパイルエラー: mismatched types *big.Int and int
// result.Mul(a, b)
// ここでも型変換が必要
// result := new(big.Int)
// result.Add(a, b) // Add も同様に直接は使えない
}
トラブルシューティング/解決策
big.Int
型と他の型の間で演算を行う場合、big.Int.SetInt64()
やbig.NewInt()
などを使用してbig.Int
型に変換してから演算を行います。
package main
import (
"fmt"
"math/big"
)
func main() {
a := big.NewInt(10)
b := 5 // int 型
c := int64(2) // int64 型
// int を big.Int に変換してから Mul
bigB := big.NewInt(int64(b)) // int を int64 にキャストしてから big.NewInt
result1 := new(big.Int)
result1.Mul(a, bigB)
fmt.Printf("a * b: %s\n", result1.String()) // a * b: 50
// int64 を big.Int に変換してから Mul
bigC := big.NewInt(c)
result2 := new(big.Int)
result2.Mul(a, bigC)
fmt.Printf("a * c: %s\n", result2.String()) // a * c: 20
}
big.Int.Mul()
を使用する際の主なトラブルシューティングのポイントは以下の通りです。
- 他の数値型と演算する際は、必ず
big.Int
型に変換すること。 - 通常の
*
演算子はbig.Int
型には使えないので、常にMul()
メソッドを使用すること。 - 結果を格納する
big.Int
と引数のbig.Int
が同じインスタンスでも動作はするが、複雑な計算では中間結果用に新しいbig.Int
インスタンスを用意するのが安全で分かりやすい。 nil
ポインタの初期化忘れに注意し、new(big.Int)
やbig.NewInt()
で必ず初期化すること。
基本的な掛け算の例
これは最も基本的な使い方です。2つのbig.Int
型の値を掛け合わせ、その結果を別のbig.Int
変数に格納します。
package main
import (
"fmt"
"math/big"
)
func main() {
// 1. big.Int の初期化
// big.NewInt(値) で初期化するのが最も一般的です。
// ここでは、int64の最大値を超えるような大きな数値を設定してみます。
num1 := big.NewInt(0)
num1.SetString("123456789012345678901234567890", 10) // 10進数で文字列から設定
num2 := big.NewInt(0)
num2.SetString("987654321098765432109876543210", 10) // 別の大きな数値
// 2. 結果を格納するための big.Int 変数を準備
// new(big.Int) でゼロ値の big.Int ポインタを作成します。
result := new(big.Int)
// 3. Mul メソッドを使って掛け算を実行
// result.Mul(num1, num2) は、num1 と num2 の積を計算し、
// その結果を result に格納します。
result.Mul(num1, num2)
// 4. 結果の出力
// big.Int の値を文字列として出力するには .String() メソッドを使います。
fmt.Printf("num1 = %s\n", num1.String())
fmt.Printf("num2 = %s\n", num2.String())
fmt.Printf("num1 * num2 = %s\n", result.String())
// 出力例:
// num1 = 123456789012345678901234567890
// num2 = 987654321098765432109876543210
// num1 * num2 = 12193263111263526913617349141042784518335212389146197305910900
}
解説
Mul(x, y *Int)
:x
とy
を掛け、その結果をレシーバ(この場合はresult
)に格納します。new(big.Int)
: 結果を格納するbig.Int
のポインタを初期化するために使用します。nil
ポインタの参照を防ぐために、必ずMul
メソッドを呼び出す前に初期化してください。SetString(s string, base int)
:big.Int
は、int64
の範囲を超える数値を直接リテラルで書くことができないため、文字列として与え、基数(この場合は10進数)を指定して設定します。
同じ変数での上書き(自己参照)の例
Mul
メソッドは、結果をレシーバに上書きします。引数とレシーバが同じbig.Int
インスタンスであっても正しく動作します。
package main
import (
"fmt"
"math/big"
)
func main() {
val := big.NewInt(10) // 初期値 10
fmt.Printf("Initial val = %s\n", val.String()) // Initial val = 10
// val = val * 5
val.Mul(val, big.NewInt(5))
fmt.Printf("val * 5 = %s\n", val.String()) // val * 5 = 50
// val = val * val (val の自乗)
val.Mul(val, val)
fmt.Printf("val * val (50 * 50) = %s\n", val.String()) // val * val (50 * 50) = 2500
// 複数の掛け算をチェインすることも可能
// val = val * 2 * 3
val.Mul(val, big.NewInt(2)).Mul(val, big.NewInt(3))
fmt.Printf("val * 2 * 3 (2500 * 2 * 3) = %s\n", val.String()) // val * 2 * 3 (2500 * 2 * 3) = 15000
}
解説
- メソッドチェーン:
Mul
メソッドは*big.Int
を返すため、.Mul(...)
のように続けて別のMul
メソッドを呼び出すことができます。これは便利ですが、計算の順序と結果の格納先を明確に理解しておく必要があります。 val.Mul(val, val)
:val
の自乗を計算し、val
に格納します。val.Mul(val, big.NewInt(5))
:val
の現在の値に5を掛け、その結果を再びval
に格納します。元のval
の値は上書きされます。
他の型との掛け算(型変換の必要性)の例
big.Int
はGoの組み込み型(int
, int64
など)と直接演算できません。他の型をbig.Int
に変換してからMul
メソッドを使う必要があります。
package main
import (
"fmt"
"math/big"
)
func main() {
bigNum := big.NewInt(100) // big.Int 型
intNum := 5 // int 型
int64Num := int64(20) // int64 型
// intNum を big.Int に変換してから掛け算
result1 := new(big.Int)
result1.Mul(bigNum, big.NewInt(int64(intNum))) // int を int64 にキャストしてから big.NewInt
fmt.Printf("bigNum * intNum = %s\n", result1.String()) // bigNum * intNum = 500
// int64Num を big.Int に変換してから掛け算
result2 := new(big.Int)
result2.Mul(bigNum, big.NewInt(int64Num))
fmt.Printf("bigNum * int64Num = %s\n", result2.String()) // bigNum * int64Num = 2000
// 負の数との掛け算
negativeNum := big.NewInt(-7)
result3 := new(big.Int)
result3.Mul(bigNum, negativeNum)
fmt.Printf("bigNum * negativeNum = %s\n", result3.String()) // bigNum * negativeNum = -700
}
解説
big.NewInt(int64(intNum))
:int
型のintNum
を直接big.NewInt()
に渡すことはできません(型が異なるため)。一度int64
にキャストしてからbig.NewInt()
に渡すことで、big.Int
型の値を作成しています。
複数のbig.Int
の演算を組み合わせて複雑な式を計算する例です。中間結果を格納するbig.Int
変数を適宜用意することが重要です。
package main
import (
"fmt"
"math/big"
)
func main() {
A := big.NewInt(0)
A.SetString("1000000000000000000", 10) // 10^18
B := big.NewInt(0)
B.SetString("2000000000000000000", 10) // 2 * 10^18
C := big.NewInt(5)
D := big.NewInt(3)
// 例: (A * B) - (C * D) を計算する
// まず A * B を計算
mulAB := new(big.Int)
mulAB.Mul(A, B)
fmt.Printf("A * B = %s\n", mulAB.String())
// 次に C * D を計算
mulCD := new(big.Int)
mulCD.Mul(C, D)
fmt.Printf("C * D = %s\n", mulCD.String())
// 最後に引き算 (math/big パッケージの Add/Sub なども同様のインターフェース)
finalResult := new(big.Int)
finalResult.Sub(mulAB, mulCD) // Subtract メソッド
fmt.Printf("(A * B) - (C * D) = %s\n", finalResult.String())
// 出力例:
// A * B = 2000000000000000000000000000000000000
// C * D = 15
// (A * B) - (C * D) = 1999999999999999999999999999999999985
}
math/big
パッケージには、Mul
だけでなく、Add
(足し算)、Sub
(引き算)、Div
(割り算)、Mod
(剰余)、Exp
(べき乗)など、さまざまな算術演算メソッドが用意されており、これらも同様のインターフェース(z.Op(x, y)
形式)を持っています。mulAB
とmulCD
という中間結果を格納するためのbig.Int
変数を導入しています。これにより、計算の各ステップが明確になり、コードの読みやすさとデバッグのしやすさが向上します。
厳密に言えば、Go言語の標準ライブラリにおいて、big.Int
型の値を「掛け算」するための直接的な「代替メソッド」は存在しません。math/big
パッケージが提供するMul()
メソッドが、その目的のための唯一の公式な方法です。
しかし、「代替手段」という言葉を広くとらえ、以下のような観点から解説します。
- 他の演算との組み合わせによる乗算のシミュレーション(非現実的)
- パフォーマンスが問題になる場合の最適化戦略
- サードパーティライブラリの利用(一般的ではないが可能性として)
- そもそも
big.Int
が必要ないケース
他の演算との組み合わせによる乗算のシミュレーション (非現実的)
これは理論上の話であり、実際のプログラミングでMul()
の代替として使うことは全く推奨されません。多倍長整数の掛け算は、コンピュータ内部では基本的に「繰り返し足し算」や、より効率的なアルゴリズム(例: Karatsubaアルゴリズム、Toom-Cookアルゴリズムなど)で実装されています。
例えば、A×B は、A を B 回足すことと同じです。
package main
import (
"fmt"
"math/big"
)
func main() {
a := big.NewInt(100)
b := big.NewInt(5)
// Mul の代替として、a を b 回足す (非常に非効率!)
simulatedResult := big.NewInt(0) // 初期値 0
// b の値を int64 に変換(b が大きすぎるとオーバーフローする)
bInt64 := b.Int64()
if b.Cmp(big.NewInt(0)) < 0 { // b が負の場合
fmt.Println("Negative multiplier is not supported in this simulation.")
return
}
if b.Cmp(big.NewInt(1<<63-1)) > 0 { // b が int64 の最大値を超える場合
fmt.Println("Multiplier is too large for int64 conversion.")
return
}
for i := int64(0); i < bInt64; i++ {
simulatedResult.Add(simulatedResult, a) // simulatedResult = simulatedResult + a
}
fmt.Printf("a = %s, b = %s\n", a.String(), b.String())
fmt.Printf("Simulated a * b = %s\n", simulatedResult.String()) // 結果: 500
// 比較のための正規の Mul
actualResult := new(big.Int).Mul(a, b)
fmt.Printf("Actual a * b = %s\n", actualResult.String()) // 結果: 500
}
なぜ非現実的か
- int64への変換が必要
ループのカウンタは通常の整数型で管理する必要があるため、掛け算の片方がint64
の範囲に収まらない場合、この方法自体が破綻します。 - 非常に非効率的
big.Int
は巨大な数を扱うため、B
が非常に大きい場合(例: 10100)、このループは天文学的な回数実行され、現実的な時間では完了しません。Mul()
メソッドは、このような大きな数の掛け算に特化した高速なアルゴリズムを内部で利用しています。
したがって、これはあくまで概念的な説明であり、実際のコードでMul()
の代わりに使うべきではありません。
パフォーマンスが問題になる場合の最適化戦略
big.Int.Mul()
自体が非常に最適化されたコードですが、アプリケーション全体のパフォーマンスが問題になる場合、Mul()
の呼び出し回数を減らす、またはbig.Int
自体の利用を避けるなどのアプローチが「代替」となり得ます。
-
並列化
- 複数の独立した
big.Int.Mul()
演算がある場合、Goのgoroutineとチャネルを使って並列に実行することで、全体のスループットを向上させることが可能です。ただし、一つのMul()
演算自体を並列化するのは、math/big
パッケージの内部実装に依存するため、通常はできません。
- 複数の独立した
-
計算結果のキャッシュ
- 同じ
big.Int.Mul()
の結果が何度も必要になる場合、一度計算した結果をキャッシュ(メモリに保存)しておき、再利用することで計算時間を短縮します。特に、非常に大きな数の掛け算は計算コストが高いため、キャッシュは有効な戦略です。
- 同じ
-
必要最小限のbig.Intの利用
- 計算の途中で
big.Int
が必要になる部分だけをbig.Int
にし、それ以外の部分は通常のint64
などで処理できないか検討します。 - 例えば、小さな定数との乗算であれば、
big.NewInt(someInt64).Mul(someBigInt, big.NewInt(smallInt64))
のように、都度big.Int
に変換して計算します。
- 計算の途中で
-
- 計算ロジックを見直し、
big.Int
での乗算回数を減らせないか検討します。例えば、共通因数を事前に計算しておく、指数計算の場合はExp()
メソッド(べき乗)を利用するなどです。 big.Int.Exp(x, y, m)
は、xy(modm) を効率的に計算できます。もしモジュロ演算が必要な場合、これは繰り返し乗算よりもはるかに高速です。
- 計算ロジックを見直し、
サードパーティライブラリの利用 (一般的ではないが可能性として)
Goのmath/big
パッケージは多倍長整数演算の標準実装であり、非常に効率的です。しかし、ごく稀に、特定のニッチな要件(例: 特定のハードウェアに最適化された実装、異なるアルゴリズムの評価など)のために、サードパーティの多倍長整数ライブラリが存在する可能性があります。
そもそもbig.Intが必要ないケース
これは「代替手段」というよりは、「big.Int.Mul()
を使うべきではないケース」と考えるべきです。
-
浮動小数点数の計算が必要な場合
もし計算結果が整数ではなく、非常に高精度の浮動小数点数が必要な場合は、math/big.Float
型を検討するべきです。big.Int
は整数のみを扱います。 -
int64の範囲に収まる場合
計算結果がint64
(約9×1018)の範囲に収まることが確実であれば、通常のint64
型を使用するべきです。big.Int
を使うと、アロケーション(メモリ確保)やポインタのデリファレンス、内部の複雑なアルゴリズムのため、通常のint64
演算に比べて著しくパフォーマンスが低下します。package main import "fmt" func main() { // big.Int を使う必要がない例 a := 123456789 b := 987654321 // 結果が int64 の範囲に収まることが明らかな場合 result := int64(a) * int64(b) // int を int64 にキャストして乗算 fmt.Printf("int64 result = %d\n", result) // int64 result = 121932631112635269 }
Go言語でbig.Int
型の掛け算を行う場合、big.Int.Mul()
メソッドが標準的かつ最も効率的な唯一の公式な方法です。
「代替手段」を考える場合は、以下の点を考慮してください。
- そもそも
big.Int
が必要なのかを再検討し、int64
やfloat64
で十分な場合はそちらを使用するべきです。 - パフォーマンスが問題の場合、
Mul()
自体の代替ではなく、計算アルゴリズムの改善、キャッシュ、並列化、big.Int
の利用範囲の限定といったより上位の最適化戦略を検討します。 Mul()
を他の演算でシミュレートするのは非現実的で非効率的です。