🔍

Goも当然にMapよりSwitchが速い ― 制約によるコンパイル時最適化の威力

に公開
2025/09/09
7件

MapはO(1)、SwitchはO(N)?

Goのmapは、なにか深遠なるアルゴリズムによって取得時O(1)が成立していると聞いています。
switchは単純に一つ一つの節にマッチするか検証しているからO(N)ですよね?
だから、switchよりもmapを使ったほうがいいと思います

switchで十分実現できる処理がmapで書かれていたのでレビューで変えるように指摘すると、このようなことを言われました。
確かにmapは取得時O(1)の時間計算量が掲げられていますし、対してswitchは一つ一つ検証していそうです。
ここで、O(N)程度の時間計算量なんか気にするのはcaseが1万とかになってからにしろ、「推測するより計測せよ」「早すぎる最適化」だと、どっちもどっち論で話を終わらせてもいいのですが、Goのような恵まれた環境ではちょっとでもopが増えたりnsレベルでも速度が遅くなるのは気になるのかもしれません。

しかし実際に計測してみるとswitchのほうが速いケースのほうが多いです。
なんてこった!時間計算量が裏切った!信じていたのに!

納得できないレビュイーのために、せっかくなのでコンパイラの実装も交えて見ていきましょう。

switchはO(N)?

switch文をコンパイラがどう解釈するかは
https://github.com/golang/go/blob/go1.25.1/src/cmd/compile/internal/walk/switch.go#L280-L294
の部分がわかりやすいでしょうか。
jump tableをまず最初に適用可能か調べて適用可能であれば適用し、そうでなければ二分探索を行うという記述です。

jump tableは機械語で言えば大体1命令であるようなもので、ざっくり表現するなら配列アクセスと同じような処理で条件分岐を実現します。
mapアクセスのO(1)を信じているのであれば、配列アクセスと中身がほぼ同じだと言われればjump tableのO(1)はすんなりと信じることができるでしょう。

そして、適用外だった場合の二分探索ですが、O(\log N)ですね。
適用外になるには、jump table命令が使用できない[1]かjump table命令では非効率[2]かといったときですが、そうなったとしてもO(\log N)なわけです。

一応ですが、if文の列挙と同レベルまで落ちるケースもあります。
https://github.com/golang/go/blob/go1.25.1/src/cmd/compile/internal/walk/switch.go#L144-L154
逆に言えばこれまでの話はif文の列挙よりもswitch文の方が速くなりやすいという話でもあったわけです。

総じて、switchは基本的にcase数に対してO(1)、一部の場合でO(\log N)、もっとも不利な条件を仮定してもifの列挙という十分に高速なO(N)を安定的に提供してくれるということが言えます。

MapはO(1)?

翻ってGoのmapはどうでしょうか。
まず触れておかなければならないのは、mapがO(1)と言われているのが平均計算量のことであるということです。
ハッシュ値が衝突するなどの理由で、最悪計算量はO(N)にもなり、その時点で配列アクセスと同じものとみなせるものではありません。

最良でO(1)最悪でO(N)。さっきの説明のとおりならだいたいswitchと同じじゃん、と思うのも早計です。
まず、mapを動かすためにはhash計算が必要で、これは比較的重い処理であるというのが一つ。
またmapは、SwissTableなどアルゴリズムの進展でだいぶそのようなことはなくなりましたが、メモリ局所性などの理由から二分探索O(\log N)のTreeMapのほうが速いことすらあるという代物です。
対してswitchがO(1)でなくなるのは一部のケースに限られ、基本的には一貫してO(1)アクセスを提供します。

言い換えれば

  • 線形の計算量であってもNが小さいうちは定数項が支配的になりやすい (mapは定数項が重い、switchは定数項が軽い)
  • switchが線形の計算量であるというのが誤解

ということで、冒頭のレビュイーは推測の出発点を間違えていたということになります。

