Goの検索処理 sliceとmapどっちが速いのだね?

こんにちは。サーバサイドエンジニアの藤原です。

コーヒーはローソン派です。
今までは店員さんが淹れてくれてましたが、最近自分で淹れる方式に変わっちゃいましたね。
うちの近くのローソンだけなのかな🤔
味も微妙に変わってるんですよね。

ということで、HRBrain Advent Calendar 6日目を担当させていただきます。 qiita.com

map or slice どっちかね?

go言語で検索や突き合わせ処理をパフォーマンスを考慮して実装する際に、sliceを使うべきかmapを使うべきか迷われたことはないでしょうか。

自分は、なんとなくKeyのバリエーションが多い場合はmapにしたほうが計算量が一定だから良さそうだよなーと思い、mapを使うケースが多いです。
しかし、果たしてそれで合っているのか前から気になってました。そこで今回改めてちゃんと調べてみました。

ポイント💡

単純に検索スピードだけ考えるとmapのほうが有利なのですが、実際にmapを使おうと思うとsliceからmapへの変換処理が必要です。
今回はそこも加味して計測します。
また、sliceの検索は線形探索と二分探索の2パターンで試したいと思います。二分探索の場合は事前のソートが必要ですのでそこも加味します。

まとめると、試すのは以下の3パターンです。

  • slice:線形探索
  • slice:ソート+二分探索
  • map:sliceをmap変換 + map検索

また、検索対象のデータの件数に加え、検索の実施回数もポイントになると思いますのでその組み合わせもいくつか試してみたいと思います。

実装

検索対象のデータ(intのslice)と検索データ(intのslice)を渡して、検索データが見つかった位置の一覧を返すというプログラムを書きます。 検索データが見つからない場合は-1を詰めます。

線形探索

// LinearSearch 線形探索
func LinearSearch(nums, searches []int) (pos []int) {
    pos = make([]int, len(searches))
    for si, s := range searches {
        pos[si] = -1
        for i, num := range nums {
            if s == num {
                pos[si] = i
                break
            }
        }
    }
    return pos
}

ソート+二分探索

// BinarySearch 二分探索
func BinarySearch(nums, searches []int) (pos []int) {
    sort.Slice(nums, func(i, j int) bool { return nums[i] < nums[j] })
    return binarySearch(nums, searches)
}

// numsはソート済み
func binarySearch(nums, searches []int) (pos []int) {
    pos = make([]int, len(searches))
    for si, s := range searches {
        idx := sort.Search(len(nums), func(i int) bool { return nums[i] >= s })
        if idx < len(nums) && nums[idx] == s {
            pos[si] = idx
        } else {
            pos[si] = -1
        }
    }
    return pos
}

sliceをmap変換 + map検索

// MapSearch ハッシュ(Map)探索
func MapSearch(nums, searches []int) (pos []int) {
    m := sliceToMap(nums)
    return mapSearch(m, searches)
}

func mapSearch(m map[int]int, searches []int) (pos []int) {
    pos = make([]int, len(searches))
    for si, s := range searches {
        if idx, ok := m[s]; ok {
            pos[si] = idx
        } else {
            pos[si] = -1
        }
    }
    return pos
}

func sliceToMap(nums []int) map[int]int {
    m := make(map[int]int, len(nums))
    for i, num := range nums {
        m[num] = i
    }
    return m
}

データ

検索対象のデータと検索データの組み合わせは以下を用意します。

検索対象のデータ件数 検索データ件数
1000 3
1000 100
1000 1000
10000 3
10000 100
10000 1000
10000 10000
50000 10000

また、線形探索はデータの一致する位置次第で計算量に差が出てしまうので、 今回は「先頭から順に一致するパターン」と「全く一致しないパターン」の2パターンにします。 「全く一致しないパターン」の場合、検索対象のデータを全て見るという動きになるので線形探索にとっては不利にはなります。

計測用の実装

goのbenchmark機能を用いて計測します。

冗長なテストコードは見るのも直すのも辛いのでこんな感じで共通化します。

type searchFunc func(nums, searches []int) (pos []int)

