切り捨ての割り算を切り上げに変える式 (a+b-1)/bが面白い

公開日: 2026年5月3日 19時5分 1401文字 7分間

Go言語において、整数型の切り上げ計算を行う際に$(a+b-1)/b$という式を用いる手法とその仕組みについて解説しています。この式は非負整数において非常に高速に動作しますが、負の数では正しく計算できないため注意が必要です。ベンチマークの結果、負の数に対応する場合でも条件付きの式が最速であり、汎用性を重視するならmathライブラリの利用が推奨されます。

mathライブラリ

最近Goを勉強していて絶対値を求めたい場面があったのでそれについて調べたんですが、どうやらintの計算ではわざわざmath.Abs()を呼び出すよりも自分で関数を書いたほうが早いということがわかりました。1.21以降のGoではmaxやminも標準ライブラリに組み込まれているので、mathライブラリはもうimportしない方向で書きたいな〜と思っていたのですが、切り上げを実装したいタイミングがあり躓きました。

そこで調べてみたところ、a/ba/bの商を切り上げたい場合は(a+b1)/b(a+b-1)/bを使うと良いとの事。

この関数はaaが非負整数かつbbが正の数の場合にのみ正しく動作します

func ceilDiv(a int,b int) int {
	return (a+b-1)/b
}

試しに使ってみると期待通りの結果になったのですが、イマイチパット見で式の意味を理解できなかったので考えてみました。

式を分解してみる

(a+b-1)/bという式の分子は、ab-1という二つの項に分解できます。つまり、やっていることとしては商に(b-1)/bをそのまま足しているだけなのです。実際に数字を代入して見てみます。

b=3の時、分子を1ずつ上げていってみると…

0/3+2/3=02/30/3 + 2/3 = 0⋯2/3

1/3+2/3=11/3 + 2/3 = 1

2/3+2/3=11/32/3 + 2/3 = 1⋯1/3

3/3+2/3=12/33/3 + 2/3 = 1⋯2/3

4/3+2/3=24/3 + 2/3 = 2

と、余りが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が正でない場合、b=0b=0の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(a+b1)/b(a+b−1)/b (0≦a0<bじゃないと壊れる)
CeilDiv2符号を見て式を(a+b-1)/b、(a+b+1)/b、a/bの中から選ぶ
CeilDiv3符号と余りから判定して+1
CeilDiv4math.Ceil()
CeilDiv50≦a0<bなら(a+b1)/b(a+b-1)/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

やはり条件付きとはいえ(a+b1)/b(a+b-1)/bが最速のようです。負の数に対応した関数ではmathライブラリとの両刀型が最速のようですが、素のmath.Ceil()とは10%程度の差なので特にこだわりがない場合はそちらを使用したほうが良さそうです。

CeilDiv2と3はLLMが書いてくれたのですが、かなり微妙な結果になりました。

参考にしたサイト