このようなことから、実際にベンチマークを取ってみたときに、mapで書くよりもif文の連鎖のほうが速く、それよりさらにswitch文で書いたほうが速いというケースがほとんどになるのです。
結論としてはswitchが基本的に速度面で有利なので、分岐数が100を超えてきて初めてmapを検討するために計測を始めるくらいの向き合い方でいいと思いますが、そもそもswitchで分岐数が100を超えそうなときは設計を疑ったほうがいいかもしれません。

ベンチマーク

古いPC(Windows10, Intel 6700K)で全くクリーンではないので参考までに
https://gist.github.com/CrafterKina/59bfd349160d9c111676911bca1d4056

実装タイプ 小規模(8) 中規模(100) 大規模(1000) 性能特性
整数リテラルSwitch 2.47 ns/op 6.83 ns/op 12.40 ns/op Jump Table
整数Map 11.32 ns/op 21.10 ns/op 18.71 ns/op Swiss Table
浮動小数点数リテラルSwitch - - 17.30 ns/op 二分探索
浮動小数点数Map - - 37.45 ns/op Swiss Table
変数Switch 0.59 ns/op 11.74 ns/op 285.1 ns/op 線形探索
感想

ベンチマークは実際の環境で困ったときに測るほうがよっぽど有用だと思っているので補足程度に認識お願いします。

整数リテラルswitchが大規模になるにつれて遅くなっていますが、アセンブリを見てみた感じjump tableは使っていたのでキャッシュミスとかなんですかね。まぁ1000ケースのswitch書いたらまずIDEが重くなったので実用的にはどうでもいいといえばどうでもいい。
整数Mapが中規模と大規模で速さ変わらないのはO(1)の片鱗をのぞかせてて面白いですね。
二分探索が別に遅くも早くもないのはまぁ1000程度でそんな差が出ることも望めないという感じ。二分探索のアセンブリも見れて満足。
線形探索でもmapよりswitchのほうが速いのはそれだけmapが重いのを示してますね。100程度だったらmapのO(1)よりsliceで線形探索した方が良いまであるというやつですね。

SwitchよりMapのほうが対応関係を示しているとわかりやすい?

MapはKeyとValueの対応を表すための構造を値として示せるじゃないですか
対してSwitchは制御構造だから、対応関係を表しているとわからないじゃないですか
だから、switchよりもmapを使ったほうが分かりやすくていいと思います

これもレビューで指摘したときに言われました。

switch v {
case 1:
    return "good morning"
case 2:
    return "good afternoon"
case 3:
    return "good evening"
default:
    return "hello"
}

という節内で即座に値をreturnしているswitchと、

map[int]string{
	1: "good morning",
	2: "good afternoon",
	3: "good evening",
}

というMapを見比べたとき、Mapが単純に対応関係を書いているだけなのに対し、switchは余計な構文がついてしまっているように見えます。
それもそのはずで、switchは即座に値を返して対応関係を作る以外のこともできるわけで、節内で即座に値のreturnしかしていないということは全節を見るまでわからないわけです。

また、一行目だけ見てみますと、

