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

F8e7a1a5f90a13a1dd9fb57ce65cab77?s=47 avvmoto
October 28, 2019

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

F8e7a1a5f90a13a1dd9fb57ce65cab77?s=128

avvmoto

October 28, 2019
Tweet

Transcript

  1. Copyright (C) 2018 DeNA Co.,Ltd. All Rights Reserved. 1 Go

    で超高速な 経路探索エンジンをつくる 2019 10/28 井本 裕 オートモーティブ事業本部 DeNA Co., Ltd.
  2. 2 宣伝:DeNA.go 次回は11月1日! フレームワーク事例報告多め Beego 事例、Goa 事例 !!

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

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

  5. GoConference’19 5

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

    - 対応エリア現在拡大中 - 東京、神奈川、大阪、京都
  7. 7 本日のお題:経路探索 カーナビは運転ルートをどうしてあれほど高速に計算できるか、 気になったことはありませんか? 1. 経路探索とは 2. アルゴリズム紹介 3. 実装の高速化

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

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

    Google 2019
  10. © DeNA Co., Ltd. 経路探索の使いみち 1. アプリ上で、到着予測時間を出す 2. 交通シミュレーター ⁃

    サービス改善用に、シミュレーターで実験 ⁃ 膨大な回数、経路探索計算を行う 10 注:開発中のダミー画面です 実際と異なる場合がございます
  11. 経路探索のアルゴリズム 11 1. 経路探索とは 2. 経路探索のアルゴリズム 3. 実装の高速化

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

  13. © DeNA Co., Ltd. 地図のデータ化 - 最短経路を求めるにあたって、道路の形状は問題とならない - 必要なもの -

    どこと、どこが繋がっているか? - 繋がっている場所の、移動コストは? - 道路をグラフ(データ構造)として表現し問題を解く 13 地図データ(c) OpenStreetMap
  14. © DeNA Co., Ltd. グラフ - グラフの構成要素 - Node -

    頂点のこと - Edge - 辺のこと。Node と Node をつなぐ。 - 重み、向きの有無がある - 今回は重み付き有向グラフとして道路を表現 14 Node Edge 一方通行
  15. 経路探索のアルゴリズム 1. 地図のデータ化 2. ダイクストラ 3. ダイクストラを高速化 15

  16. © DeNA Co., Ltd. ダイクストラ法 - 最短経路問題を解くアルゴリズム - 出発となるノードから、近い順に全方向に向かって最短経路を列挙していく -

    後述する経路探索の手法の基礎となる 問題 起点Aから全ノードの、最短経路とコストを調べる。 アルゴリズム ノードの集合を2つ用意しておく。 - 確定ノード: 最短経路と、そのコストが確定したノード。 - 未確定ノード: 最短経路と、そのコストが未確定。暫定の値が入っている 16 A B C D E 1 2 4 3 4 2 1
  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 ∞ ∞ ∞ ∞
  18. © DeNA Co., Ltd. ダイクストラ法 問題:起点Aから全ノードの、最短経路とコストを求める。 STEP2: 未確定ノードから、一番コストの小さいノードを探す。 これを確定ノードとする。 (今回の場合はノードA)

    確定ノード: [A] 未確定ノード: [...] 18 A B C D E 1 2 4 3 4 2 1 赤:確定ノードや、確定コスト 黒:未確定ノード、暫定コスト 0 ∞ ∞ ∞ ∞ A
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  29. © DeNA Co., Ltd. ダイクストラ法 まとめ 1. 起点の最小距離を0、ほかのノードの値を未定義(∞)に設定 2. 未確定ノードのうち、最小値のコストのノードを見つけ、確定ノードとする。

    3. 2で確定ノードとなったノードjから伸びているノードを、未確定ノードに入れる。もし必 要があれば、暫定的なコストを更新する。 4. 全てのノードが確定ノードになっていなければ、 STEP2に戻る 29
  30. © DeNA Co., Ltd. ダイクストラ法の課題は計算速度 - 地図のノード数の規模感 数十万(タクシーの1交通圏)〜数百万(東京神奈川全体)〜 1.5千万(日本全体) -

    Dijkstra の計算量は O(N^2) であり、数百万件を1-10msでは捌けない go + gonum, i5 3.1GHz, 横浜市西区中区を中心としたエリアの道路グラフをサンプリングしてグラフを作成。ランダム 100経路の平均値 30
  31. © DeNA Co., Ltd. ダイクストラ法の改善 - 探索空間を限定する - Bidirectional Dijkstra’s

    Algorithm - 事前計算しておく - Contraction Hierarchy 31
  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 での探索範 囲
  33. © DeNA Co., Ltd. 事前計算しておく手法 - 事前計算しておくことで、二点間の最短経路検索(以後 Query と呼ぶ)を高速に行う手 法が多数提案されている

    - それぞれ、事前計算のコストやQueryのコストの傾向が異なる。 33 Bast, Hannah, et al. "Route planning in transportation networks." Algorithm engineering. Springer, Cham, 2016. 19-80.
  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
  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
  36. © DeNA Co., Ltd. Contraction Hierarchy を実装する ダイクストラと比べ、アルゴリズム的に様々な工夫がされているが、 実装上の工夫として求められることは、ほぼダイクストラと同様であった。 このあと本セッションでは、Go

    でダイクストラを高速に実装にする話をしていきます。 36
  37. 実装の高速化 37 1. 経路探索とは 2. アルゴリズム紹介 3. 実装の高速化

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

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

    STEP2に戻る 39 Priority Queue
  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
  41. Priority Queue の実装方法 - Priority Queueをgoで実装するなら、標準ライブラリーのcontainer/heapを 使うのが恐らく最も簡単 - godoc の

    Exampleに Priority Queue の実装が載っている - https://godoc.org/container/heap#example-package--PriorityQueue 41
  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/
  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
  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
  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
  46. Slice の仕組みのおさらい - まずは前提知識の整理 - ゴール - sliceとarrayの関係 - slice

    の lengthとcapacityについて - appendの処理内容 (The Go Blog の Go Slices: usage and internalsが詳しい) 46
  47. sliceとarrayについて - array - 固定長 - slice - 大きさを後から変更可能 -

    sliceは内部に、ベースとなる arrayへのポインタを持っている - このarrayは基底配列と呼ばれる 47 var a [4]int var s []int
  48. sliceとarrayについて slice の構成要素 1. 基底配列 (array) へのポインター 2. length ⁃

    実際にsliceの要素が何個存在するか 3. capacity ⁃ 基底配列の容量 48 (source: Go Slices: usage and internals )

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

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

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

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

    防いでいる。 52
  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 )
  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するだけ
  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 拡張が必要なケース
  56. sliceの拡張 - 実際のsliceの拡張の処理はruntime の growslice() で確認できる。 https://github.com/golang/go/blob/db16de920370892b0241d3fa0617dddff2417a4d/src/runtime/slice.go#L188 56

  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
  58. Slice の仕組み - ゴール - sliceとarrayの関係 - slice の lengthとcapacityについて

    - appendの処理内容 以上を踏まえた上で、sliceのベストプラクティスを見ていきましょう 58
  59. sliceのベストプラクティス1 length や capacity を指定して slice を作成する - 事前に必要な長さの目安を capacityに明示しよう

    - appendを使えば、それ以上に要素を追加してもOK 59 s1 := make([]int, length) // capacity = length s2 := make([]int, length, capacity)
  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されている
  61. sliceのベストプラクティス3 length を延長し allocate 済みの領域を使おう 例:構造体のsliceに要素を一つ追加する allocate済みの領域が s の中にあるので、有効利用したい 61

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

    i++ { if 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 を延長
  63. sliceのベストプラクティス3 length を延長し allocate 済みの領域を使おう 例:構造体のsliceに要素を一つ追加する 63 for i:=0; i<E;

    i++ { if 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で 基底配列を拡張
  64. sliceのベストプラクティス まとめ 1. length や capacity を指定して slice を作成する 2.

    メモリアロケートの回数は少なくしよう 3. length を延長し allocate 済みの領域を使う 64
  65. Priority Queue の実装を高速化しよう(再掲) 65 type PriorityQueue []*Item func main() {

    pq := PriorityQueue{} heap.Init(&pq) item := &Item{ value: "orange", priority: 1, } heap.Push(&pq, item) }
  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 を作成する
  67. Priority Queue の実装を高速化しよう 67 type PriorityQueue []Item func main() {

    pq := make(PriorityQueue, 0, N) heap.Init(&pq) pq.PushFast("orange", 1) } 2. メモリアロケートの回数は少なく しよう (Item はまとめてallocate)
  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 済みの 領域を使う
  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() } }
  70. 実装の高速化 構成 1. slice篇 2. map篇 70

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

    STEP2に戻る 71 Priority Queue グラフライブラリー
  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}]
  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 として表現
  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 として表現
  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 の取得
  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)
  77. go の map の仕組み - go の map はただの ハッシュテーブル

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

    性能が下がる - スレッショルドを超えると map の拡張が走る - 既存 Bucket の2倍の Bucket の配列が allocate される - key/value が追加または削除のタイミングで、古い Bucket配列から新しい Bucket 配列へ「Evacuate(退避)」してゆく 80
  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
  82. go の map の仕組み まとめ - go の map はハッシュテーブル

    - slice 同様、内部的に capacity や、mapの拡張がある - 事前に要素数の目安がわかるなら、”capacity hint ” を指定してmap を作 ることで、 map の拡張が最小限に押さえられる 82 make(map[string]int) make(map[string]int, 100)
  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] } 注意:模式的に簡略化しています
  84. グラフライブラリーの高速化の結果 東京都+神奈川県の一部(2次メッシュ18個分)でランダムに50✕50のCost Matrix を算出 GCE c2-standard-30(30 基の vCPU と 120

    GB のメモリ) Contraction hierarchiesを実装 - before(map版) : 800msec - after(slice版) : 80msec 84
  85. まとめ - 経路探索と、代表的アルゴリズムを紹介 - ダイクストラ法をgoで実装時に役立つ知識を紹介 - sliceのしくみ - mapのしくみ -

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

  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