Erlang中有8种基础数据类型(integer、 float、 atom、 reference、 fun、 port、 pid、 binary)和两种复合结构(tuple、list)。在erl编程时,对这些数据类的内存分配及引用都是透明的,了解erl数据类型的内部实现,可以让我们更优更合理的运用数据类型,更好地评估程序性能。
假设erl中有这么一组数据: 1、2、3、"a"、"b"、"c",
在erl虚拟机(vm)中将会这样描述erl数据:1F、2F、3F、3214B、4B6CB、1EDCB,
从上面可以看出,在vm内部,erl的数据类型是以后缀来识别的,比如小整数的后缀都为F,封装方法可用C表示为 (var << 4) | 0xF。
同时可以看出字符串的后缀都为B,但却不同与小整数的封装方法,数字串以及更多的数据类型究竟是如何封装的呢?我们继续往下看。
下面是一些erl数据 对应 在vm中的描述的示例。
@erlang | @vm |
---|---|
1 | 0x1F |
2 | 0x2F |
1234567890 | 0x1B0A37A |
1234567891 | 0x1B0A3A2 |
-1 | 0xFFFFFFFF |
-2 | 0xFFFFFFEF |
[1,2] | 0x1B0BDE9 |
[3,4] | 0x1B0BEA5 |
"ab" | 0x1B0A19D |
"cb" | 0x1B0A1CD |
<<"ab">> | 0x1B0BFAE |
<<"cd">> | 0x1B0C06A |
rolong | 0x6BD0B |
mytest | 0x6BD4B |
以上看来,有些类型似乎不好找规律了,只好翻阅erlang源码,一探究竟。
erl_term.h中可以看到如下代码:
#define _TAG_PRIMARY_SIZE 2
#define _TAG_PRIMARY_MASK 0x3
#define TAG_PRIMARY_HEADER 0x0
#define TAG_PRIMARY_LIST 0x1
#define TAG_PRIMARY_BOXED 0x2
#define TAG_PRIMARY_IMMED1 0x3
#define _TAG_IMMED1_SIZE 4
#define _TAG_IMMED1_MASK 0xF
#define _TAG_IMMED1_PID ((0x0 << _TAG_PRIMARY_SIZE) | TAG_PRIMARY_IMMED1)
#define _TAG_IMMED1_PORT ((0x1 << _TAG_PRIMARY_SIZE) | TAG_PRIMARY_IMMED1)
#define _TAG_IMMED1_IMMED2 ((0x2 << _TAG_PRIMARY_SIZE) | TAG_PRIMARY_IMMED1)
#define _TAG_IMMED1_SMALL ((0x3 << _TAG_PRIMARY_SIZE) | TAG_PRIMARY_IMMED1)
#define _TAG_IMMED2_SIZE 6
#define _TAG_IMMED2_MASK 0x3F
#define _TAG_IMMED2_ATOM ((0x0 << _TAG_IMMED1_SIZE) | _TAG_IMMED1_IMMED2)
#define _TAG_IMMED2_CATCH ((0x1 << _TAG_IMMED1_SIZE) | _TAG_IMMED1_IMMED2)
#define _TAG_IMMED2_NIL ((0x3 << _TAG_IMMED1_SIZE) | _TAG_IMMED1_IMMED2)
我们来简化一下代码,并将16进制数改为用2进制来表示,如下:
#define _TAG_PRIMARY_SIZE 2
#define _TAG_PRIMARY_MASK 11B
#define TAG_PRIMARY_HEADER 00B
#define TAG_PRIMARY_LIST 01B
#define TAG_PRIMARY_BOXED 10B
#define TAG_PRIMARY_IMMED1 11B
#define _TAG_IMMED1_SIZE 4
#define _TAG_IMMED1_MASK 1111B
#define _TAG_IMMED1_PID 0011B
#define _TAG_IMMED1_PORT 0111B
#define _TAG_IMMED1_IMMED2 1011B
#define _TAG_IMMED1_SMALL 1111B
#define _TAG_IMMED2_SIZE 6
#define _TAG_IMMED2_MASK 111111B
#define _TAG_IMMED2_ATOM 001011B
#define _TAG_IMMED2_CATCH 011011B
#define _TAG_IMMED2_NIL 111011B
由上可看出,对这些类型进行了分层,每2位就一个层次,分别为primary、immed1、immed2。
知道了这些定义,对于erl数据类型的内部实现就已清楚了大半,以下举例说明。
例1:判断Erlang数据是否为list的方法
如果要判断一个erl数据是否为list,只需要判断最后两位是否为01,可描述为:(var & 11B) == 01B现在我们来验证一下表1中的一组的数据。
-----------------
[1,2] -> 0x1B0BDE9
[3,4] -> 0x1B0BEA5
-----------------
1B0BDE9和1B0BEA5的最后一位用2进制来表示,分别为:1001B,0101B
可见最后两位都为01,即为TAG_PRIMARY_LIST的值。
例2:检测Erlang数据类型的方法
假设vm中收到一个值为0xFFFFFFFB的erl变量,要想知道这是一个什么数据,可做如下分析。0xFFFFFFFB的最后一个字节(0xFB)用2进制表示为:0x11111011B
首先判断TAG_PRIMARY,也就是最后两位,值为11B,对应了上面的定义TAG_PRIMARY_IMMED1。
目前只知道它是immed1类型, 要想进一步了解它是什么东西,只能继续判断前面两位(10B)。
现在知道最后四位为1011B,对应定义_TAG_IMMED1_IMMED2,即为immed2,仍然不明确它的类型,
只能再取前面两位(11B), 现在知道最后六位为111011B,对应_TAG_IMMED2_NIL,
说明它是一个nil数据,即为一个空的list(或者是list的末端,具体内容在以后文章中进一步阐述)。
例3:Erlang中的整型处理
在表1中:-----------------
1 -> 0x1F
2 -> 0x2F
-----------------
整数1和2封装成erl数据分别为1F和2F,从这里我们很容易知道它们的封装和解封取值的过程,
将1F解封取出整数值,只需要右移四位(1F >> 4)即可。
但是对于大整数,这方法就不可取了,如:
-----------------------------------
1234567890 -> 0x1B0A37A
1234567891 -> 0x1B0A3A2
-----------------------------------
以例2的方法,可知0x1B0A37A和0x1B0A3A2是BOXED类型,0x1B0A37A和0x1B0A3A2去掉tag的值后,其实就是一个erl内部的数据指针。
可见erl内部将整数分为小整型和大整型, 但对于erl程编来说,它是透明的。
例4:Erlang中的字符串实现方式
------------------------"ab" -> 0x1B0A19D
"cb" -> 0x1B0A1CD
------------------------
以例1的方法,可以知道0x1B0A19D和0x1B0A1CD均是list类型,说明erl中的字符串是用list实现的。
和例3中的大整数类似,0x1B0A19D和0x1B0A1CD去掉tag值以后,就是指向字符串的指针值。
在源码中有一段关于header的注释:
/*
* HEADER representation:
*
* aaaaaaaaaaaaaaaaaaaaaaaaaatttt00 arity:26, tag:4
*
* HEADER tags:
*
* 0000 ARITYVAL
* 0001 BINARY_AGGREGATE |
* 001x BIGNUM with sign bit |
* 0100 REF |
* 0101 FUN | THINGS
* 0110 FLONUM |
* 0111 EXPORT |
* 1000 REFC_BINARY | |
* 1001 HEAP_BINARY | BINARIES |
* 1010 SUB_BINARY | |
* 1011 Not used
* 1100 EXTERNAL_PID | |
* 1101 EXTERNAL_PORT | EXTERNAL THINGS |
* 1110 EXTERNAL_REF | |
* 1111 Not used
*
* COMMENTS:
*
* - The tag is zero for arityval and non-zero for thing headers。
* - A single bit differentiates between positive and negative bignums。
* - If more tags are needed, the REF and and EXTERNAL_REF tags could probably
* be combined to one tag。
*
* XXX: globally replace XXX_SUBTAG with TAG_HEADER_XXX
*/
在前面例子中我们可以知道,在vm中,像大整数、tuple等类似的数据在传递时,只是传递了一个被封装过的类型为boxed的指针值,要想进一步了解指针所指的内容结构,
首先要取出内容中的开头的4个字节,即为header值。
现在我们假设有一个header值为0xC0(11000000B),从上面注释中可以得知,尾部为000000B的header对应的header tag为arityval,
关于header更具体的说明,还要参阅以下代码及注释。
#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 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 */
#define _TAG_HEADER_ARITYVAL (TAG_PRIMARY_HEADER|ARITYVAL_SUBTAG)
#define _TAG_HEADER_FUN (TAG_PRIMARY_HEADER|FUN_SUBTAG)
#define _TAG_HEADER_POS_BIG (TAG_PRIMARY_HEADER|POS_BIG_SUBTAG)
#define _TAG_HEADER_NEG_BIG (TAG_PRIMARY_HEADER|NEG_BIG_SUBTAG)
#define _TAG_HEADER_FLOAT (TAG_PRIMARY_HEADER|FLOAT_SUBTAG)
#define _TAG_HEADER_EXPORT (TAG_PRIMARY_HEADER|EXPORT_SUBTAG)
#define _TAG_HEADER_REF (TAG_PRIMARY_HEADER|REF_SUBTAG)
#define _TAG_HEADER_REFC_BIN (TAG_PRIMARY_HEADER|REFC_BINARY_SUBTAG)
#define _TAG_HEADER_HEAP_BIN (TAG_PRIMARY_HEADER|HEAP_BINARY_SUBTAG)
#define _TAG_HEADER_SUB_BIN (TAG_PRIMARY_HEADER|SUB_BINARY_SUBTAG)
#define _TAG_HEADER_EXTERNAL_PID (TAG_PRIMARY_HEADER|EXTERNAL_PID_SUBTAG)
#define _TAG_HEADER_EXTERNAL_PORT (TAG_PRIMARY_HEADER|EXTERNAL_PORT_SUBTAG)
#define _TAG_HEADER_EXTERNAL_REF (TAG_PRIMARY_HEADER|EXTERNAL_REF_SUBTAG)
#define _TAG_HEADER_BIN_MATCHSTATE (TAG_PRIMARY_HEADER|BIN_MATCHSTATE_SUBTAG)
#define _TAG_HEADER_MASK 0x3F
#define _HEADER_SUBTAG_MASK 0x3C /* 4 bits for subtag */
#define _HEADER_ARITY_OFFS 6
从上面代码的第一行可以得知,header值为11000000B的数据是一个tuple,它的长度为11000000B >> 6 == 11B,即是一个长度为3的tuple。
END,此文可作为阅读Erlang C源码的入门知识,对于erl编程和nif的开发及理解也有一定帮助,关于其它数据类型及数据结构的具体实现,有兴趣的同学可以参阅$ERL_TOP/erts/emulator/beam/erl_term.h