switch v {

はswitch文なんだなーと思うだけ

map[int]string {

はKeyがintでValueがstringのMapなんだなとわかるわけです。

なるほど、確かに冒頭の話は間違いではなさそうです。
ですが、速度よりも可読性を重視するとしてもmapは良い選択ではありません。

先程のmapを関数を通じて提供してみましょう。

var greetingMap = map[int]string{
	1: "good morning",
	2: "good afternoon",
	3: "good evening",
}

func Greeting(t int) string {
	if v, ok := greetingMap[t]; ok {
		return v
	}
	return "hello"
}

どうでしょうか?

まず、Greeting関数のインターフェースを知りたい人は、

func Greeting(t int) string

を見るので、

var greetingMap = map[int]string

という記述の重要さは小さくなりました。

そして、このGreeting関数がどういう対応関係を持つのかというのを把握するためには、greetingMapとGreeting関数のfallbackの分断された二箇所を見る必要が出てきたということもわかります。

さらに、mapは実行時に要素を変更することが可能なので、スコープ内でmapが変更される可能性を調べ上げなければその二箇所に書かれていることだけが真実だということも確定できないということも言えます。

これがswitchになれば

func Greeting(t int) string {
	switch t {
	case 1:
		return "good morning"
	case 2:
		return "good afternoon"
	case 3:
		return "good evening"
	default:
		return "hello"
	}
}

素晴らしい。mapの持っていた問題点はすべて解決し、インターフェースが一見してわかるという特徴はそのままです。

switchで新たに発生している問題といえば、構文が宣言的なものから手続き的なものに変化したことによって、やろうと思えばcaseの中で長い処理をベタ書きできるようになってしまったというのが思い浮かびますが……それはレビューで弾きましょう。
そんなことが起こり得る環境でswitchではなくmapであれば宣言的に書いてくれるはずという仮定も虚しいです。
逆に、基本的に純粋関数で書かれているプロジェクトなら、あまり気にしなくてもいいでしょう。

mapよりもswitchが当然に速いという感覚

記事冒頭のレビュイーの感覚に、いやいやとんでもない誤解だぞと、推測せずとも計測せずともswitchのほうが速いことを当然に期待するものじゃないかと思われた方々も多くいらっしゃると思います。
自分もそうで、最初からこの記事のように(特にGo言語において)完全な根拠を揃えて言語化してからmapではなくswitchを使えとレビューしたわけではありません。
switchのほうがmapより速くてわかりやすいでしょうというのを当然の感覚として持っていたわけです。

実際に計測すること、実際のコンパイラの実装を読むこと、実際に生成されるマシン語を読むこと、これらは確実ですがコストがかかります。
なにより、そもそもの感覚がなければ疑問にも思わず、計測も調査も行おうとすら思わないかもしれません。

コンパイラ最適化を信じているとか、ハッシュテーブル実装が重いことを経験的に知っているとか、そもそも条件式をベタ書きでいくら列挙しようがO(1)感覚でいいものでしょとか。個別具体で言えばそういうことになりますが、この件をより一般的な教訓にするなら

静的に決定できるものは静的な形で書け

ということです。
mapはメモリ上に積まれる構造体であるわけで、要素の増減はもちろんのこと、宣言時や取得時にも動的な要素が絡みます。
これは、コンパイル時の最適化が効きにくいことを意味していて、速度的に不利な傾向があります。
また動的であるということはそれだけ不確実性が増すということで、コード上の処理の追いやすさにも影響します。

この原則はmap以外のことにも言えて、最も単純なものでは変数よりも定数を選べとか、動的型よりも静的型を選べ、ということにも通底しています。
静的に決定できるものを静的に書くことが、静的型付け言語を好む人に共通する基本的なマインドセットとも言いかえられます。

動的型付け言語の人々はまた違ったマインドセットを持っていたりもします[3]が、静的型付け言語ではこの教訓を持っていれば一つ指針になるのではないでしょうか。

脚注
  1. 浮動小数点数、32bit環境の64bit数値、アーキテクチャにjump table命令が存在しないなど ↩︎

  2. switchの密度が十分に濃くないなど ↩︎

  3. 実際このレビュイーたちはJavaScriptの経験が主な人たちでした ↩︎

Discussion

藤田望藤田望

ベンチマークで「小規模」のcaseの数が8となっているのはテーブルジャンプに最適化される最少の条件に合わせているのだと思いますが記事も同様にされると良いのでは?
caseの数が3ではテーブルジャンプも二分探索もされないと思うので出現頻度の順でifで判定した方が速いまでありそうです。

藤田望藤田望

この記事はswitch推しですがswitchによるテーブルジャンプがmapよりも効率良いとしてもテーブルジャンプはCPUのパイプラインをフラッシュするのでそんな良いものでもなく連番から対応する文字列を取得する程度の処理であれば配列やsliceを使った方が効率的だと思います。
ベンチマークのmain_test.goに配列/sliceでの実装を加え、副作用のない関数の戻り値を_へ代入するのは最適化で関数の呼び出しが削除される可能性があるのでsinkへ代入する変更を加えてテストしてみました。

$ go version
go version go1.24.2 linux/amd64
$ go test -bench=.
goos: linux
goarch: amd64
pkg: example.com/benchmark
cpu: AMD Ryzen 7 7735HS with Radeon Graphics
BenchmarkSmallSwitch-16                    21831             54158 ns/op
BenchmarkSmallMap-16                       14983             78911 ns/op
BenchmarkSmallArray-16                    144888              8450 ns/op
BenchmarkMediumSwitch-16                   17983             66845 ns/op
BenchmarkMediumMap-16                      13786            100134 ns/op
BenchmarkMediumSlice-16                   131512              9087 ns/op
BenchmarkLargeSwitch-16                    16383             72969 ns/op
BenchmarkLargeMap-16                       12818             93943 ns/op
BenchmarkLargeSlice-16                    132778              8804 ns/op
BenchmarkSmallSwitchSingle-16           768503281                1.566 ns/op
BenchmarkSmallMapSingle-16              298777204                4.053 ns/op
BenchmarkSmallArraySingle-16            1000000000               1.134 ns/op
BenchmarkMediumSwitchSingle-16          128503909                9.447 ns/op
BenchmarkMediumMapSingle-16             172481728                6.975 ns/op
BenchmarkMediumSliceSingle-16           997997932                1.203 ns/op
BenchmarkLargeSwitchSingle-16           124961968                9.636 ns/op
BenchmarkLargeMapSingle-16              149200860                8.023 ns/op
BenchmarkLargeSliceSingle-16            991693106                1.195 ns/op
PASS
ok      example.com/benchmark   30.979s

Smallに対してMediumとLargeでのSwitchのパフォーマンスが低いので確認したところ
https://godbolt.org/z/oad1TcWqa
mediumSwitch()とlargeSwitch()のサイズが大きいためベンチマーク関数からの呼び出しでインライン展開されないことが原因のようです。

main_test.go変更差分
main_test.go
diff --git a/main_test.go b/main_test.go
index 74c8029..00f2503 100644
--- a/main_test.go
+++ b/main_test.go
@@ -17,6 +17,19 @@ var smallMap = map[int]string{
        8: "greetings",
 }

+var sink string
+
+var smallArray = [...]string{
+       1: "good morning",
+       2: "good afternoon",
+       3: "good evening",
+       4: "good night",
+       5: "hello",
+       6: "hi",
+       7: "hey",
+       8: "greetings",
+}
+
 func smallSwitch(t int) string {
        switch t {
        case 1:
@@ -47,6 +60,13 @@ func smallMapLookup(t int) string {
        return "unknown"
 }

+func smallArrayLookup(t int) string {
+        if t >= 1 && t < len(smallArray) {
+                return smallArray[t]
+        }
+        return "unknown"
+}
+
 var mediumMap = make(map[int]string)

 func init() {
@@ -55,6 +75,15 @@ func init() {
        }
 }

+var mediumSlice []string
+
+func init() {
+        mediumSlice = make([]string, 101)
+        for i := 1; i <= 100; i++ {
+                mediumSlice[i] = fmt.Sprintf("value_%d", i)
+        }
+}
+
 func mediumSwitch(t int) string {
        switch t {
        case 1:
@@ -269,6 +298,13 @@ func mediumMapLookup(t int) string {
        return "unknown"
 }

+func mediumSliceLookup(i int) string {
+        if i >= 1 && i < len(mediumSlice) {
+                return mediumSlice[i]
+        }
+        return "unknown"
+}
+
 // 大規模テスト用データ (1000 cases)
 var largeMap = make(map[int]string)

@@ -278,6 +314,15 @@ func init() {
        }
 }

+var largeSlice []string
+
+func init() {
+        largeSlice = make([]string, 1001)
+        for i := 1; i <= 1000; i++ {
+                largeSlice[i] = fmt.Sprintf("value_%d", i)
+        }
+}
+
 func largeSwitch(t int) string {
        switch t {
        case 1:
@@ -2292,6 +2337,13 @@ func largeMapLookup(t int) string {
        return "unknown"
 }

+func largeSliceLookup(i int) string {
+        if i >= 1 && i < len(largeSlice) {
+                return largeSlice[i]
+        }
+        return "unknown"
+}
+
 func generateTestData(size, maxKey int) []int {
        data := make([]int, size)
        for i := 0; i < size; i++ {
@@ -2311,7 +2363,7 @@ func BenchmarkSmallSwitch(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = smallSwitch(key)
+                       sink = smallSwitch(key)
                }
        }
 }
@@ -2322,7 +2374,18 @@ func BenchmarkSmallMap(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = smallMapLookup(key)
+                       sink = smallMapLookup(key)
+               }
+       }
+}
+
+func BenchmarkSmallArray(b *testing.B) {
+       testData := generateTestData(10000, 8)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               for _, key := range testData {
+                       sink = smallArrayLookup(key)
                }
        }
 }
@@ -2333,7 +2396,7 @@ func BenchmarkMediumSwitch(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = mediumSwitch(key)
+                       sink = mediumSwitch(key)
                }
        }
 }
@@ -2344,7 +2407,18 @@ func BenchmarkMediumMap(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = mediumMapLookup(key)
+                       sink = mediumMapLookup(key)
+               }
+       }
+}
+
+func BenchmarkMediumSlice(b *testing.B) {
+       testData := generateTestData(10000, 100)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               for _, key := range testData {
+                       sink = mediumSliceLookup(key)
                }
        }
 }