func commonBenchmark(b *testing.B, nums, searches []int, f searchFunc) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        f(nums, searches)
    }
}

そしてこのように引数変えるだけで、いい感じになるようにします。

//
// data-1000, search-3
//

func Benchmark_LinearSearch_1000__3(b *testing.B) {
    commonBenchmark(b, nums1000, search3, LinearSearch)
}

func Benchmark_BinarySearch_1000__3(b *testing.B) {
    commonBenchmark(b, nums1000, search3, BinarySearch)
}


//
// data-1000, search-100
//

func Benchmark_LinearSearch_1000__100(b *testing.B) {
    commonBenchmark(b, nums1000, search100, LinearSearch)
}

結果

準備できましたので、動かしてみましょう!

go test -bench . -benchmem
Benchmark_LinearSearch_1000__3_NotFound-12               1200898              1041 ns/op              32 B/op          1 allocs/op
Benchmark_BinarySearch_1000__3_NotFound-12                 35037             34245 ns/op              96 B/op          3 allocs/op
Benchmark_binarySearch_1000__3_NotFound-12               9712423               115 ns/op              32 B/op          1 allocs/op
Benchmark_MapSearch____1000__3_NotFound-12                 38277             32859 ns/op           41193 B/op          8 allocs/op
Benchmark_mapSearch____1000__3_NotFound-12              17367688              61.9 ns/op              32 B/op          1 allocs/op
Benchmark_LinearSearch_1000__100_NotFound-12               41889             29805 ns/op             896 B/op          1 allocs/op
Benchmark_BinarySearch_1000__100_NotFound-12               30975             37237 ns/op             960 B/op          3 allocs/op
Benchmark_binarySearch_1000__100_NotFound-12              342697              3282 ns/op             896 B/op          1 allocs/op
Benchmark_MapSearch____1000__100_NotFound-12               36074             32351 ns/op           42057 B/op          8 allocs/op
Benchmark_mapSearch____1000__100_NotFound-12              783559              1288 ns/op             896 B/op          1 allocs/op
Benchmark_LinearSearch_1000__1000_NotFound-12               3541            287076 ns/op            8192 B/op          1 allocs/op
Benchmark_BinarySearch_1000__1000_NotFound-12              18427             67671 ns/op            8256 B/op          3 allocs/op
Benchmark_binarySearch_1000__1000_NotFound-12              37875             31188 ns/op            8192 B/op          1 allocs/op
Benchmark_MapSearch____1000__1000_NotFound-12              27123             45042 ns/op           49353 B/op          8 allocs/op
Benchmark_mapSearch____1000__1000_NotFound-12              83547             12659 ns/op            8192 B/op          1 allocs/op
Benchmark_LinearSearch_10000_3_NotFound-12                133090             10778 ns/op              32 B/op          1 allocs/op
Benchmark_BinarySearch_10000_3_NotFound-12                  1992            587961 ns/op              96 B/op          3 allocs/op
Benchmark_binarySearch_10000_3_NotFound-12               6005101               195 ns/op              32 B/op          1 allocs/op
Benchmark_MapSearch____10000_3_NotFound-12                  3416            370013 ns/op          322289 B/op         13 allocs/op
Benchmark_mapSearch____10000_3_NotFound-12              19178061              63.1 ns/op              32 B/op          1 allocs/op
Benchmark_LinearSearch_10000_100_NotFound-12                4141            272541 ns/op             896 B/op          1 allocs/op
Benchmark_BinarySearch_10000_100_NotFound-12                2673            452406 ns/op             960 B/op          3 allocs/op
Benchmark_binarySearch_10000_100_NotFound-12              284740              4563 ns/op             896 B/op          1 allocs/op
Benchmark_MapSearch____10000_100_NotFound-12                2964            346300 ns/op          323174 B/op         13 allocs/op
Benchmark_mapSearch____10000_100_NotFound-12              938948              1285 ns/op             896 B/op          1 allocs/op
Benchmark_LinearSearch_10000_1000_NotFound-12                435           2870078 ns/op            8192 B/op          1 allocs/op
Benchmark_BinarySearch_10000_1000_NotFound-12               2119            486083 ns/op            8256 B/op          3 allocs/op
Benchmark_binarySearch_10000_1000_NotFound-12              26682             40681 ns/op            8192 B/op          1 allocs/op
Benchmark_MapSearch____10000_1000_NotFound-12               3466            358536 ns/op          330494 B/op         13 allocs/op
Benchmark_mapSearch____10000_1000_NotFound-12              90537             12546 ns/op            8192 B/op          1 allocs/op
Benchmark_LinearSearch_10000_10000_NotFound-12                38          27388279 ns/op           81920 B/op          1 allocs/op
Benchmark_BinarySearch_10000_10000_NotFound-12              1333            844088 ns/op           81984 B/op          3 allocs/op
Benchmark_binarySearch_10000_10000_NotFound-12              2542            417791 ns/op           81920 B/op          1 allocs/op
Benchmark_MapSearch____10000_10000_NotFound-12              2362            537530 ns/op          404225 B/op         13 allocs/op
Benchmark_mapSearch____10000_10000_NotFound-12              9194            120524 ns/op           81920 B/op          1 allocs/op
Benchmark_LinearSearch_50000_10000_NotFound-12                 7         171894166 ns/op           81920 B/op          1 allocs/op
Benchmark_BinarySearch_50000_10000_NotFound-12               382           3537735 ns/op           81984 B/op          3 allocs/op
Benchmark_binarySearch_50000_10000_NotFound-12              2385            471677 ns/op           81920 B/op          1 allocs/op
Benchmark_MapSearch____50000_10000_NotFound-12               452           2471348 ns/op         1494987 B/op        847 allocs/op
Benchmark_mapSearch____50000_10000_NotFound-12              9882            178866 ns/op           81920 B/op          1 allocs/op
Benchmark_LinearSearch_1000__3-12                        1355110               887 ns/op              32 B/op          1 allocs/op
Benchmark_BinarySearch_1000__3-12                          33398             33985 ns/op              96 B/op          3 allocs/op
Benchmark_binarySearch_1000__3-12                        9556736               138 ns/op              32 B/op          1 allocs/op
Benchmark_MapSearch____1000__3-12                          33405             31856 ns/op           41193 B/op          8 allocs/op
Benchmark_mapSearch____1000__3-12                       21335660              50.7 ns/op              32 B/op          1 allocs/op
Benchmark_LinearSearch_1000__100-12                       501254              2388 ns/op             896 B/op          1 allocs/op
Benchmark_BinarySearch_1000__100-12                        30468             41578 ns/op             960 B/op          3 allocs/op
Benchmark_binarySearch_1000__100-12                       269874              3892 ns/op             896 B/op          1 allocs/op
Benchmark_MapSearch____1000__100-12                        35611             32581 ns/op           42057 B/op          8 allocs/op
Benchmark_mapSearch____1000__100-12                      1000000              1081 ns/op             896 B/op          1 allocs/op
Benchmark_LinearSearch_1000__1000-12                        8306            152265 ns/op            8192 B/op          1 allocs/op
Benchmark_BinarySearch_1000__1000-12                       10000            105576 ns/op            8256 B/op          3 allocs/op
Benchmark_binarySearch_1000__1000-12                       17820             68080 ns/op            8192 B/op          1 allocs/op
Benchmark_MapSearch____1000__1000-12                       21610             53298 ns/op           49353 B/op          8 allocs/op
Benchmark_mapSearch____1000__1000-12                       58798             18423 ns/op            8192 B/op          1 allocs/op
Benchmark_LinearSearch_10000_3-12                        1377502               874 ns/op              32 B/op          1 allocs/op
Benchmark_BinarySearch_10000_3-12                           2472            447825 ns/op              96 B/op          3 allocs/op
Benchmark_binarySearch_10000_3-12                        7272561               164 ns/op              32 B/op          1 allocs/op
Benchmark_MapSearch____10000_3-12                           3445            337668 ns/op          322346 B/op         13 allocs/op
Benchmark_mapSearch____10000_3-12                       23896603              51.3 ns/op              32 B/op          1 allocs/op
Benchmark_LinearSearch_10000_100-12                       504427              2480 ns/op             896 B/op          1 allocs/op
Benchmark_BinarySearch_10000_100-12                         2534            454719 ns/op             960 B/op          3 allocs/op
Benchmark_binarySearch_10000_100-12                       210144              5019 ns/op             896 B/op          1 allocs/op
Benchmark_MapSearch____10000_100-12                         3038            337526 ns/op          323174 B/op         13 allocs/op
Benchmark_mapSearch____10000_100-12                       971736              1129 ns/op             896 B/op          1 allocs/op
Benchmark_LinearSearch_10000_1000-12                        7874            143806 ns/op            8192 B/op          1 allocs/op
Benchmark_BinarySearch_10000_1000-12                        2115            530711 ns/op            8256 B/op          3 allocs/op
Benchmark_binarySearch_10000_1000-12                       16612             71856 ns/op            8192 B/op          1 allocs/op
Benchmark_MapSearch____10000_1000-12                        2916            381503 ns/op          330458 B/op         13 allocs/op
Benchmark_mapSearch____10000_1000-12                       81838             14044 ns/op            8192 B/op          1 allocs/op
Benchmark_LinearSearch_10000_10000-12                         86          13614430 ns/op           81920 B/op          1 allocs/op
Benchmark_BinarySearch_10000_10000-12                        945           1193840 ns/op           81984 B/op          3 allocs/op
Benchmark_binarySearch_10000_10000-12                       1503            790767 ns/op           81920 B/op          1 allocs/op
Benchmark_MapSearch____10000_10000-12                       1629            672223 ns/op          404231 B/op         13 allocs/op
Benchmark_mapSearch____10000_10000-12                       3331            309937 ns/op           81920 B/op          1 allocs/op
Benchmark_LinearSearch_50000_10000-12                         62          17120934 ns/op           81920 B/op          1 allocs/op
Benchmark_BinarySearch_50000_10000-12                        337           3661868 ns/op           81984 B/op          3 allocs/op
Benchmark_binarySearch_50000_10000-12                       1270           1031030 ns/op           81920 B/op          1 allocs/op
Benchmark_MapSearch____50000_10000-12                        374           2872006 ns/op         1495159 B/op        848 allocs/op
Benchmark_mapSearch____50000_10000-12                       4281            263885 ns/op           81920 B/op          1 allocs/op
PASS
ok      github.com/fujiwaram/slice-vs-map       122.127s

