優先度付きキュー系モジュールのベンチマーク
『Purely Functional Data Structures』(略:PFDS)で紹介されている優先度付きキュー(+ α)をいくつか実装してみたので、その性能測定メモ。
なおPFDSは(データ構造がヒープ特性を満たしているかどうかに関係なく)優先度付きキュー群をまとめてヒープと呼称しているようなので、以降ではそれに倣うことにする。
測定対象
測定対象モジュールは以下の通り:
- leftist_heaps:
- PFDSの3.1の実装がベース
- ヒープ特性とLeftist特性を満たす二分木
- ヒープ特性: 親ノードの値は、常に子ノードの値以下 (値が小さいほど高優先)
- Leftist特性: 左の子のランクは、常に右の子のランク以上
- ランクは「右の子を辿り続けて空ノードに至るまでのパスの長さ」
- 要素の挿入とヒープ同士のマージは
O(log n)
、最小値取り出しはO(1)
、の最悪コスト
- binomial_heaps:
- splay_heaps:
- PFDSの5.4の実装がベース
- 二分探索木
- 各操作実行時に辿った経路のみをバランス(スプレー)し、よくアクセスされる要素ほど木のルート近くに配置されるようにする
- 挿入/取り出しは
O(log n)
の償却コスト - マージは
O(n)
の最悪コスト
- pairing_heaps:
- gb_set_heaps:
- list_heaps:
- ソート済みリストによるヒープ実装
- 挿入とマージは
O(n)
の最悪コスト - 取り出しは
O(1)
の最悪コスト (単にリストの先頭要素を取り出すだけ)
環境
- CPU: Intel(R) Core(TM) i7-5600U CPU @ 2.60GHz
- OS: Ubuntu-15.04
- Erlang/OTP: 17.5; パッケージ(.deb)
測定内容
測定用コード: heaps_bench.erl
以下の操作に掛かる時間を計測する:
- 要素の挿入:
- ひたすら要素を挿入して、一回辺りの挿入に掛かった平均時間を計測する
- 入力数は
4^n (n = 2..7)
- 入力列のパターンは
昇順、降順、ランダム
の3つ
- 最小要素の取り出し:
- 全入力要素の挿入後に、ひたすら最小要素の取り出しを行い、一回辺りの取り出しに掛かった平均時間を計測する
- 入力数は
4^n (n = 2..7)
- 入力列のパターンは
昇順、降順、ランダム
の3つ
- 挿入と取り出しの混合:
- 二つのヒープのマージ:
- 要素数が
4^n (n = 2..5)
の二つのヒープのマージを千回繰り返し、一回の平均所要時間を計測する - 各ヒープの要素にはランダムな値が使用される
- 要素数が
測定結果
要素の挿入
計測内容(再掲):
- ひたすら要素を挿入して、一回辺りの挿入に掛かった平均時間を計測する
- 入力数は
4^n (n = 2..7)
- 入力列のパターンは
昇順、降順、ランダム
の3つ
入力が昇順の場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_in(lists:seq(1, trunc(math:pow(4, N)))).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.395 μs | 0.229 μs | 0.125 μs | 0.083 μs | 0.854 μs | 0.270 μs |
64 | 0.422 μs | 0.120 μs | 0.089 μs | 0.073 μs | 1.115 μs | 0.656 μs |
256 | 0.533 μs | 0.121 μs | 0.063 μs | 0.056 μs | 1.467 μs | 2.460 μs |
1024 | 0.672 μs | 0.115 μs | 0.061 μs | 0.054 μs | 1.995 μs | 10.372 μs |
4096 | 0.838 μs | 0.147 μs | 0.118 μs | 0.107 μs | 2.823 μs | 41.247 μs |
16384 | 1.050 μs | 0.130 μs | 0.078 μs | 0.067 μs | 2.846 μs | 166.472 μs |
- pairing_heapsが一番高速
- paring_heaps、splay_heaps、binomial_heapsは入力数によらず
O(1)
で挿入可能 - leftist_heapsとgb_set_heapsは
O(log n)
- list_heapsは
O(n)
(常にリストの末尾への追加となる)
入力が降順の場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_in(lists:seq(trunc(math:pow(4, N)), 1, -1)).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.146 μs | 0.146 μs | 0.063 μs | 0.063 μs | 0.688 μs | 0.063 μs |
64 | 0.161 μs | 0.135 μs | 0.089 μs | 0.063 μs | 0.932 μs | 0.031 μs |
256 | 0.111 μs | 0.126 μs | 0.068 μs | 0.057 μs | 1.297 μs | 0.026 μs |
1024 | 0.131 μs | 0.119 μs | 0.068 μs | 0.056 μs | 1.737 μs | 0.029 μs |
4096 | 0.127 μs | 0.119 μs | 0.083 μs | 0.063 μs | 2.493 μs | 0.049 μs |
16384 | 0.147 μs | 0.166 μs | 0.079 μs | 0.072 μs | 2.574 μs | 0.032 μs |
- list_heapsが一番高速 (常にリストの先頭への追加となる)
- ただしpairing_heapsとsplay_heapsの所要時間も(単なる先頭要素の追加に対して)二倍程度に収まっているので悪くはない
- list_heaps、paring_heaps、splay_heaps、binomial_heaps、leftist_heapsは入力数によらず
O(1)
で挿入可能 - gb_set_heapsだけが
O(log n)
入力がランダムの場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_in([random:uniform() || _ <- lists:seq(1, trunc(math:pow(4, N)))]).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.313 μs | 0.125 μs | 0.208 μs | 0.063 μs | 0.604 μs | 0.146 μs |
64 | 0.344 μs | 0.135 μs | 0.344 μs | 0.073 μs | 0.750 μs | 0.401 μs |
256 | 0.367 μs | 0.132 μs | 0.480 μs | 0.053 μs | 0.930 μs | 1.323 μs |
1024 | 0.493 μs | 0.131 μs | 0.727 μs | 0.058 μs | 1.171 μs | 5.988 μs |
4096 | 0.528 μs | 0.150 μs | 1.014 μs | 0.060 μs | 1.811 μs | 24.181 μs |
16384 | 0.724 μs | 0.163 μs | 1.209 μs | 0.078 μs | 2.027 μs | 93.670 μs |
- pairing_heapsが一番高速
- paring_heaps、binomial_heapsは入力数によらず
O(1)
で挿入可能 - leftist_heaps、splay_heaps、gb_set_heapsは
O(log n)
- list_heapsは
O(n)
実測結果のまとめ (挿入)
注意: 以降に出てくる実測値のオーダーは観測値を眺めての印象に基づくものであり、厳密なものではない
オーダー:
入力パターン | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
昇順 | 1 | 1 | 1 | 1 | log n | n |
降順 | log n | 1 | 1 | 1 | log n | 1 |
ランダム | log n | 1 | log n | 1 | log n | n |
入力数=1024(4^5)
の際の平均所要時間(μs):
入力パターン | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
昇順 | 0.672 | 0.115 | 二番:0.061 | 最速:0.054 | 1.995 | 10.372 |
降順 | 0.131 | 0.119 | 0.068 | 二番:0.056 | 1.737 | 最速:0.029 |
ランダム | 0.493 | 二番:0.131 | 0.727 | 最速:0.058 | 1.171 | 5.988 |
pairing_heapsは優秀。
最小要素の取り出し
計測内容(再掲):
- 全入力要素の挿入後に、ひたすら最小要素の取り出しを行い、一回辺りの取り出しに掛かった平均時間を計測する
- 入力数は
4^n (n = 2..7)
- 入力列のパターンは
昇順、降順、ランダム
の3つ
入力が昇順の場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_out(lists:seq(1, trunc(math:pow(4, N)))).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.375 μs | 0.167 μs | 0.125 μs | 0.208 μs | 0.250 μs | 0.042 μs |
64 | 0.453 μs | 0.214 μs | 0.156 μs | 0.167 μs | 0.203 μs | 0.063 μs |
256 | 0.599 μs | 0.230 μs | 0.143 μs | 0.164 μs | 0.241 μs | 0.047 μs |
1024 | 0.842 μs | 0.285 μs | 0.156 μs | 0.179 μs | 0.230 μs | 0.047 μs |
4096 | 1.243 μs | 0.286 μs | 0.170 μs | 0.316 μs | 0.245 μs | 0.043 μs |
16384 | 1.428 μs | 0.322 μs | 0.156 μs | 0.183 μs | 0.268 μs | 0.049 μs |
- list_heapsは先頭要素を取り出すだけなので速い
- binomial_heapsとleftist_heapsは
O(log n)
。後者の方が劣化が激しい。 - gb_set_treesが
O(1)
に見えるのが意外 (微増はしているけど)
入力が降順の場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_out(lists:seq(trunc(math:pow(4, N)), 1, -1)).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.083 μs | 0.188 μs | 0.083 μs | 0.063 μs | 0.292 μs | 0.042 μs |
64 | 0.078 μs | 0.229 μs | 0.083 μs | 0.052 μs | 0.234 μs | 0.063 μs |
256 | 0.063 μs | 0.230 μs | 0.066 μs | 0.057 μs | 0.270 μs | 0.046 μs |
1024 | 0.069 μs | 0.276 μs | 0.073 μs | 0.060 μs | 0.249 μs | 0.044 μs |
4096 | 0.060 μs | 0.297 μs | 0.065 μs | 0.061 μs | 0.275 μs | 0.045 μs |
16384 | 0.068 μs | 0.404 μs | 0.069 μs | 0.059 μs | 0.287 μs | 0.059 μs |
- 一番速いのはやっぱりlist_heaps
- ただし降順の場合は、lefist_heaps/splay_heaps/pairing_heaps、の所要時間がだいぶlist_heapsに近づいている
- binomial_heapsとgb_set_heapsの傾向は、昇順の場合とほぼ同様
入力がランダムの場合
%% [計測関数] %% N = 2..7 > heaps_bench:bench_out([random:uniform() || _ <- lists:seq(1, trunc(math:pow(4, N)))]).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.271 μs | 0.417 μs | 0.208 μs | 0.188 μs | 0.271 μs | 0.042 μs |
64 | 0.427 μs | 0.526 μs | 0.130 μs | 0.313 μs | 0.224 μs | 0.068 μs |
256 | 0.590 μs | 0.779 μs | 0.104 μs | 0.406 μs | 0.236 μs | 0.042 μs |
1024 | 0.807 μs | 1.027 μs | 0.116 μs | 0.578 μs | 0.310 μs | 0.049 μs |
4096 | 1.238 μs | 1.643 μs | 0.110 μs | 0.684 μs | 0.304 μs | 0.056 μs |
16384 | 1.543 μs | 1.637 μs | 0.107 μs | 0.864 μs | 0.326 μs | 0.051 μs |
- lists_heapsが最速なのは変わらず
- lefist_heaps, pairing_heaps, gb_set_heapsが
O(log n)
になり、他の入力順よりも性能が落ちている感がある
実測結果のまとめ (取り出し)
オーダー:
入力パターン | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
昇順 | log n | log n | 1 | 1 | 1 | 1 |
降順 | 1 | log n | 1 | 1 | 1 | 1 |
ランダム | log n | log n | 1 | log n | log n | 1 |
入力数=1024(4^5)
の際の平均所要時間(μs):
入力パターン | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
昇順 | 0.842 | 0.285 | 二番:0.156 | 0.179 | 0.230 | 最速:0.047 |
降順 | 0.069 | 0.276 | 0.073 | 二番:0.060 | 0.249 | 最速:0.044 |
ランダム | 0.807 | 1.027 | 二番:0.116 | 0.578 | 0.310 | 最速:0.049 |
- 当然list_heapsが一番速い (全てのケースでリストの先頭要素を取り出すだけ)
- 二番目はsplay_heaps、三番目はpairing_heapsかgb_set_heapsのどちらか、といったところ
挿入と取り出しの混合
計測内容(再掲):
- 常時一定数の要素を保持しているヒープに対して、挿入と取り出しを繰り返す
- 今回は、具体的には以下のような条件で計測を行う
- ヒープの要素数は常に
4^n (n = 2..7)
- このヒープに対して「要素取り出し + 優先度が最低の要素の挿入」を一万回繰り返し、一回の処理の平均所要時間を計測する
- ヒープの要素数は常に
%% [計測関数] %% X = 2..7 > N = trunc(math:pow(4, X)), > Input = [{in, I} || I <- lists:seq(1, N)] ++ [case I rem 2 of 0 -> out; 1 -> {in, I} end || I <- lists:seq(N+1, 10000+N+1)], > heaps_bench:bench_in_out(Input).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.316 μs | 0.258 μs | 0.145 μs | 0.130 μs | 0.514 μs | 0.209 μs |
64 | 0.492 μs | 0.292 μs | 0.153 μs | 0.142 μs | 0.746 μs | 0.646 μs |
256 | 0.706 μs | 0.314 μs | 0.163 μs | 0.150 μs | 1.037 μs | 2.436 μs |
1024 | 0.861 μs | 0.355 μs | 0.165 μs | 0.151 μs | 1.339 μs | 9.645 μs |
4096 | 1.093 μs | 0.369 μs | 0.165 μs | 0.167 μs | 1.816 μs | 39.733 μs |
16384 | 1.093 μs | 0.275 μs | 0.155 μs | 0.151 μs | 2.460 μs | 170.720 μs |
O(log n) | O(1) | O(1) | O(1) | O(log n) | O(n) |
- paring_heaps、splay_heaps、binomial_heapsの3つが、ヒープ内の要素数に影響を受けずに
O(1)
で各操作が行えていた - 特にpairing_heapsとsplay_heapsの性能は良好
二つのヒープのマージ
計測内容(再掲):
- 要素数が
4^n (n = 2..5)
の二つのヒープのマージを千回繰り返し、一回の平均所要時間を計測する - 各ヒープの要素にはランダムな値が使用される
%% [計測関数] %% X = 2..5 > HeapSize = lists:seq(1, trunc(math:pow(4, X)), > heaps_bench:bench_merge( [begin {[random:uniform() || _ <- HeapSize], [random:uniform() || _ <- HeapSize]} end || _ <- lists:seq(1, 1000)]).
結果:
入力数 | leftist | binomial | splay | pairing | gb_set | list |
---|---|---|---|---|---|---|
16 | 0.478 μs | 0.141 μs | 3.765 μs | 0.102 μs | 4.575 μs | 1.174 μs |
64 | 0.825 μs | 0.197 μs | 15.291 μs | 0.114 μs | 16.559 μs | 5.072 μs |
256 | 0.960 μs | 0.196 μs | 65.527 μs | 0.114 μs | 69.478 μs | 28.971 μs |
1024 | 1.279 μs | 0.159 μs | 261.275 μs | 0.110 μs | 264.889 μs | 78.302 μs |
O(log n) | O(1) | O(n) | O(1) | O(n) | O(n) |
- pairing_heapsとbinomial_heapsは
O(1)
でマージできている - leftist_heapsは
O(log n)
だが、これも十分高速 - 残りは
O(n)
であり、マージが重要な用途での使用は現実的ではなさそうな感じ
感想
今回計測したほぼ全てのケースで良好な結果が出ているので、基本的にはpairing_heapsを使っておけば問題はなさそう。
ただしsplay_heapsとの差は(マージの場合を除けば)結構軽微なので、実装や利用途によってはsplay_heapsの方が良い可能性もあるかもしれない。