@@ -2355,7 +2429,7 @@ func BenchmarkLargeSwitch(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = largeSwitch(key)
+                       sink = largeSwitch(key)
                }
        }
 }
@@ -2366,43 +2440,72 @@ func BenchmarkLargeMap(b *testing.B) {

        for i := 0; i < b.N; i++ {
                for _, key := range testData {
-                       _ = largeMapLookup(key)
+                       sink = largeMapLookup(key)
+               }
+       }
+}
+
+func BenchmarkLargeSlice(b *testing.B) {
+       testData := generateTestData(10000, 1000)
+       b.ResetTimer()
+
+       for i := 0; i < b.N; i++ {
+               for _, key := range testData {
+                       sink = largeSliceLookup(key)
                }
        }
 }

 func BenchmarkSmallSwitchSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = smallSwitch(i%10 + 1)
+               sink = smallSwitch(i%10 + 1)
        }
 }

 func BenchmarkSmallMapSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = smallMapLookup(i%10 + 1)
+               sink = smallMapLookup(i%10 + 1)
+       }
+}
+
+func BenchmarkSmallArraySingle(b *testing.B) {
+       for i := 0; i < b.N; i++ {
+               sink = smallArrayLookup(i%10 + 1)
        }
 }

 func BenchmarkMediumSwitchSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = mediumSwitch(i%60 + 1)
