データの存在チェック
setはそもそもが常にユニーク判定が必要なのでデータの検索は速いデータ構造です。 hashmapのようなアルゴリズムでO(1)に近いデータの存在チェックができるのは、まぁそうだろうなと理解はしていました。
だがしかし、10万個の要素から10万回存在チェックすると思いのほか差が大きい。
listはsetよりかなり遅い、そしてarray.arrayがlistよりもさらに遅い。
array.arrayは'i'を指定してintが格納されることを宣言しています。
data structure | loop | time | script | list | for | 450.540 | append_to_list_with_loop.py | list | Comprehension | 456.691 | append_to_list_with_comp.py | set | for | 0.909 | add_to_set_with_loop.py | set | Comprehension | 0.889 | add_to_set_with_comp.py | set | convert from list Comprehension | 0.889 | add_to_set_with_comp.py | array | for | 873.938 | append_to_array_with_loop.py | array | Comprehension | 867.495 | append_to_array_with_comp.py |
行ごとの実行時間など
line_profilerで行ごとの実行回数や処理時間を確認します。
append_to_list_with_loop.py
17行目、listから要素の検索でほぼ全ての時間を使っています。
Timer unit: 1e-06 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
12 @profile
13 def proc():
14 3 9.0 3.0 0.0 cnt = 0
15 3 1558856.0 519618.7 0.4 data = create_data()
16 300003 205982.0 0.7 0.0 for i in range(100000):
17 300000 441637215.0 1472.1 99.6 if randint(1, 10000000) in data:
18 3039 3107.0 1.0 0.0 cnt += 1
add_to_set_with_loop.py
14行目のデータ生成と17行目のsetから要素の検索のトータル時間がほぼ同じになっています。
Timer unit: 1e-06 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
11 @profile
12 def proc():
13 3 8.0 2.7 0.0 cnt = 0
14 3 1593041.0 531013.7 48.2 data = create_data()
15 300003 114824.0 0.4 3.5 for i in range(100000):
16 300000 1596492.0 5.3 48.3 if randint(1, 10000000) in data:
17 2936 1242.0 0.4 0.0 cnt += 1
append_to_array_with_loop.py
18行目、array.arrayからの要素の検索はとても遅くlistの2倍近い時間がかかっています。
Timer unit: 1e-06 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
13 @profile
14 def proc():
15 3 5.0 1.7 0.0 cnt = 0
16 3 1586118.0 528706.0 0.2 data = create_data()
17 300003 200262.0 0.7 0.0 for i in range(100000):
18 300000 873139503.0 2910.5 99.8 if randint(1, 10000000) in data:
19 2905 2835.0 1.0 0.0 cnt += 1
メモリ効率
memory_profilerで行ごとにどのくらいメモリの増減があるか見ていきます。
append_to_list_with_loop.py
listの場合は10万要素で0.7MiBくらい増加します。1要素7byteくらい?
Line # Mem usage Increment Line Contents
================================================
5 35.980 MiB 35.980 MiB @profile
6 def create_data():
7 35.980 MiB 0.000 MiB l = list()
8 41.668 MiB 0.000 MiB for i in range(100000):
9 41.668 MiB 0.699 MiB l.append(randint(1, 10000000))
10 41.668 MiB 0.000 MiB return l
append_to_list_with_comp.py
リスト内包表記を使ってもメモリの使用量は変わらないようです。
Line # Mem usage Increment Line Contents
================================================
5 35.984 MiB 35.984 MiB @profile
6 def create_data():
7 41.645 MiB 0.699 MiB return [randint(1, 10000000) for i in range(100000)]
add_to_set_with_loop.py
9行目、setの場合はlistの6倍近いメモリを使っているようです。1要素40byteくらい?
Line # Mem usage Increment Line Contents
================================================
5 36.062 MiB 36.062 MiB @profile
6 def create_data():
7 36.062 MiB 0.000 MiB s = set()
8 45.277 MiB 0.000 MiB for i in range(100000):
9 45.277 MiB 4.000 MiB s.add(randint(1, 10000000))
10 45.277 MiB 0.000 MiB return s
add_to_set_with_comp.py
リスト内包表記と同様、セット内包表記にしてもメモリ利用量は変わりません。
Line # Mem usage Increment Line Contents
================================================
5 36.094 MiB 36.094 MiB @profile
6 def create_data():
7 45.312 MiB 4.000 MiB return {randint(1, 10000000) for i in range(100000)}
append_to_list_with_comp_and_create_set.py
リスト内包表記でデータを作ってからsetを生成するとメモリ使用量は増えました。1要素60byteくらいに。list生成分とset生成分よりも多く増えているのでrandintの影響があるかも?今更ですが、そもそもrandintである必要はなかった気がします。
Line # Mem usage Increment Line Contents
================================================
5 36.047 MiB 36.047 MiB @profile
6 def create_data():
7 47.770 MiB 6.043 MiB return set([randint(1, 10000000) for i in range(100000)])
append_to_array_with_loop.py
んー。70KiBしか増えないのはちょっとおかしい気が。はてはて?別の世界に行ってる?
Line # Mem usage Increment Line Contents
================================================
6 36.051 MiB 36.051 MiB @profile
7 def create_data():
8 36.059 MiB 0.008 MiB a = array('i')
9 36.484 MiB 0.000 MiB for i in range(100000):
10 36.484 MiB 0.070 MiB a.append(randint(1, 10000000))
11 36.484 MiB 0.000 MiB return a
append_to_array_with_comp.py
リスト内包表記からarrayの生成。リスト内包表記分だけ増えてますね…
Line # Mem usage Increment Line Contents
================================================
6 36.109 MiB 36.109 MiB @profile
7 def create_data():
8 41.797 MiB 0.699 MiB return array('i', [randint(1, 10000000) for i in range(100000)])
感想
だいたい思っていた通りなのだけれど次のような感想を得られた。
そしてsetに関して、以下は感覚的にわかるのだけれど。
setは通常はO(1)
ハッシュ衝突時には最悪O(n)になる
ハッシュ衝突のあたりのコードとかアルゴリズムとか知らないので勉強が必要。
そもそもhashmapのアルゴリズムとか、10年くらい前にLightCloudチラ見してたときに出くわしたConsistent Hash Ringのイメージくらいしかなくて、Pythonのhashmap的なものがどういう設計なのかもわかってない。
次はdisモジュールでちゃんと確認しよう。