Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Go Conference 2019 Autumn Go で超高速な 経路探索エンジンをつくる/Go Conference 2019 Autumn go-ch

avvmoto
October 28, 2019

Go Conference 2019 Autumn Go で超高速な 経路探索エンジンをつくる/Go Conference 2019 Autumn go-ch

avvmoto

October 28, 2019
Tweet

More Decks by avvmoto

Other Decks in Programming

Transcript

  1. Copyright (C) 2018 DeNA Co.,Ltd. All Rights Reserved.
    1
    Go で超高速な
    経路探索エンジンをつくる
    2019 10/28
    井本 裕
    オートモーティブ事業本部
    DeNA Co., Ltd.

    View Slide

  2. 2
    宣伝:DeNA.go 次回は11月1日!
    フレームワーク事例報告多め
    Beego 事例、Goa 事例 !!

    View Slide

  3. 3
    宣伝: 技術書典 7 DeNA Kubernetesわいわい会の本

    View Slide

  4. GoConference’19
    タクシー配車サービス MOV
    4

    View Slide

  5. GoConference’19
    5

    View Slide

  6. GoConference’19
    6
    - スマホアプリでタクシーに配車依頼
    - 電話をかけなくてOK
    - 全車輌ネット決済対応
    - 事前のクレカ登録で、車内での支払い無し
    - 対応エリア現在拡大中
    - 東京、神奈川、大阪、京都

    View Slide

  7. 7
    本日のお題:経路探索
    カーナビは運転ルートをどうしてあれほど高速に計算できるか、
    気になったことはありませんか?
    1. 経路探索とは
    2. アルゴリズム紹介
    3. 実装の高速化

    View Slide

  8. 経路探索とは
    8
    1. 経路探索とは
    2. アルゴリズム紹介
    3. 実装の高速化

    View Slide

  9. © DeNA Co., Ltd.
    最短経路問題とは
    地図上の1点から1点に移動する際の最小のコストとその経路を見つける
    コスト = 時間、距離など
    地図データ(c) Google 2019

    View Slide

  10. © DeNA Co., Ltd.
    経路探索の使いみち
    1. アプリ上で、到着予測時間を出す
    2. 交通シミュレーター
    ⁃ サービス改善用に、シミュレーターで実験
    ⁃ 膨大な回数、経路探索計算を行う
    10
    注:開発中のダミー画面です
    実際と異なる場合がございます

    View Slide

  11. 経路探索のアルゴリズム
    11
    1. 経路探索とは
    2. 経路探索のアルゴリズム
    3. 実装の高速化

    View Slide

  12. 経路探索のアルゴリズム
    1. 地図のデータ化
    2. ダイクストラ
    3. ダイクストラを高速化
    12

    View Slide

  13. © DeNA Co., Ltd.
    地図のデータ化
    - 最短経路を求めるにあたって、道路の形状は問題とならない
    - 必要なもの
    - どこと、どこが繋がっているか?
    - 繋がっている場所の、移動コストは?
    - 道路をグラフ(データ構造)として表現し問題を解く
    13
    地図データ(c) OpenStreetMap

    View Slide

  14. © DeNA Co., Ltd.
    グラフ
    - グラフの構成要素
    - Node
    - 頂点のこと
    - Edge
    - 辺のこと。Node と Node をつなぐ。
    - 重み、向きの有無がある
    - 今回は重み付き有向グラフとして道路を表現
    14
    Node
    Edge
    一方通行

    View Slide

  15. 経路探索のアルゴリズム
    1. 地図のデータ化
    2. ダイクストラ
    3. ダイクストラを高速化
    15

    View Slide

  16. © DeNA Co., Ltd.
    ダイクストラ法
    - 最短経路問題を解くアルゴリズム
    - 出発となるノードから、近い順に全方向に向かって最短経路を列挙していく
    - 後述する経路探索の手法の基礎となる
    問題
    起点Aから全ノードの、最短経路とコストを調べる。
    アルゴリズム
    ノードの集合を2つ用意しておく。
    - 確定ノード: 最短経路と、そのコストが確定したノード。
    - 未確定ノード: 最短経路と、そのコストが未確定。暫定の値が入っている
    16
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1

    View Slide

  17. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP1:
    起点ノードAからAまでの暫定コストは0として、
    ノードAを未確定ノードとする。
    起点からその他のノードへの暫定コストは∞とする。
    確定ノード: []
    未確定ノード: [A]
    17
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤 : 確定ノードや、確定コスト
    黒 : 未確定ノード、暫定コスト
    0




    View Slide

  18. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP2:
    未確定ノードから、一番コストの小さいノードを探す。
    これを確定ノードとする。
    (今回の場合はノードA)
    確定ノード: [A]
    未確定ノード: [...]
    18
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    0




    A

    View Slide

  19. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP3:
    前ステップで確定ノードになったノードから
    (今回の場合ノードA)、
    繋がっているノードを未確定ノードに入れる。
    (ノードB, C, D)
    また、未確定ノードの暫定コストを更新する。
    確定ノード: [A]
    未確定ノード: [B, C, D]
    19
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    ∞→1



    4
    ∞→2

    View Slide

  20. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP4:
    全てのノードが確定ノードになっていなければ、STEP2 に戻る。
    つまり、未確定ノードから、一番コストの小さいノードを探す。
    これを確定ノードとする。
    (今回の場合はノードC)
    このとき、ノードCへは、 A→Cとたどるのが最小経路と言える。
    確定ノード: [A, C]
    未確定ノード: [B, D]
    20
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1

    4
    2
    C

    View Slide

  21. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP5:
    STEP2, 3 を繰り返していく。
    前ステップで確定ノードになったノードから
    (今回の場合ノードC)、
    繋がっているノードを未確定ノードに入れる。
    (ノードD, E)
    Dについては、Aから行くよりも、Cから行くほうがコストが安いので、
    暫定コストを更新する。Eも暫定コスト更新する。
    確定ノード: [A, C]
    未確定ノード: [B, D, E]
    21
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1


    5
    4

    3
    2

    View Slide

  22. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP6:
    STEP2, 3 を繰り返していく。
    未確定ノードのうち、一番暫定コスト安いBを
    確定ノードにする。
    確定ノード: [A, C, B]
    未確定ノード: [D, E]
    22
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    5
    3
    2
    B

    View Slide

  23. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP7:
    STEP2, 3 を繰り返していく。
    前ステップで確定ノードになったノードから
    (今回の場合ノードB)、
    繋がっているノードの、暫定コストを更新する。
    B→Dと行くのはコスト5で、既存の暫定コストよりも悪化するので
    更新しない。
    確定ノード: [A, C, B]
    未確定ノード: [D, E]
    23
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    5
    3
    2

    View Slide

  24. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP8:
    STEP2, 3 を繰り返していく。
    未確定ノードのうち、一番暫定コスト安いDを
    確定ノードにする。
    確定ノード: [A, C, B, D]
    未確定ノード: [E]
    24
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    5
    3
    2
    D

    View Slide

  25. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP8:
    STEP2, 3 を繰り返していく。
    Dを確定ノードにしたので、隣接ノードEの暫定コストを更新する。
    D→Eと行くとコスト4であり、既存の暫定コストよりも小さい。
    そのためEの暫定コストを更新する。
    確定ノード: [A, C, B, D]
    未確定ノード: [E]
    25
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    5→4
    3
    2

    View Slide

  26. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP9:
    STEP2, 3 を繰り返していく。
    未確定ノードのうち、一番暫定コスト安いDを
    確定ノードにする。
    確定ノード: [A, C, B, D, E]
    未確定ノード: []
    26
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    4
    3
    2
    E

    View Slide

  27. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP10:
    STEP2, 3 を繰り返していく。
    未確定ノードのうち、一番暫定コスト安いEを
    確定ノードにする。
    確定ノード: [A, C, B, D, E]
    未確定ノード: []
    27
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    4
    3
    2
    E

    View Slide

  28. © DeNA Co., Ltd.
    ダイクストラ法
    問題:起点Aから全ノードの、最短経路とコストを求める。
    STEP11:
    全てのノードのコストと最短経路が確定したので、終了!
    確定ノード: [A, C, B, D, E]
    未確定ノード: []
    28
    A
    B
    C
    D
    E
    1
    2
    4
    3
    4
    2
    1
    赤:確定ノードや、確定コスト
    黒:未確定ノード、暫定コスト
    緑:今回更新した、暫定コスト
    0
    1
    4
    3
    2

    View Slide

  29. © DeNA Co., Ltd.
    ダイクストラ法
    まとめ
    1. 起点の最小距離を0、ほかのノードの値を未定義(∞)に設定
    2. 未確定ノードのうち、最小値のコストのノードを見つけ、確定ノードとする。
    3. 2で確定ノードとなったノードjから伸びているノードを、未確定ノードに入れる。もし必
    要があれば、暫定的なコストを更新する。
    4. 全てのノードが確定ノードになっていなければ、 STEP2に戻る
    29

    View Slide

  30. © DeNA Co., Ltd.
    ダイクストラ法の課題は計算速度
    - 地図のノード数の規模感
    数十万(タクシーの1交通圏)〜数百万(東京神奈川全体)〜 1.5千万(日本全体)
    - Dijkstra の計算量は O(N^2) であり、数百万件を1-10msでは捌けない
    go + gonum, i5 3.1GHz, 横浜市西区中区を中心としたエリアの道路グラフをサンプリングしてグラフを作成。ランダム 100経路の平均値
    30

    View Slide

  31. © DeNA Co., Ltd.
    ダイクストラ法の改善
    - 探索空間を限定する
    - Bidirectional Dijkstra’s Algorithm
    - 事前計算しておく
    - Contraction Hierarchy
    31

    View Slide

  32. © DeNA Co., Ltd.
    Bidirectional Dijkstra’s Algorithm
    ■ 開始点と到達点の両端からダイクストラで交互に探索を行う
    ■ もう片方で探索済みのノードにあたったら終了
    ⁃ それが最短経路であるためにはもう少し条件がある
    ■ 探索範囲が劇的に減るので速い
    source:
    http://www.cs.princeton.edu/courses/archive/spr06/cos423/Handouts/EPP%20shortest%20path%20algorithms.pdf
    Dijkstra での探索範囲 Bidirectional Dijkstra での探索範

    View Slide

  33. © DeNA Co., Ltd.
    事前計算しておく手法
    - 事前計算しておくことで、二点間の最短経路検索(以後 Query と呼ぶ)を高速に行う手
    法が多数提案されている
    - それぞれ、事前計算のコストやQueryのコストの傾向が異なる。
    33
    Bast, Hannah, et al. "Route planning in transportation networks." Algorithm engineering. Springer, Cham, 2016. 19-80.

    View Slide

  34. © DeNA Co., Ltd.
    Contraction Hierarchy
    - 今回我々が実装したのはContraction Hierarchy
    - OSRM(Open Source Routing Machine) でも採用
    - 事前計算の時間、Query に要する時間がバランスが取れていると判断
    - 事前計算が短いと、渋滞の反映がやりやすくなる
    34
    source: https://movingai.com/IJCAI18-HS/ijcai-hs-harabor.pdf

    View Slide

  35. © DeNA Co., Ltd.
    Contraction Hierarchy
    - 原論文: Geisberger, Robert, et al. "Contraction Hierarchies: Faster and Simpler
    Hierarchical Routing in Road Networks."
    - ざっくり説明
    - Bidirectional Dijkstraベース
    - 隣り合っていないノード間の最短経路をショートカットとして、事前にエッジとして
    追加しておく
    - 経路検索時には、ショートカットを利用することで、ターゲットにより早く着くように
    なる
    35
    source: https://movingai.com/IJCAI18-HS/ijcai-hs-harabor.pdf

    View Slide

  36. © DeNA Co., Ltd.
    Contraction Hierarchy を実装する
    ダイクストラと比べ、アルゴリズム的に様々な工夫がされているが、
    実装上の工夫として求められることは、ほぼダイクストラと同様であった。
    このあと本セッションでは、Go でダイクストラを高速に実装にする話をしていきます。
    36

    View Slide

  37. 実装の高速化
    37
    1. 経路探索とは
    2. アルゴリズム紹介
    3. 実装の高速化

    View Slide

  38. 実装の高速化 構成
    1. slice篇
    2. map篇
    38

    View Slide

  39. ダイクストラのこの部分の高速化をします
    1. 起点の最小距離を0、ほかのノードの値を未定義(∞)に設定
    2. 未確定ノードのうち、最小値のコストのノードを見つけ、確定ノードとする。
    3. 2で確定ノードとなったノードjから伸びているノードを、未確定ノードに入れる。もし必
    要があれば、暫定的なコストを更新する。
    4. 全てのノードが確定ノードになっていなければ、 STEP2に戻る
    39
    Priority Queue

    View Slide

  40. Priority Queue
    優先度付きキュー(ゆうせんどつき -、英: priority queue)は、以下の4つの操作をサポートする抽象
    データ型である。
    ● キューに対して要素を優先度付きで追加する。
    ● 最も高い優先度を持つ要素をキューから取り除き、それを返す。
    ● (オプション) 最も高い優先度を持つ要素を取り除くことなく参照する。
    ● (オプション) 指定した要素を取り除くことなく優先度を変更する
    source: 優先度付きキュー - Wikipedia
    40
    queue := NewMinPQ(0)
    queue.Push("プリン", 100)
    queue.Push("うまい棒", 10)
    queue.Push("ケーキ", 3000)
    fmt.Print(queue.Pop())
    fmt.Print(queue.Pop())
    fmt.Print(queue.Pop())
    // Output:
    // うまい棒: 10
    // プリン: 100
    // ケーキ: 3000
    優先度付き Push
    安い順に Pop

    View Slide

  41. Priority Queue の実装方法
    - Priority Queueをgoで実装するなら、標準ライブラリーのcontainer/heapを
    使うのが恐らく最も簡単
    - godoc の Exampleに Priority Queue の実装が載っている
    - https://godoc.org/container/heap#example-package--PriorityQueue
    41

    View Slide

  42. container/heap 紹介
    42
    利用手順
    - heap.Interface を満たす実装を用意
    これはアイテムを保存するもの
    - heap.Push(), heap.Pop()経由でアイテムを
    追加・削除することで、2分木としてメンテナ
    ンスされる
    type Interface interface {
    Push(x interface{}) // add x as element Len()
    Pop() interface{} // remove and return element Len() - 1.
    Len() int // sort.Interface
    Less(i, j int) bool // sort.Interface
    Swap(i, j int) // sort.Interface
    }
    source: https://golang.org/pkg/container/heap/

    View Slide

  43. heap.Interface 実装例
    43
    type Item struct {
    value string
    priority int
    }
    type PriorityQueue []*Item
    func (pq PriorityQueue) Len() int { return len(pq) }
    func (pq PriorityQueue) Less(i, j int) bool { return pq[i].priority > pq[j].priority }
    func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i]}
    func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    *pq = append(*pq, item)
    }
    func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil // avoid memory leak
    *pq = old[0 : n-1]
    return item
    }
    container/heap の Example から最小構成要素を抜き出し
    heap.Interface を実装
    source:
    https://golang.org/pkg/container/heap/#exam
    ple__priorityQueue

    View Slide

  44. container/heap 利用例
    44
    type PriorityQueue []*Item
    func main() {
    pq := PriorityQueue{}
    heap.Init(&pq)
    item := &Item{
    value: "orange",
    priority: 1,
    }
    heap.Push(&pq, item)
    heap.Pop(&pq)
    }
    source:
    https://golang.org/pkg/container/heap/#exam
    ple__priorityQueue

    View Slide

  45. Priority Queue の実装を高速化しよう
    - Push のたびに Item が allocate されている
    - ダイクストラのように、Push の回数が極めて多いと問題となる
    - ダイクストラ自体は、ほとんど巨大なfor文が回る処理
    - これの実装を改良して、より処理速度が早くなるようにしてみましょう
    45
    type PriorityQueue []*Item
    func main() {
    pq := PriorityQueue{}
    heap.Init(&pq)
    item := &Item{
    value: "orange",
    priority: 1,
    }
    heap.Push(&pq, item)
    }
    source:
    https://golang.org/pkg/container/heap/#exam
    ple__priorityQueue

    View Slide

  46. Slice の仕組みのおさらい
    - まずは前提知識の整理
    - ゴール
    - sliceとarrayの関係
    - slice の lengthとcapacityについて
    - appendの処理内容
    (The Go Blog の Go Slices: usage and internalsが詳しい)
    46

    View Slide

  47. sliceとarrayについて
    - array
    - 固定長
    - slice
    - 大きさを後から変更可能
    - sliceは内部に、ベースとなる arrayへのポインタを持っている
    - このarrayは基底配列と呼ばれる
    47
    var a [4]int
    var s []int

    View Slide

  48. sliceとarrayについて
    slice の構成要素
    1. 基底配列 (array) へのポインター
    2. length
    ⁃ 実際にsliceの要素が何個存在するか
    3. capacity
    ⁃ 基底配列の容量
    48
    (source: Go Slices: usage and internals )


    View Slide

  49. sliceとarrayについて
    例. make([]byte, 5)
    49
    (source: Go Slices: usage and internals )

    View Slide

  50. sliceとarrayについて:まとめ
    ややこしいので整理
    length
    実際にユーザーが何個要素をsliceに詰めたか
    capacity
    sliceが内部的に持っている基底配列の長さ
    (基底配列を変更しないかぎり) sliceのlengthの最大値はcapacityとなる
    capacity(容量、最大収納量)という名前も、こう考えるとわかりやすいかも?
    50

    View Slide

  51. sliceの拡張
    slice への要素追加は append で可能
    基底配列に余裕があれば、特に特別な処理なく、要素を追加可能
    ただcapacityを超えてappendしようとしたらどうなる?
    → slice の拡張
    51
    var s []int
    s = append(s, 10)

    View Slide

  52. slice の拡張
    sliceの拡張の流れ
    1. 現在の基底配列の2倍の長さのarrayをアロケートする
    2. 現在の基底配列の内容を、新しいarrayにコピーする
    3. sliceの基底配列を新しいarrayに切り替える
    倍々と大きくしていくことで、このsliceの拡張の処理が度々発生する事態を
    防いでいる。
    52

    View Slide

  53. slice の拡張
    appendの模擬コード
    53
    func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
    // allocate double what's needed, for future growth.
    newSlice := make([]byte, (n+1)*2)
    copy(newSlice, slice)
    slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
    }
    (source: Go Slices: usage and internals )

    View Slide

  54. slice の拡張
    appendの模擬コード
    54
    func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
    // allocate double what's needed, for future growth.
    newSlice := make([]byte, (n+1)*2)
    copy(newSlice, slice)
    slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
    }
    (source: Go Slices: usage and internals )
    slice 拡張しないのなら、copyするだけ

    View Slide

  55. slice の拡張
    appendの模擬コード
    55
    func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
    // allocate double what's needed, for future growth.
    newSlice := make([]byte, (n+1)*2)
    copy(newSlice, slice)
    slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
    }
    (source: Go Slices: usage and internals )
    slice 拡張しないのなら、copyするだけ
    slice 拡張が必要なケース

    View Slide

  56. sliceの拡張
    - 実際のsliceの拡張の処理はruntime の growslice() で確認できる。
    https://github.com/golang/go/blob/db16de920370892b0241d3fa0617dddff2417a4d/src/runtime/slice.go#L188
    56

    View Slide

  57. sliceの拡張
    - 実験
    - sliceをcapacityを指定せず作成
    - 要素を一つずつ、20回append
    - arrayの長さが1,2,4,8,16,32となるsliceの拡張が発生する
    - 無駄に発生したコスト
    - 複数回のallocateのコスト、そしてメモリコピーのコスト
    - 長さが20のarrayで十分なはずが、32の長さで確保されている
    57
    var s []int
    for i:=0; i<20; i++ {
    s = append(s, 10)
    fmt.Println(cap(s))
    }
    1
    2
    4
    4
    8
    8
    8
    8
    16
    16
    16
    16
    16
    16
    16
    16
    32
    32
    32
    32

    View Slide

  58. Slice の仕組み
    - ゴール
    - sliceとarrayの関係
    - slice の lengthとcapacityについて
    - appendの処理内容
    以上を踏まえた上で、sliceのベストプラクティスを見ていきましょう
    58

    View Slide

  59. sliceのベストプラクティス1
    length や capacity を指定して slice を作成する
    - 事前に必要な長さの目安を capacityに明示しよう
    - appendを使えば、それ以上に要素を追加してもOK
    59
    s1 := make([]int, length) // capacity = length
    s2 := make([]int, length, capacity)

    View Slide

  60. sliceのベストプラクティス2
    メモリアロケートの回数は少なくしよう
    構造体のpointerへのsliceだと、pointerしかallocateされていない。
    構造体のsliceとして、一回でまるっとallocateしておこう
    60
    s1 := make([]*BigStruct, 0, 1e5)
    s1 := make([]*BigStruct, 0, 1e5)
    for i:=0; i<1e5; i++ {
    s1 = append(s1, &BigStruct{})
    }
    s2 := make([]BigStruct, 0, 1e5)
    構造体は、都度allocateされている

    View Slide

  61. sliceのベストプラクティス3
    length を延長し allocate 済みの領域を使おう
    例:構造体のsliceに要素を一つ追加する
    allocate済みの領域が s の中にあるので、有効利用したい
    61
    s := make([]BigStruct, 0, c)
    for i:=0; is = append(s, NewBigStruct(i))
    }
    構造体は、都度allocateされている

    View Slide

  62. sliceのベストプラクティス3
    length を延長し allocate 済みの領域を使おう
    例:構造体のsliceに要素を一つ追加する
    62
    for i:=0; iif len(s)+1 < cap(s) {
    s=s[:len(s)+1]
    InitBigStruct(&s[len(s)])
    }
    }
    s[0] ... s[len(s)-1] s[len(s)] ... s[cap(s)-1]
    s
    s[0:len(s)]
    すでに要素が入っていて、利
    用済みの領域
    s[len(s):cap(s)]
    アロケート済みだが、
    まだ利用されていない領域
    s[:len(s)+1]
    slice の length を延長

    View Slide

  63. sliceのベストプラクティス3
    length を延長し allocate 済みの領域を使おう
    例:構造体のsliceに要素を一つ追加する
    63
    for i:=0; iif len(s)+1 < cap(s) {
    s=s[:len(s)+1] // slice の延長
    InitBigStruct(&s[len(s)])
    } else if cap(s) < len(s)+1 {
    s = append(s, NewBigStruct())
    }
    }
    capacityが足りなくなったら、appendで
    基底配列を拡張

    View Slide

  64. sliceのベストプラクティス まとめ
    1. length や capacity を指定して slice を作成する
    2. メモリアロケートの回数は少なくしよう
    3. length を延長し allocate 済みの領域を使う
    64

    View Slide

  65. Priority Queue の実装を高速化しよう(再掲)
    65
    type PriorityQueue []*Item
    func main() {
    pq := PriorityQueue{}
    heap.Init(&pq)
    item := &Item{
    value: "orange",
    priority: 1,
    }
    heap.Push(&pq, item)
    }

    View Slide

  66. Priority Queue の実装を高速化しよう
    66
    type PriorityQueue []*Item
    func main() {
    pq := make(PriorityQueue, 0, N)
    heap.Init(&pq)
    item := &Item{
    value: "orange",
    priority: 1,
    }
    heap.Push(&pq, item)
    }
    1. length や capacity を指定して
    slice を作成する

    View Slide

  67. Priority Queue の実装を高速化しよう
    67
    type PriorityQueue []Item
    func main() {
    pq := make(PriorityQueue, 0, N)
    heap.Init(&pq)
    pq.PushFast("orange", 1)
    }
    2. メモリアロケートの回数は少なく
    しよう
    (Item はまとめてallocate)

    View Slide

  68. Priority Queue の実装を高速化しよう
    68
    func (q *PriorityQueue) PushFast(value string,
    priority float64) {
    l := len(q)
    if l < cap(q) {
    q = q[:l+1]
    q[l].value = value
    q[l].priority = priority
    } else {
    q = append(q, Item{
    value: value,
    priority: priority,
    })
    }
    heap.Push(p.p, &q[l])
    }
    3. length を延長し allocate 済みの
    領域を使う

    View Slide

  69. Priority Queue の実装を高速化しよう
    PQのPush()をtestingパッケージでベンチマーク
    allocateがなくなった!
    速度も2倍高速化!
    69
    $ go test -bench . -benchmem
    goos: darwin
    goarch: amd64
    BenchmarkTheirs-8 30000000 49.8 ns/op 16 B/op 1 allocs/op
    BenchmarkOurs-8 100000000 20.4 ns/op 0 B/op 0 allocs/op
    PASS
    ok
    const benchmarkSize = 10000
    func BenchmarkTheirs(b *testing.B) {
    b.StopTimer()
    for i := 0; i < b.N; {
    p := NewMinPQ(benchmarkSize)
    b.StartTimer()
    for j := 0; j < benchmarkSize; j++ {
    p.Push(0, 0)
    i++
    if i >= b.N {
    return
    }
    }
    b.StopTimer()
    }
    }

    View Slide

  70. 実装の高速化 構成
    1. slice篇
    2. map篇
    70

    View Slide

  71. ダイクストラのこの部分の高速化をします
    1. 起点の最小距離を0、ほかのノードの値を未定義(∞)に設定
    2. 未確定ノードのうち、最小値のコストのノードを見つけ、確定ノードとする。
    3. 2で確定ノードとなったノードjから伸びているノードを、未確定ノードに入れ
    る。もし必要があれば、暫定的なコストを更新する。
    4. 全てのノードが確定ノードになっていなければ、 STEP2に戻る
    71
    Priority Queue
    グラフライブラリー

    View Slide

  72. グラフライブラリーとは?
    - グラフ(データ構造)を扱うライブラリー
    - ダイクストラをするのに必要なもの
    - エッジの追加
    - あるノードの、近隣ノードの取得
    72
    0
    1
    3
    2
    1
    2
    4
    var g Graph
    g.SetEdge(0, 1, 2.0)
    g.SetEdge(0, 2, 4.0)
    g.SetEdge(0, 3, 1.0)
    fmt.Println(g.From(0))
    // Output:
    // [{1, 2.0}, {2, 4.0}, {3, 1.0}]

    View Slide

  73. type SimpleGraph struct {
    from map[int64]map[int64]float64
    }
    func (g *SimpleGraph) SetEdge(from, to int64, weight float64) {
    if _, ok := g.from[from]; !ok {
    g.from[from] = map[int64]float64{}
    }
    g.from[from][to] = weight
    }
    グラフライブラリーの単純な実装
    73
    from
    To
    weight
    Edge を map の map として表現

    View Slide

  74. type SimpleGraph struct {
    from map[int64]map[int64]float64
    }
    func (g *SimpleGraph) SetEdge(from, to int64, weight float64) {
    if _, ok := g.from[from]; !ok {
    g.from[from] = map[int64]float64{}
    }
    g.from[from][to] = weight
    }
    グラフライブラリーの単純な実装
    74
    from
    To
    weight
    Edge を map の map として表現

    View Slide

  75. type SimpleGraph struct {
    from map[int64]map[int64]float64
    }
    func (g *SimpleGraph) From(id int64) (nodes []int64, costs []float64) {
    for node, cost := range g.from[id] {
    nodes = append(nodes, node)
    costs = append(costs, cost)
    }
    return
    }
    グラフライブラリーの実装例
    75
    from
    To
    weight
    近隣 Node の取得

    View Slide

  76. グラフライブラリーの課題
    - このようなグラフライブラリーを使うと、ダイクストラの経路探索の無視で
    きない割合が、mapアクセスに
    - (ノード数約1000万、エッジ数約3000万)
    - go tool pprofでベンチマークが取れる
    go の map はどのようなものだろうか?
    76
    v := m["key"] → runtime.mapaccess1(m, "key", &v)
    v, ok := m["key"] → runtime.mapaccess2(m, "key", &v, &ok)

    View Slide

  77. go の map の仕組み
    - go の map はただの ハッシュテーブル cf.https://github.com/golang/go/blob/master/src/runtime/map.go#L9
    - ハッシュテーブルとは
    - ハッシュテーブルはキーをもとに生成されたハッシュ値を添え字とした配列である。通常、配列の添え字には非負整数し
    か扱えない。そこで、キーを要約する値であるハッシュ値を添え字として値を管理することで、検索や追加を要素数によら
    ず定数時間O(1)で実現する。
    77
    source: ハッシュテーブル - Wikipedia

    View Slide

  78. go の map の仕組み
    謝辞:この解説は Macro View of Map Internals In Go に大きくよります。2013年の記事ですが、現在のruntimeのコード
    と比較しても、大きな違いはないようです。
    - goのmapは内部的に「Bucket」の配列となる
    - 1つのBucket には、8つのキー/値ペアを保存
    - Bucketの個数は常に2の累乗
    - マップのキーはハッシュ値に変換され、ハッシュ値の LOB(low order bits; 下位ビット) がBucketの選択に用いられる
    78
    keys
    “orange” ハッシュ値
    ハッシュ関数
    Bucket
    Bucket
    Bucket
    ...
    LOB
    key: value 8個分

    View Slide

  79. go の map の仕組み
    - Bucket の構成
    - ハッシュ値のHOB (top 8 high order bits)の配列
    - key/valueのペアを格納するバイトの配列
    - LOBが衝突して、同じBucketに9個以上入れる場合は?
    - Overflow Bucket が作成され、各Bucket から参照される
    79
    source:Macro View of Map Internals In Go
    https://www.ardanlabs.com/blog/2013/12/macro-view-of-map-internals-in-go.html

    View Slide

  80. go の map の仕組み
    - map に key/value の追加を繰り返すと、Overflow Bucketが増えてゆき、
    性能が下がる
    - スレッショルドを超えると map の拡張が走る
    - 既存 Bucket の2倍の Bucket の配列が allocate される
    - key/value が追加または削除のタイミングで、古い Bucket配列から新しい Bucket
    配列へ「Evacuate(退避)」してゆく
    80

    View Slide

  81. go の map の仕組み
    - 事前にmapの目安がわかるなら、”capacity hint ” を指定してmap を作る
    ことで、 map の拡張が最小限に押さえられる
    - slice 同様、初期の capacity を超えて要素を追加することができる
    - benchmark とってみると、capacity を指定したほうが、key/valueの追加は
    早い
    81
    make(map[string]int)
    make(map[string]int, 100)
    cf. https://golang.org/ref/spec#Map_types
    goos: darwin
    goarch: amd64
    BenchmarkMapAssignNoCap-8 10000000 176 ns/op 87 B/op 0 allocs/op
    BenchmarkMapAssignWithCap-8 20000000 107 ns/op 0 B/op 0 allocs/op

    View Slide

  82. go の map の仕組み まとめ
    - go の map はハッシュテーブル
    - slice 同様、内部的に capacity や、mapの拡張がある
    - 事前に要素数の目安がわかるなら、”capacity hint ” を指定してmap を作
    ることで、 map の拡張が最小限に押さえられる
    82
    make(map[string]int)
    make(map[string]int, 100)

    View Slide

  83. グラフライブラリーの高速化
    - キーアイディア:map access はやめて代わりに slice access にしよう
    - node ID は飛び飛びの値でありえる
    → node ID を全部sliceにつめる
    → node ID の代わりに、slice で何番目かのindexを使う
    - 繋がっている所を辿っていくダイクストラの用途だと、これで充分だった
    83
    type SimpleGraph struct {
    fromCost [][]float64
    fromIndex [][]int
    nodes []int64
    indexOf map[int64]int
    }
    func (g *SimpleGraph) From(index int) (nodeIndex []int,
    costs []float64) {
    return g.fromIndex[index], g.fromCost[index]
    }
    注意:模式的に簡略化しています

    View Slide

  84. グラフライブラリーの高速化の結果
    東京都+神奈川県の一部(2次メッシュ18個分)でランダムに50✕50のCost Matrix を算出
    GCE c2-standard-30(30 基の vCPU と 120 GB のメモリ)
    Contraction hierarchiesを実装
    - before(map版) : 800msec
    - after(slice版) : 80msec
    84

    View Slide

  85. まとめ
    - 経路探索と、代表的アルゴリズムを紹介
    - ダイクストラ法をgoで実装時に役立つ知識を紹介
    - sliceのしくみ
    - mapのしくみ
    - slice を作るときは、capacity, length を指定しよう
    - map を作るときは、capacity hintを指定しよう
    85

    View Slide

  86. おことわり
    - 経路探索の高速化のためにはアルゴリズム上の工夫が大変重要です。
    - 本発表では実装上の工夫に触れました。アルゴリズム上の工夫は多数
    残されています。
    - 高速化は、実際にどこが遅いか計測しながら行いましょう。
    86

    View Slide

  87. 参考文献
    ■ Bast, Hannah, et al. "Route planning in transportation networks." Algorithm engineering. Springer, Cham, 2016. 19-80.
    ■ Geisberger, Robert, et al. "Contraction hierarchies: Faster and simpler hierarchical routing in road networks."
    International Workshop on Experimental and Efficient Algorithms. Springer, Berlin, Heidelberg, 2008.
    ■ 最短経路探査アルゴリズムの実装
    ■ An Introduction to Contraction Hierarchies
    ■ The Go Blog Go Slices: usage and internals
    ■ Macro View of Map Internals In Go

    View Slide