+               sink = mediumSwitch(i%60 + 1)
        }
 }

 func BenchmarkMediumMapSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = mediumMapLookup(i%60 + 1)
+               sink = mediumMapLookup(i%60 + 1)
+       }
+}
+
+func BenchmarkMediumSliceSingle(b *testing.B) {
+       for i := 0; i < b.N; i++ {
+               sink = mediumSliceLookup(i%60 + 1)
        }
 }

 func BenchmarkLargeSwitchSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = largeSwitch(i%1100 + 1)
+               sink = largeSwitch(i%1100 + 1)
        }
 }

 func BenchmarkLargeMapSingle(b *testing.B) {
        for i := 0; i < b.N; i++ {
-               _ = largeMapLookup(i%1100 + 1)
+               sink = largeMapLookup(i%1100 + 1)
+       }
+}
+
+func BenchmarkLargeSliceSingle(b *testing.B) {
+       for i := 0; i < b.N; i++ {
+               sink = largeSliceLookup(i%1100 + 1)
        }
 }
1
藤田望藤田望

コードレビューではコードの確からしさや保守性を考慮された書き方をされてるかが重要でありパフォーマンス改善はそれが問題となっている場合でなければ重要ではありません。mapで十分実現できる処理をswitchに変えるよう指摘するのは適当ではないと思います。

