Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

GoConference’19 5

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

© 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

Slide 20

Slide 20 text

© 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

Slide 21

Slide 21 text

© 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

Slide 22

Slide 22 text

© 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

Slide 23

Slide 23 text

© 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

Slide 24

Slide 24 text

© 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

Slide 25

Slide 25 text

© 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

Slide 26

Slide 26 text

© 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

Slide 27

Slide 27 text

© 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

Slide 28

Slide 28 text

© 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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

© 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 での探索範 囲

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

© 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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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/

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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


Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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 )

Slide 54

Slide 54 text

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するだけ

Slide 55

Slide 55 text

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 拡張が必要なケース

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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されている

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

sliceのベストプラクティス3 length を延長し allocate 済みの領域を使おう 例:構造体のsliceに要素を一つ追加する 62 for i:=0; i

Slide 63

Slide 63 text

sliceのベストプラクティス3 length を延長し allocate 済みの領域を使おう 例:構造体のsliceに要素を一つ追加する 63 for i:=0; i

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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 を作成する

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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 済みの 領域を使う

Slide 69

Slide 69 text

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() } }

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

グラフライブラリーとは? - グラフ(データ構造)を扱うライブラリー - ダイクストラをするのに必要なもの - エッジの追加 - あるノードの、近隣ノードの取得 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}]

Slide 73

Slide 73 text

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 として表現

Slide 74

Slide 74 text

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 として表現

Slide 75

Slide 75 text

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 の取得

Slide 76

Slide 76 text

グラフライブラリーの課題 - このようなグラフライブラリーを使うと、ダイクストラの経路探索の無視で きない割合が、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)

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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個分

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

グラフライブラリーの高速化 - キーアイディア: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] } 注意:模式的に簡略化しています

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

参考文献 ■ 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