ブログ  前の記事  次の記事  2009-03-18 

たけまる / ets で bag と set のどちらを使うべきか


2009-03-18

_ ets で bag と set のどちらを使うべきか [erlang]

Erlang には ets というハッシュテーブルがあります.これはプロセス間
で状態を共有するときなどに利用します.

一般のハッシュテーブルは,キーに対応する値をひとつしか持てませんが,
ets では複数の値を持たせることができます.そのためには,初期化する
ときに bag という種類のテーブルを指定します.

bag では一つのキーが複数の値を持つことができます.

キー
a 1, 2, 3
b 1, ...

実際に動かすときは,こんな感じです.
> T = ets:new(mybag, [bag]).
> ets:insert(T, {a,1}).
> ets:insert(T, {a,2}).
> ets:lookup(T, a).
[{a,1},{a,2}]

ただし,同じ値を複数持つことはできません.つまり,次のようなことは
ありません.

キー
a 1, 1, 1
b 1, ...

また,通常の set という種類のテーブルでもリストを使うことによって,
同じことはできます.

キー
a [1, 2, 3]
b [1, ...]
...

さて,bag と set のどちらを使うべきでしょうか.性能を比較したことが
あったので,その結果を紹介します.

■ 単純な速度比較

実験を行い,bag と set に値を追加する速度を比較しました.

bag の場合は,複数のキー・値ペアを作り,それを追加しました.たとえ
ば,3 つの値を追加するときは,次のようになります.

> ets:insert(T, [{a,1}, {a,2}, {a,3}]).

これに対し,set では複数の値をリストにまとめ,それをキーと対応づけ
て追加しました.

> ets:insert(T, {a, [1,2,3]}).

値の数を変化させながら実験を行いました.表は,1024 試行にかかった
時間です.

値の数 bag [usec] set [usec] bag/set
1 2479 2469 1.00
4 6850 3878 1.77
16 28538 8876 3.22
64 185535 24948 7.44
256 1835755 83339 22.0
1024 28822918 459741 62.7

値の数が増えるほど bag が遅くなっています.bag の実装を調べていない
ので,はっきりした理由はわかりませんが,次のようなことが言えそうで
す.bag では,追加しようとしている値がすでに存在するかどうかをチェッ
クします.これにより,値の数が多いほど bag が遅くなっているのでしょ
う.

メモリ使用量は次のようでした.単位は word 数です.

値の数 bag set bag/set
1 10552 12600 0.84
4 42039 18744 2.24
16 166711 43320 3.85
64 665143 141624 4.70
256 2659383 534840 4.97
1024 10636471 2107704 5.05

■ 値チェックをしてから追加

では,どういうときに bag を使ったらよいのでしょうか.bag が強みを発
揮する状況を考えてみました.

実際には,すでに存在するキー・値ペアに,さらに値を追加するようなこ
とがあるでしょう.たとえば,次のような表があったとして,

キー
a 1, 2

a に 3 を追加して,次のようにします.

キー
a 1, 2, 3

このようにして値を追加するとき,bag では単純に追加するだけですが,
set の場合には古い値を取り出して,新しい値を追加したリストを生成し,
追加し直すことになります.つまり,bag と set の操作は,それぞれ次
のようになります.

bag のとき:
> ets:insert(T, {a, 3}).

set のとき:
> V = ets:lookup(T, a).
> ets:insert(T, {a, [3|V]}).

このような条件で実験した結果が次の表になります.

値の数 bag [usec] set [usec] bag/set
1 2535 3478 0.73
4 10007 15162 0.66
16 39596 103670 0.38
64 229646 1302852 0.18
256 2068567 18765916 0.11
1024 28103824 336595985 0.083

先ほどと異なり,値の数が増えるほど bag のほうが速くなっています.
set の,値を取り出してから追加するという処理が,かなり重いようです.

メモリ使用量も示しておきます.単位は word 数です.

値の数 bag set bag/set
1 10552 17720 0.60
4 42039 33080 1.27
16 166711 94520 1.76
64 665143 340280 1.95
256 2659383 1323320 2.01
1024 10636471 5255480 2.02

メモリ使用量は,どちらの実験でも set のほうが少ないです.ただし,
set のメモリ使用量が先ほどより増えている理由は不明です (上書きされ
たメモリが garbage collection されてないのだと思うけど).

■ まとめ

というわけで,速度を重視するのであれば,値を追加するときにすでに存
在する値を取得するかどうかで,bag と set の選択が決まりそうです.メ
モリを重視するのであれば,set が有利です.

なお,実際には値の大きさやキーの数によっても結果が変わってくると思
うので,この結果を参考に個々の状況に合わせて調べなおした方が安全で
す.

■ ソースコード

実験に使用したソースコードです.

-module(a).
-compile(export_all).
-define(KEYS, 1024).
-define(VALS, 1).

bag_insert(0) ->
    ok;
bag_insert(K) ->
    V = lists:map(fun(X) -> {K, X} end, lists:seq(1, ?VALS)),
    ets:insert(bag, V),
    bag_insert(K-1).

set_insert(0) ->
    ok;
set_insert(K) ->
    V = lists:map(fun(X) -> X end, lists:seq(1, ?VALS)),
    ets:insert(set, {K, V}),
    set_insert(K-1).

start() ->
    ets:new(bag, [bag, named_table]),
    {Usec, _} = timer:tc(?MODULE, bag_insert, [?KEYS]),
    io:format("bag: ~p [usec]~n", [Usec]),
    io:format("bag: ~p W~n", [ets:info(bag, memory)]),    
	
    ets:new(set, [set, named_table]),
    {Usec1, _} = timer:tc(?MODULE, set_insert, [?KEYS]),
    io:format("set: ~p [usec]~n", [Usec1]),
    io:format("set: ~p W~n", [ets:info(set, memory)]),

    ok.

-module(b).
-compile(export_all).
-define(KEYS, 1024).
-define(VALS, 1).

bag_insert(0) ->
    ok;
bag_insert(K) ->
    lists:foreach(
      fun(X) ->
              ets:insert(bag, {K, X})
      end,
      lists:seq(1, ?VALS)
     ),
    bag_insert(K-1).

set_insert(0) ->
    ok;
set_insert(K) ->
    lists:foreach(
      fun(X) ->
              V = ets:lookup(bag, K),
              ets:insert(set, {K, [X|V]})
      end,
      lists:seq(1, ?VALS)
     ),
    set_insert(K-1).

start() ->
    ets:new(bag, [bag, named_table]),
    {Usec, _} = timer:tc(?MODULE, bag_insert, [?KEYS]),
    io:format("bag: ~p [usec]~n", [Usec]),
    io:format("bag: ~p W~n", [ets:info(bag, memory)]),
	
    ets:new(set, [set, named_table]),
    {Usec1, _} = timer:tc(?MODULE, set_insert, [?KEYS]),
    io:format("set: ~p [usec]~n", [Usec1]),
    io:format("set: ~p W~n", [ets:info(set, memory)]),

    ok.

一言メッセージをこっそり送信できます (非公開)
 今年の西暦→