MapはKeyとValueの対応を表すための構造を値として示せるじゃないですか
対してSwitchは制御構造だから、対応関係を表しているとわからないじゃないですか
だから、switchよりもmapを使ったほうが分かりやすくていいと思います

という主張は正当だし実際mapの方が保守性が高くて良いでしょう。パフォーマンスが重要ということであれば配列やsliceへの変更が労力も小さくて済むので現実的ですね。

KinaKina

そうですね。実際のところ、パフォーマンスは記事冒頭にも書いているようにどうでもいいです。
自分はmapよりもswitchのほうが保守性が高いと考えているので、パフォーマンスが悪いと考えてswitchよりもmapを選ぶのは正しくないというのが前段の趣旨です。
mapは動的にエントリーが増えていくことも含意している書き方ですので、静的に決定される値の対応関係というより狭義の構造を表すためには、それに特化しているswitchを選ぶべきだと考えています。
静的に解決できることによって、処理系にとっていいことがあると説明しているのが前段で、人間にとっていいことがあると説明しているのが後段ですね。

藤田望藤田望

「自分はmapよりもswitchのほうが保守性が高いと考えているので」ということで主観による指摘なわけですが、レビュイーも

MapはKeyとValueの対応を表すための構造を値として示せるじゃないですか
対してSwitchは制御構造だから、対応関係を表しているとわからないじゃないですか
だから、switchよりもmapを使ったほうが分かりやすくていいと思います

と主観による主張をされており、互いに言い分があるのであればレビュイーを納得させることは困難だと思います。
記事の脚注に「実際このレビュイーたちはJavaScriptの経験が主な人たちでした」とあることから察するにKinaさんはベテランでありレビュイーはそうではないのだと思います。レビューの場ではベテランは強い発言力を持つ場合が普通にあり、弱い側が納得をしてなくても指摘を受けざるを得ない状況ということは起こりうるでしょう。そうなっては禍根を残す原因にもなりそうですね。
主観による主張は平行線で解決は困難ですが社内やチームでのコーディングルールとして静的に書ける部分はそれを優先する等のルールが設けられていればそのようなストレスからは解放されます。指摘も「コーディングルールに従い~に修正してください」で済むこととなります。

KinaKina

コーディングルールを整えておけば無用な軋轢を生まないというのは、一つチーム運営上のテクニックですね。
「静的に書けるものは静的に書く」ルールを作る際に、それにはどのようなメリットが有るのかの理由を示す材料にこの記事を使ってもらえればと思います。

主観 vs.主観ではないかという部分について補足すると、このレビューはmapを関数を通じて提供した形のコードに対してのものであり、記事中でもその前提でswitchとmapの構文を比較しています。
「mapは対応関係を表す」に対して、関数で提供するのだから変わらないというレビューコメントですね。

これだけならどっちもどっちの主観の問題になっただけですが、さらにswitchとmapの構文によって

  • fallbackの記述位置の分離/集約
  • mapの実行時変更の可能性
  • mapでもswitchでも手続き的な値の生成という悪用が可能でレビューで弾く必要がある

という特徴があるために、switchを選ぶことに優位性が出てくるということを記事では解説しています。
「自分の考えでは」とつけたのは、保守性が高いとまで言うためには記事中の論点が全部どうでも良いとは思わないという前提が必要なため、前提の共有のない記事の読者に向けてです。
記事に挙げた特性のいずれも重視しない立場でもイーブンになるだけなので、構文に優位性があるということそれ自体は主観の問題ではありません。

ルール化は重要ですが、なぜそのルールになっているのか技術的に説明することで(立場の差がそれを難しくするとしても)建設的な議論が成り立つということも重要だと思います。

藤田望藤田望

この記事は表題から内容までかなりの部分を実行パフォーマンスについて説明されていると思いますが、重要なのが静的に書くということであれば全体の見直しをされると良いと思います。