ets (erlang term storage) 與 dets (disk ets) 是用來負責大量 Erlang Term 的儲存機制。兩個都是提供大量的 key-value 尋找表,ETS 放記憶體, DETS 放 disk,都可被數個 process 共享,ETS 與 DETS 是 key-value 結構,只是 erlang tuples 的集合。
存在 ETS 表內的資料,是短暫的 transient,當 ETS 被 disposed,資料就會被刪除,而 DETS 內的資料是永續 persistent 的,即使erlang crash,資料也不會受損,在啟動 DETS 時,會自動檢查資料一致性 consistency,如果資料有問題,就會試圖維修,因為所有表格都要被檢查,這個動作會花上一些時間,但系統 crash 之前的最後一筆資料可能會遺失。
當系統 crash 而需要重置時,DETS 就會需要花上一些時間,才能將啟動的程序做完。
如果 APP 以 erlang 原生的機制:nondestructive assignment 與 pure erlang data structure 實作時,發現有實作的困難,而且需要高效率處理大量的資料,就可以使用 ETS。
ETS 並不是用 erlang 實作的,內部也跟普通的 erlang 物件不一樣,而且 ETS 並沒有 garbage collection 的機制,不會因為 gc 而影響效率。
基本操作
- 建立新 table 或開啟舊 table
ets:new 或 dets:open_file - 對某個 table 插入 tuple/tuple list
insert(Tablename, X) ,其中 X 是一個 tuple/tuple list - 對某個 table 查詢,以取得 tuple list
lookup(TableName, Key) ,回傳符合 Key 的 tuple list - 銷毀一個 table
ets:delete(TableId) 或 dets:close(TableId)
table 型別
基本型別有兩種(1) set: key 不可重複 (2) bag: 允許數個 tuple(值組) 有相同的 key
set 或 bag 都有兩種變形, 所以有四種表格
- set:所有的 key 都不一樣
- ordered set:tuple 會根據 key 值被排序
- bag:key 可以重複
- duplicate bag:key 跟 tuple 都可以重複
-module(ets_test).
-export([start/0]).
start() ->
lists:foreach(fun test_ets/1,
[set, ordered_set, bag, duplicate_bag]).
test_ets(Mode) ->
TableId = ets:new(test, [Mode]),
ets:insert(TableId, {a,1}),
ets:insert(TableId, {b,2}),
ets:insert(TableId, {a,1}),
ets:insert(TableId, {a,3}),
List = ets:tab2list(TableId),
io:format("~-13w => ~p~n", [Mode, List]),
ets:delete(TableId).
測試
1> ets_test:start().
set => [{b,2},{a,3}]
ordered_set => [{a,3},{b,2}]
bag => [{b,2},{a,1},{a,3}]
duplicate_bag => [{b,2},{a,1},{a,1},{a,3}]
ok
ETS 表格的效率考量
在系統內部,ETS 表格是用 hash 表格呈現的(ordered set 例外, 內部使用的是平衡二元樹),這表示使用 set,空間有浪費,而使用 ordered set,時間有浪費。
插入 set,耗費的時間是固定的,插入ordered set,表格內的項目越多,花的時間越多。
bag 每次有新的元素加入,都要比對是否重複,如果相同 key 的值組很多,就會很耗時間,而 duplicate bag 插入資料所花的時間就比較短。
ETS 的 owner 是建立它的行程,行程死亡或呼叫 ets:delete,表格就會被刪除。
ETS 不是 gc 處理的對象,即使有大量資料存在 ETS 裡面,也不會因為 garbage collection 影響效率。
當一個 tuple 插入到ETS表,代表此 tuple 的所有資料結構從行程堆疊被複製到 ETS 表。進行 lookup 時,結果的 tuple 會從 ETS 複製到行程的堆疊 & heap,除了 binary 資料之外的所有資料結構都是這樣。
大型二元資料會存在獨立的 heap 儲存區域,此區域可被數個行程及 ETS表共享,「計算參考個數」的gc會管理個別的二元資料,當使用到此 binary 的行程和表格的總數量降為 0 時,此儲存區域會被回收。
換句話說:「含有大型二元資料」的訊息,成本很低。在 tuple 插入包含二元的資料到 ETS 表中,也很便宜。
原則:盡可能地使用二元,來表示字串&大型的無型別記憶體區塊。
建立 ETS表
呼叫 ets:new 的行程被認為是該 table 的 owner,如果 owner process 死亡,table 的空間就會自動被回收。
@spec ets:new(Name, [Opt]) -> TableId
Name 是 atom
[Opt]是選項清單
set | ordered_set | bag | duplicate_bag
private -> 只有 owner 行程可以讀寫
public -> 只要知道 TableId 都可以讀寫此table
protected -> 只有 owner 行程可以讀寫,其他行程可以讀取資料
named_table -> 可使用 Name 作為後續的操作
{keypos, K} -> 把 K 當作 key的位置,正常狀況 K 為 1
預設值 [set, protected, {keypos,1}]
public table 因任何行程都可以讀寫,使用者必須自己確定此 table 的讀寫有一致性,資料才不會亂掉。
最常使用的是 protected,因為只有 owner 行程可以改變 table 的資料,而所有行程都可以讀取資料,資料是共享的,成本幾乎為 0。
ets 範例: trigram
功能:寫一個探索程式,試圖預測某個字串是否為一個英文單字
方法:使用三個字母組成的序列 trigram,測試字串中所有連續字母的序列,是否符合「產生自大量英文字」的 trigram 集合
程式:從一個很大的英文字集合中,找出所有 trigram
- 做一個 iterator,在英文字典中,取得所有 trigram
- 建立 ETS,型別是集合 & 有序集合,代表所有的 trigram,建立一個集合包含所有的 trigram
- 測量建立這些不同表格所需的時間
- 測量存取這些不同表格所需要的時間
- 根據量測結果,選取最佳者,並為它寫存取常式
-module(lib_trigrams).
-export([for_each_trigram_in_the_english_language/2,
make_tables/0, timer_tests/0,
open/0, close/1, is_word/2,
how_many_trigrams/0,
make_ets_set/0, make_ets_ordered_set/0, make_mod_set/0,
lookup_all_ets/2, lookup_all_set/2
]).
-import(lists, [reverse/1]).
% 測量產生 set, ordered set, 或使用 erlang sets 模組 所要花的時間,以及平均每個 trigram 耗用的位元組大小
make_tables() ->
{Micro1, N} = timer:tc(?MODULE, how_many_trigrams, []),
io:format("Counting - No of trigrams=~p time/trigram=~p~n",[N,Micro1/N]),
{Micro2, Ntri} = timer:tc(?MODULE, make_ets_ordered_set, []),
FileSize1 = filelib:file_size("trigramsOS.tab"),
io:format("Ets ordered Set size=~p time/trigram=~p~n",[FileSize1/Ntri,
Micro2/N]),
{Micro3, _} = timer:tc(?MODULE, make_ets_set, []),
FileSize2 = filelib:file_size("trigramsS.tab"),
io:format("Ets set size=~p time/trigram=~p~n",[FileSize2/Ntri, Micro3/N]),
{Micro4, _} = timer:tc(?MODULE, make_mod_set, []),
FileSize3 = filelib:file_size("trigrams.set"),
io:format("Module sets size=~p time/trigram=~p~n",[FileSize3/Ntri, Micro4/N]).
% 測量 lookup 每一個 trigram 所要花的平均時間
timer_tests() ->
time_lookup_ets_set("Ets ordered Set", "trigramsOS.tab"),
time_lookup_ets_set("Ets set", "trigramsS.tab"),
time_lookup_module_sets().
time_lookup_ets_set(Type, File) ->
% 以 ets:file2tab 將 檔案轉換為 table
{ok, Tab} = ets:file2tab(File),
% table 換成 list
L = ets:tab2list(Tab),
Size = length(L),
% 開始測量 lookup 的總時間
{M, _} = timer:tc(?MODULE, lookup_all_ets, [Tab, L]),
io:format("~s lookup=~p micro seconds~n",[Type, M/Size]),
ets:delete(Tab).
lookup_all_ets(Tab, L) ->
lists:foreach(fun({K}) -> ets:lookup(Tab, K) end, L).
time_lookup_module_sets() ->
% 讀取檔案,取得 Bin
{ok, Bin} = file:read_file("trigrams.set"),
% binary_to_term 換成 sets
Set = binary_to_term(Bin),
% sets:to_list 換成 list
Keys = sets:to_list(Set),
Size = length(Keys),
% 開始測量 lookup 的總時間
{M, _} = timer:tc(?MODULE, lookup_all_set, [Set, Keys]),
io:format("Module set lookup=~p micro seconds~n",[M/Size]).
lookup_all_set(Set, L) ->
lists:foreach(fun(Key) -> sets:is_element(Key, Set) end, L).
% 計算總共有幾個 trigrams
how_many_trigrams() ->
F = fun(_, N) -> 1 + N end,
for_each_trigram_in_the_english_language(F, 0).
%% ==========================
% 利用 trigrams 建立 set, ordered set table
make_ets_ordered_set() -> make_a_set(ordered_set, "trigramsOS.tab").
make_ets_set() -> make_a_set(set, "trigramsS.tab").
make_a_set(Type, FileName) ->
% 建立 set 或 ordered set 的 table
Tab = ets:new(table, [Type]),
% 定義 fun,將 Str 轉換為 binary ,然後以 {Key} 形式的 tuple 存到 ets Tab 裡面
F = fun(Str, _) -> ets:insert(Tab, {list_to_binary(Str)}) end,
% 將 F 送入 for_each_trigram_in_the_english_language
for_each_trigram_in_the_english_language(F, 0),
% 把 Table 的資料,轉存到 Filename 檔案裡面
% Dumps the table Tab to the file Filename
ets:tab2file(Tab, FileName),
% 查詢 Tab 的 size
Size = ets:info(Tab, size),
% 刪除 Tab
ets:delete(Tab),
% 把 Size 傳回去
Size.
% 改用 sets 模組來存放 trigrams
make_mod_set() ->
D = sets:new(),
F = fun(Str, Set) -> sets:add_element(list_to_binary(Str),Set) end,
D1 = for_each_trigram_in_the_english_language(F, D),
% term_to_binary 把 sets 轉成 binary,然後再寫入到檔案中
file:write_file("trigrams.set", [term_to_binary(D1)]).
%% ==========================
%% An iterator that iterates through all trigrams in the language
%% 將英文中每個 trigram 套用 fun F
% F 是 fun(Str,A) -> A
% Str 的範圍遍及語言中所有 trigram,A是 accumulator
% 使用 354984 個英文字產生 trigram
for_each_trigram_in_the_english_language(F, A0) ->
{ok, Bin0} = file:read_file("354984si.ngl.gz"),
% 以 zlib:gunzip 解壓縮
Bin = zlib:gunzip(Bin0),
scan_word_list(binary_to_list(Bin), F, A0).
scan_word_list([], _, A) ->
A;
scan_word_list(L, F, A) ->
{Word, L1} = get_next_word(L, []),
% Word 就是 reverse([$\s|L])
% 把 $\s 加到 Word 前面,就等於 $\s + L + $\s
% 每個單字的前後刻意加上一個空白字元,在這裡刻意將空白字元當作正常的字母
A1 = scan_trigrams([$\s|Word], F, A),
% 處理完成,取得 $\s + L + $\s 的所有 trigram 後,再繼續處理下一個 word
scan_word_list(L1, F, A1).
%% scan the word looking for \r\n
%% the second argument is the word (reversed) so it
%% has to be reversed when we find \r\n or run out of characters
% 取得下一行的單字
% 一個字元一個字元慢慢累加到 L,如果 pattern 吻合 [\r\n|T],
% 就代表到該行行末,這時候就回傳 reverse([$\s|L])
% $\s 是一個空白字元,就等於把空白字元加到 L 前面,然後再 reverse 這個 list
get_next_word([$\r,$\n|T], L) -> {reverse([$\s|L]), T};
get_next_word([H|T], L) -> get_next_word(T, [H|L]);
% 最後一行的 word 沒有行末的 $\r $\n,最後會是 [],因此就直接回傳 reverse([$\s|L])
get_next_word([], L) -> {reverse([$\s|L]), []}.
% 三個字元的 list
scan_trigrams([X,Y,Z], F, A) ->
F([X,Y,Z], A);
% 超過三個字元的 list
scan_trigrams([X,Y,Z|T], F, A) ->
A1 = F([X,Y,Z], A),
scan_trigrams([Y,Z|T], F, A1);
% 其他狀況的 list,就直接把 accumulator A 傳回去
scan_trigrams(_, _, A) ->
A.
%% ==========================
%% access routines
%% open() -> Table
%% close(Table)
%% is_word(Table, String) -> Bool
is_word(Tab, Str) -> is_word1(Tab, "\s" ++ Str ++ "\s").
is_word1(Tab, [_,_,_]=X) -> is_this_a_trigram(Tab, X);
is_word1(Tab, [A,B,C|D]) ->
case is_this_a_trigram(Tab, [A,B,C]) of
true -> is_word1(Tab, [B,C|D]);
false -> false
end;
is_word1(_, _) ->
false.
is_this_a_trigram(Tab, X) ->
% 以 lookup 的方式,確認是不是在 字典產生的所有 trigram 裡面
case ets:lookup(Tab, list_to_binary(X)) of
[] -> false;
_ -> true
end.
open() ->
% filename:dirname(code:which(?MODULE)) 可取得載入目前模組的目錄
{ok, I} = ets:file2tab(filename:dirname(code:which(?MODULE))
++ "/trigramsS.tab"),
I.
close(Tab) -> ets:delete(Tab).
建立 table 要花的時間
測量產生 set, ordered set, 或使用 erlang sets 模組 所要花的時間,以及平均每個 trigram 耗用的位元組大小
1> lib_trigrams:make_tables().
Counting - No of trigrams=3357707 time/trigram=0.30437438406626904
Ets ordered Set size=19.029156153630503 time/trigram=1.0733515461593284
Ets set size=19.028408559947668 time/trigram=0.7421731556684368
Module sets size=9.433978132884777 time/trigram=2.5252352274930483
ok
總共有 3357707 個trigram,每個trigram處理了 0.3 sec。
在 ordered set,插入一個 trigram 要花 1.07 sec,耗用 19.029 bytes
在 set,插入一個 trigram 要花 0.74 sec,耗用 19.028 bytes
在 sets,插入一個 trigram 要花 2.52 sec,耗用 9.433 bytes
存取 table 要花的時間
2> lib_trigrams:timer_tests().
Ets ordered Set lookup=0.6541444724792076 micro seconds
Ets set lookup=0.37379684141669 micro seconds
Module set lookup=0.5606952621250351 micro seconds
ok
結果
ETS set 的效能最好
預測某個字串是否為一個英文單字
3> Table=lib_trigrams:open().
32784
4> lib_trigrams:is_word(Table, "test").
true
5> lib_trigrams:is_word(Table, "tess").
true
6> lib_trigrams:is_word(Table, "ess").
true
7> lib_trigrams:is_word(Table, "esx").
false
8> lib_trigrams:is_word(Table, "esxit").
false
9> lib_trigrams:close(Table).
true
DETS
DETS 將 erlang tuple 儲存在 disk 裡面,檔案最大的體積為 2GB,必須先開啟才能使用,使用完畢也要關閉檔案。如果沒有正確關閉,下次開啟會自動修復,但會花費一些時間。
當一個 DETS 表被開啟,必須指定一個全域的名稱,如果兩個以上的本地行程用相同名稱 & 選項開啟一個 DETS,這些行程就會共享表格,此表格會一直開啟,直到所有行程把表格關閉/行程當機後,才真正地關閉。
範例:檔名索引
建立一個磁碟式表格,將檔案對應到整數,或反向對應:filename2index & index2filename。
建立一個 DETS表,在內部放入三個不同型別的值組 tuple
{free, N}
N 是表格中的第一個自由的索引,當我們在表格中輸入一個新檔名,會被指定索引 N
{FileNameBin, K}
FileNameBin(二元) 被指定索引 K
{K, FileNameBin}
K (整數) 代表檔案 FilenameBin
注意每個新檔案的加入,如何在表格內加入兩個項目:
File -> Index
Index -> Filename
先寫一個程式,會開啟/關閉「儲存所有檔案名稱」的 DETS表
-module(lib_filenames_dets).
-export([open/1, close/0, test/0, filename2index/1, index2filename/1]).
open(File) ->
io:format("dets opened:~p~n", [File]),
Bool = filelib:is_file(File),
% ?MODULE 會自動展開成 lib_filenames_dets,因為 DETS 必須要有一個唯一的 table 名稱
% 而 模組名稱也是唯一的,所以就使用 ?MODULE 來當作 Table 名稱
case dets:open_file(?MODULE, [{file, File}]) of
% 開啟 dets,但以 filelib:is_file 判斷是不是新的檔案
% 如果檔案原本不存在,就 insert 一個 {free,1} 來初始化這個 DETS
{ok, ?MODULE} ->
case Bool of
true -> void;
false -> ok = dets:insert(?MODULE, {free,1})
end,
true;
{error,_Reason} ->
io:format("cannot open dets table~n"),
exit(eDetsOpen)
end.
close() -> dets:close(?MODULE).
% 為求高效率,檔名必須要是 binary
% 在 ETS, DETS 裡面,要習慣用 binary 來表示字串
%% 注意: 這裡可能會產生 race condition,在 dets:insert 被呼叫前,
%% 如果有兩個平行的 process 呼叫了 dets:lookup,filename2index將產生錯誤的結果
filename2index(FileName) when is_binary(FileName) ->
case dets:lookup(?MODULE, FileName) of
[] ->
[{_,Free}] = dets:lookup(?MODULE, free),
ok = dets:insert(?MODULE,
[{Free,FileName},{FileName,Free},{free,Free+1}]),
Free;
[{_,N}] ->
N
end.
% 將索引值換成檔名
index2filename(Index) when is_integer(Index) ->
case dets:lookup(?MODULE, Index) of
% 發生錯誤時,回傳 error
[] -> error;
[{_,Bin}] -> Bin
end.
test() ->
file:delete("./filenames.dets"),
open("./filenames.dets"),
F = lib_files_find:files(".", ".ebeam$", true),
lists:foreach(fun(I) -> filename2index(list_to_binary(I)) end, F).
測試
1> lib_filenames_dets:test().
dets opened:"./filenames.dets"
ok
ETS 與 DETS 的其他功能
- 根據 pattern 來取出或刪除物件
- 在 ETS 與 DETS 之間做轉換,或是在 ETS 與磁碟檔案之間做轉換
- 為一個 Table 找出 resource usage
- traverse Table 內的元素
- 修復破碎的 DETS table
- 視覺化 Table
ETS 與 DETS 原本是設計用來實現 Mnesia 的,Mnesia 內部使用了 ETS 與 DETS Table,並把一些細節隱藏起來,提供了高階的功能。
參考
Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World
沒有留言:
張貼留言