當我們談論Erlang Maps時,我們談論什麼 Part 1
阿新 • • 發佈:2019-02-03
Erlang 增加 Maps資料型別並不是很突然,因為這個提議已經進行了2~3年之久,只不過Joe Armstrong老爺子最近一篇文章Big changes to Erlang掀起不小了風浪.這篇文章用了比較誇張的說法:"Records are dead - long live maps !",緊接著在國內國外社群這句話就傳遍了.馬上就有開發者憂心忡忡的在Stackoverflow上提問:Will Erlang R17 still have records? 套用一句文藝的話,當我們談論Maps時,實際上是表達我們對record的不滿,這些不滿/痛點恰好就是我們寄希望於Maps能夠提供給我們的.本文將盡可能的逐一列出這些點,並嘗試分析原因,下篇文章將深入分析Maps的一些細節.
Record的痛點
使用Record我們遇到哪些痛點呢?這些痛點在Maps出現之後有所改善嗎?我們先從細數痛點開始:1.可以把record的name用作引數嗎?簡單講就是#RecordName{} 可以嗎?
12345678910 | 7> rd(person,{name,id}). person 8> #person{}. #person{name = undefined,id = undefined} 9> P=person. person 10> #P{}. * 1: syntax error before: P 10> |
12345 | 10> N=name. name 11> #person{N= "zen" }. * 1: field 'N' is not an atom or _ in record person 12> |
123456789101112131415 | Eshell V6.0 (abort with ^G) 1> rd(foo,{a,b,c}). foo 2> rd(a,{f,m}). a 3> rd(f,{id,name}). f 4> #foo{a=#a{f=#f{id=2002,name= "zen" },m=1984},b=1234,c=2465}. #foo{a = #a{f = #f{id = 2002,name = "zen"},m = 1984}, b = 1234,c = 2465} 5> D=v(4). #foo{a = #a{f = #f{id = 2002,name = "zen"},m = 1984}, b = 1234,c = 2465} 6> D#foo.a#a.f#f.name. "zen" |
原因何在?
在record相關的問題中,常常提到的一個詞就是"compile-time dependency",即record只存在於編譯時,並沒有對應實際的資料型別.record本質上是tuple在語法層面的語法糖,而上面record的諸多問題其實就是源於tuple,在著名的exprecs專案,有這樣一段描述:This parse transform can be used to reduce compile-time dependencies in large systems.Record即Tuple 在內部表示沒有record只有tuple, 下面是Erlang資料內部表示的介紹,我做了一個長圖: 這幾張圖可以幫助我們建立起來Erlang資料內部表示的思考模型,我們簡單梳理一下: Beam(Björns/Bogdans Erlang Abstract Machine)虛擬機器,包含一個擁有1024個虛擬暫存器的虛擬暫存器機,程式變數可能儲存在register或stack;垃圾回收是以程序為單位,逐代進行;Beam包含一個常量池( constant pool)不被GC.大型二進位制資料在Heap外,並可被多個程序共享;VM Code中用來表達資料型別使用的概念是Eterm:一個Eterm通常一個字(word)大小( sizeof(void *)),程序的Heap實際上就是Eterm構成的陣列,ETS也是以Eterm的形式儲存資料.暫存器(register)也是Eterm,VM中的stack也是由Eterm組成;VM需要在程序heap上分配一些Eterm來表示一些複雜的資料結構比如list,tuple;如果變數指向的資料複雜,那麼stack/register會包含指向heap的指標,換句話話說,Eterm要支援指標;
In the old days, before records, Erlang programmers often wrote access functions for tuple data. This was tedious and error-prone. The record syntax made this easier, but since records were implemented fully in the pre-processor, a nasty compile-time dependency was introduced.
This module automates the generation of access functions for records. While this method cannot fully replace the utility of pattern matching, it does allow a fair bit of functionality on records without the need for compile-time dependencies.
Eterm其實是使用一些二進位制資料位來標記當前的資料型別,Erlang使用了一個層次化的標記系統,最基礎的是使用最低兩位primary tags來標識: 00 = Continuation pointer (return address on stack) or header word on heap
01 = Cons cell (list)
10 = Boxed (tuple, float, bignum, binary, external pid/port, exterrnal/internal ref ...)
11 = Immediate (the rest - secondary tag present)具體到Boxed型別,繼續細分:– 0000 = Tuple
– 0001 = Binary match state (internal type)
– 001x = Bignum (needs more than 28 bits)
– 0100 = Ref
– 0101 = Fun
– 0110 = Float
– 0111 = Export fun (make_fun/3)
– 1000 - 1010 = Binaries
– 1100 - 1110 = External entities (Pids, Ports and Refs)看到了吧,這裡已經沒有record的蹤影了,只有tuple,而對於Maps,我們已經可以在17.0-rc2/erts/emulator/beam/erl_term.h的程式碼中找到它的subtag:#define ARITYVAL_SUBTAG (0x0 << _TAG_PRIMARY_SIZE) /* TUPLE */
#define BIN_MATCHSTATE_SUBTAG (0x1 << _TAG_PRIMARY_SIZE)
#define POS_BIG_SUBTAG (0x2 << _TAG_PRIMARY_SIZE) /* BIG: tags 2&3 */
#define NEG_BIG_SUBTAG (0x3 << _TAG_PRIMARY_SIZE) /* BIG: tags 2&3 */
#define _BIG_SIGN_BIT (0x1 << _TAG_PRIMARY_SIZE)
#define REF_SUBTAG (0x4 << _TAG_PRIMARY_SIZE) /* REF */
#define FUN_SUBTAG (0x5 << _TAG_PRIMARY_SIZE) /* FUN */
#define FLOAT_SUBTAG (0x6 << _TAG_PRIMARY_SIZE) /* FLOAT */
#define EXPORT_SUBTAG (0x7 << _TAG_PRIMARY_SIZE) /* FLOAT */
#define _BINARY_XXX_MASK (0x3 << _TAG_PRIMARY_SIZE)
#define REFC_BINARY_SUBTAG (0x8 << _TAG_PRIMARY_SIZE) /* BINARY */
#define HEAP_BINARY_SUBTAG (0x9 << _TAG_PRIMARY_SIZE) /* BINARY */
#define SUB_BINARY_SUBTAG (0xA << _TAG_PRIMARY_SIZE) /* BINARY */
#define MAP_SUBTAG (0xB << _TAG_PRIMARY_SIZE) /* MAP */
#define EXTERNAL_PID_SUBTAG (0xC << _TAG_PRIMARY_SIZE) /* EXTERNAL_PID */
#define EXTERNAL_PORT_SUBTAG (0xD << _TAG_PRIMARY_SIZE) /* EXTERNAL_PORT */
#define EXTERNAL_REF_SUBTAG (0xE << _TAG_PRIMARY_SIZE) /* EXTERNAL_REF */ 感興趣的話,可以繼續在otp_src_17.0-rc2\erts\emulator\beam\erl_term.h中看到tuple實現相關的程式碼,搜尋/* tuple access methods */程式碼段. 注意裡面提到的erts_debug:size/1 和 erts_debug:flat_size/1方法,可以幫助我們檢視共享和非共享狀態資料佔用的字數.所謂的共享和非共享,就是通過複用一些資料塊(即指標指向)而不是通過資料拷貝,這樣提高效率.在一些萬不得已的情況下再觸發拷貝,比如資料發往別的節點,存入ETS等等, Erlang Efficiency Guide 很多優化的小技巧都是從這個出發點考慮的. 那去掉primary tag和sub tag之後tuple是一個什麼樣的資料結構呢?我們可以從兩個角度來看,首先是Erlang Interface Reference Manual中erl_mk_tuple方法明確指示了tuple實際上是一個Eterm的陣列:ETERM *erl_mk_tuple(array, arrsize)
Types:
ETERM **array;
int arrsize;
Creates an Erlang tuple from an array of Erlang terms.
array is an array of Erlang terms.
arrsize is the number of elements in array. 另外一個角度就是在bif.c中,tuple_to_list和list_to_tuple的實現,其實就是陣列和連結串列的互相轉換,看程式碼還可以知道通過make_arityval(len)冗餘了陣列的長度.對於tuple,獲得size和按照索引訪問資料都是很快的.這也就是找EEP43中提到過的Record的優勢:
- 快速查詢 O(1), 編譯期間完成了對key的索引,對於小資料量存取相當快 (~50 values),
- 沒有過多額外的記憶體消耗,只有Value和name 2+ N個字 (name + size+ N)
- 函式頭完成匹配