Goで整数の絶対値を求めるのに math.Abs() を使わない方がいい理由
Go1. はじめに
Goで整数の絶対値を求めるとき、特に何も考えずに math.Abs()
を使っていました。しかし、math.Abs()
は float64
を引数に取る関数なので、本当に問題がないのか気になって調べてみました。その結果、整数の絶対値を求めるときに math.Abs()
を使うには注意が必要だということがわかりました。
注意が必要なのは以下の2点です。
以下に詳しく説明します。
2. int64
の大きい整数だと間違った値を返してしまう
int64
の整数の絶対値を求める場合、math.Abs()
は func Abs(x float64) float64
の形式の関数なので、int64(math.Abs(float64(x)))
のように、float64
に変換してから、math.Abs()
を適用する必要があります。しかし、64ビットの浮動小数点数表現である float64
の仮数部は52ビット[1]なので、52ビットに収まらない大きさの int64
の整数は正確に表現できない場合があります。
確かめるために、以下のように Test関数を作りました。
1 2 3 4 5 6 7 8 9 |
package main import ( "math" ) func Abs1(x int64) int64 { return int64(math.Abs(float64(x))) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package main import ( "math/rand" "testing" ) func TestAbs1(t *testing.T) { for x := (int64(1) << 53); 0 < x; x++ { got1 := Abs1(x) want1 := x if got1 != want1 { t.Errorf("Abs1(%v) = %v, want %v", x, got1, want1) break } } } |
テストを実行したところ、以下のようになりました。
1 2 3 4 5 |
$ go test --- FAIL: TestAbs1 (0.00s) main_test.go:14: Abs1(9007199254740993) = 9007199254740992, want 9007199254740993 FAIL exit status 1 |
x = 9007199254740993 (0000000000100000000000000000000000000000000000000000000000000001) で int64(math.Abs(float64(x)))
を使って絶対値を求めると、正しい絶対値の値とは異なり、エラーになっています。
このように、int64
の大きい整数だと、int64(math.Abs(float64(x)))
で絶対値を求めると、間違った値を返してしまうことがあります。
3. if
を使った場合よりも遅い
もうひとつわかったことは、math.Abs()
を使って整数の絶対値を求めるときの速度が、if
を使った場合よりも遅いということです。
こちらも確かめるために、以下のように Benchmarkを作成しました。今回は int32
の整数を使用しています。0 から 99 までの整数 x
に対して、x
と -x
の絶対値を求める速度を計測しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package main import ( "math" ) func Abs1(x int32) int32 { return int32(math.Abs(float64(x))) } func Abs2(x int32) int32 { if x >= 0 { return x } return -x } func Abs3(x int32) int32 { y := x >> 31 return (x ^ y) - y } func main() { } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "math/rand" "testing" ) var blackhole int32 func BenchmarkAbs1(b *testing.B) { for i := 0; i < b.N; i++ { for x := int32(0); x < 100; x++ { blackhole = Abs1(x) blackhole = Abs1(-x) } } } func BenchmarkAbs2(b *testing.B) { for i := 0; i < b.N; i++ { for x := int32(0); x < 100; x++ { blackhole = Abs2(x) blackhole = Abs2(-x) } } } func BenchmarkAbs3(b *testing.B) { for i := 0; i < b.N; i++ { for x := int32(0); x < 100; x++ { blackhole = Abs3(x) blackhole = Abs3(-x) } } } |
絶対値を計算する関数の3つ目の Abs3()
はネットで見つけたテクニックを実装したものです。
手元のPCでBenchmarkを実行したところ、以下のようになりました。(int64
でもBenchmarkを実行してみましたが、結果は同様でした。)
1 2 3 |
BenchmarkAbs1-24 7820001 153.2 ns/op 0 B/op 0 allocs/op BenchmarkAbs2-24 22875997 54.08 ns/op 0 B/op 0 allocs/op BenchmarkAbs3-24 20532565 58.97 ns/op 0 B/op 0 allocs/op |
この結果からわかるように、math.Abs()
を使って整数の絶対値を求める関数 Abs1()
が、if
を使って絶対値を求める関数 Abs2()
よりも3倍近く実行時間がかかっていることがわかります。また、ネットで見つけた整数の絶対値を求めるテクニックを使った関数 Abs3()
よりも Abs2()
の方が若干速いこともわかります。
4. おわりに
Goで整数の絶対値を求めるときに math.Abs()
を使う場合に、注意が必要な点についてまとめてみました。実行速度のことも考えると、math.Abs()
ではなく、if
を使った関数で絶対値を求める方がよさそうです。
間違いやおかしい点などございましたら、@hideaki_sakai までお知らせください。