mathライブラリ
最近Goを勉強していて絶対値を求めたい場面があったのでそれについて調べたんですが、どうやらintの計算ではわざわざmath.Abs()を呼び出すよりも自分で関数を書いたほうが早いということがわかりました。1.21以降のGoではmaxやminも標準ライブラリに組み込まれているので、mathライブラリはもうimportしない方向で書きたいな〜と思っていたのですが、切り上げを実装したいタイミングがあり躓きました。
そこで調べてみたところ、の商を切り上げたい場合はを使うと良いとの事。
この関数はが非負整数かつが正の数の場合にのみ正しく動作します
func ceilDiv(a int,b int) int {
return (a+b-1)/b
}
試しに使ってみると期待通りの結果になったのですが、イマイチパット見で式の意味を理解できなかったので考えてみました。
式を分解してみる
(a+b-1)/bという式の分子は、aとb-1という二つの項に分解できます。つまり、やっていることとしては商に(b-1)/bをそのまま足しているだけなのです。実際に数字を代入して見てみます。
b=3の時、分子を1ずつ上げていってみると…
と、余りが1/b以上のときに(b-1)/bが足されることで1以上に変わり切り上げられていることがわかります。
自分ではすぐ思いつけなかった式なので、分解して考えてみたら納得できて面白かったです。
余談: 負の数について
先程述べたように、この式は0≦a且つ0<bでないと間違った答えを出します。
例えばaが負のa=-4 b=3のとき、-4/3は-1.333……となるため切り上げの場合は-1が求められるわけですが、この式で計算すると(-4+3-1)/3=-2/3=0.666……となるので最終的に0が返されてしまいます。
またbが正でない場合、の0除算が成立しないのは自明なため割愛しますが、負であるa=4 b=-3のとき、こちらも同様に-1が求められるはずですが、この式で計算すると(4-3-1)/3=0/3=0となり間違った答えである0が返されてしまいます。
負の数値に対応したい場合はどうするべきか気になったので、ベンチマークを計測してみました。
計測に使用したコード
package main
import (
"math"
)
func CeilDiv1(a int32, b int32) int32 {
return (a+b-1)/b
}
func CeilDiv2(a int32, b int32) int32 {
if (a > 0) == (b > 0) {
if b > 0 {
return (a + b - 1) / b
}
return (a + b + 1) / b
}
return a / b
}
func CeilDiv3(a int32, b int32) int32 {
q := a / b
if a%b != 0 && ((a > 0) == (b > 0)) {
q++
}
return q
}
func CeilDiv4(a int32, b int32) int32 {
return int32(math.Ceil(float64(a) / float64(b)))
}
func CeilDiv5(a int32, b int32) int32 {
if (a >= 0) && (b > 0) {
return (a+b-1)/b
}
return int32(math.Ceil(float64(a) / float64(b)))
}
func main() {
}
package main
import (
"testing"
)
var blackhole int32
func BenchmarkCeilDiv1(b *testing.B) {
for i := 0; i < b.N; i++ {
for a := int32(-100); a < 100; a++ {
for bb := int32(1); bb < 100; bb++ {
blackhole = CeilDiv1(a, bb)
blackhole = CeilDiv1(a, -bb)
}
}
}
}
func BenchmarkCeilDiv2(b *testing.B) {
for i := 0; i < b.N; i++ {
for a := int32(-100); a < 100; a++ {
for bb := int32(1); bb < 100; bb++ {
blackhole = CeilDiv2(a, bb)
blackhole = CeilDiv2(a, -bb)
}
}
}
}
func BenchmarkCeilDiv3(b *testing.B) {
for i := 0; i < b.N; i++ {
for a := int32(-100); a < 100; a++ {
for bb := int32(1); bb < 100; bb++ {
blackhole = CeilDiv3(a, bb)
blackhole = CeilDiv3(a, -bb)
}
}
}
}
func BenchmarkCeilDiv4(b *testing.B) {
for i := 0; i < b.N; i++ {
for a := int32(-100); a < 100; a++ {
for bb := int32(1); bb < 100; bb++ {
blackhole = CeilDiv4(a, bb)
blackhole = CeilDiv4(a, -bb)
}
}
}
}
func BenchmarkCeilDiv5(b *testing.B) {
for i := 0; i < b.N; i++ {
for a := int32(-100); a < 100; a++ {
for bb := int32(1); bb < 100; bb++ {
blackhole = CeilDiv5(a, bb)
blackhole = CeilDiv5(a, -bb)
}
}
}
}
各関数の挙動は以下の通りです。負の数に対応したい場合はCeilDiv2~5の中から選ぶ必要があります。
| 関数 | 挙動 |
|---|---|
| CeilDiv1 | (0≦aで0<bじゃないと壊れる) |
| CeilDiv2 | 符号を見て式を(a+b-1)/b、(a+b+1)/b、a/bの中から選ぶ |
| CeilDiv3 | 符号と余りから判定して+1 |
| CeilDiv4 | math.Ceil() |
| CeilDiv5 | 0≦aで0<bなら、それ以外ならmath.Ceil() |
結果はこんな感じになりました。
BenchmarkCeilDiv1-16 54919 21863 ns/op 0 B/op 0 allocs/op
BenchmarkCeilDiv2-16 27345 43815 ns/op 0 B/op 0 allocs/op
BenchmarkCeilDiv3-16 26703 44956 ns/op 0 B/op 0 allocs/op
BenchmarkCeilDiv4-16 40698 29519 ns/op 0 B/op 0 allocs/op
BenchmarkCeilDiv5-16 46238 26216 ns/op 0 B/op 0 allocs/op
やはり条件付きとはいえが最速のようです。負の数に対応した関数ではmathライブラリとの両刀型が最速のようですが、素のmath.Ceil()とは10%程度の差なので特にこだわりがない場合はそちらを使用したほうが良さそうです。
CeilDiv2と3はLLMが書いてくれたのですが、かなり微妙な結果になりました。