結果(表)

分かりにくいので表形式にしてまとめました。
赤字の部分が結果が優れているものです。 列の数字は、検索対象のデータ件数検索データ件数を示しています。
例えば、「1000-3」は「1000件の検索対象のデータから3件を検索する」パターンです。
緑の行の「Binary」はソート処理を省いたもの、「Map」はスライスからMapへの変換処理を省いたものです。参考までに計測してます。 f:id:fujiwam:20201205220931p:plain

考察

検索データ件数が100件のときは線形探索のほうが速いですが、1000件くらいからmapの検索のほうが速くなってます。
線形探索の方はデータが増えると比例して時間もかかる様になるみたいなので、だいたいこの辺りが閾値ということなのでしょう。

速度の面で見るとmapのほうが良さそうではありますが、メモリの方を見るとmapはデータ量が増えると比例してメモリの消費が増えています。 パフォーマンスを考える上でメモリの消費も気にする必要はありますので、ここはバランスを見て扱う必要がありそうです。

二分探索の方はスピード面では一位にはなれていないですが、メモリの消費量がほとんどないのにも関わらず、ある程度のスピードが出ています。
バランス考えて、二分探索を選択するというのもアリのような気がしてきました。
今回の調査の一番の収穫🌾

今回使用したコードはGitHubに上げています。

github.com

まとめ

ということで、「sliceとmapどっちが速いのだね?」の問いに対する答えとしては、
「データ量を考慮しつつ、最適なものを使いしょう」という月並みな結論になりそうです。

HRBrainではこのようなことを一緒に考えてくれる仲間を募集しています。
「そんなの知ってたぜ」というそこのあなた、ぜひご応募ください!