Erlang二进制创建的内部机制和优化(一)
这一节以实例分析的方式继续探索二进制创建的内部机制。
为了验证上一节的内容,首先在erl_bits.c中的erts_bs_appen函数里加入一些调试输出。
上面的三个测试结果,让我们产生了很多不解,下面就将这些迷团一一解开。
先将test.erl通过下面的命令编译成erlang 指令。
erlc +\'S\' test.erl
而test:t/4是通过参数动态创建的,从中可以看到它调用的函数及执行流程。
如果这时有别的ProcBin引用了这个binary,就可能会出问题,这也违反了变量不可变的原则,
在Erlang Shell中,测试代码可以写成这样:
小结
这一节以实例分析的方式继续探索二进制创建的内部机制。
为了验证上一节的内容,首先在erl_bits.c中的erts_bs_appen函数里加入一些调试输出。
void print_bin(char *title, unsigned char *data, int len)
{
Uint index = 0;
erts_fprintf(stderr,"%s {", title);
while(index < len){
if(index) erts_fprintf(stderr,",%u", data[index]);
else erts_fprintf(stderr,"%u", data[index]);
index++;
}
erts_fprintf(stderr, "} (len:%d)\n", len);
}
Eterm
erts_bs_append(Process* c_p, Eterm* reg, Uint live, Eterm build_size_term,
Uint extra_words, Uint unit)
{
Eterm bin; /* Given binary */
Eterm* ptr;
Eterm hdr;
ErlSubBin* sb;
ProcBin* pb;
Binary* binp;
Uint heap_need;
Uint build_size_in_bits;
Uint used_size_in_bits;
Uint unsigned_bits;
ERL_BITS_DEFINE_STATEP(c_p);
// 需要创建的二进制的位数: build_size_in_bits
if (is_small(build_size_term)) {
Sint signed_bits = signed_val(build_size_term);
if (signed_bits < 0) {
goto badarg;
}
build_size_in_bits = (Uint) signed_bits;
} else if (term_to_Uint(build_size_term, &unsigned_bits)) {
build_size_in_bits = unsigned_bits;
} else {
c_p->freason = unsigned_bits;
return THE_NON_VALUE;
}
// 测试输出
erts_fprintf(stderr,"*** append start *** "
"build_size_in_bits:%d, heap_top:%p\n",
build_size_in_bits, c_p->htop);
bin = reg[live];
if (!is_boxed(bin)) {
badarg:
c_p->freason = BADARG;
return THE_NON_VALUE;
}
ptr = boxed_val(bin);
// 取出二进制数据流中的header
hdr = *ptr;
if (!is_binary_header(hdr)) {
goto badarg;
}
// #MARK_A
if (hdr != HEADER_SUB_BIN) {
// 非子二进制,不可写
erts_fprintf(stderr, "not_sub_bin, not_writable, header:%X\n", hdr);
// if((hdr & _HEADER_SUBTAG_MASK) == HEAP_BINARY_SUBTAG){
// erts_fprintf(stderr, "be_heap_bin, not_writable\n", hdr);
// }else{
// erts_fprintf(stderr, "not_sub_bin, not_writable, header:%X\n", hdr);
// }
goto not_writable;
}
sb = (ErlSubBin *) ptr;
if (!sb->is_writable) {
// is_writable==0,不可写
erts_fprintf(stderr, "not_writable is_writable==0\n");
goto not_writable;
}
pb = (ProcBin *) boxed_val(sb->orig);
print_bin("pb->bytes:", (char *)pb->bytes, pb->size);
// 必须是refc binary
ASSERT(pb->thing_word == HEADER_PROC_BIN);
if ((pb->flags & PB_IS_WRITABLE) == 0) {
// 标明了不可写
erts_fprintf(stderr, "not_writable (pb->flags & PB_IS_WRITABLE) == 0\n");
goto not_writable;
}
/*
* OK, the binary is writable.
*/
// 测试输出
erts_fprintf(stderr, "writable\n");
erts_bin_offset = 8*sb->size + sb->bitsize;
if (unit > 1) {
if ((unit == 8 && (erts_bin_offset & 7) != 0) ||
(erts_bin_offset % unit) != 0) {
goto badarg;
}
}
used_size_in_bits = erts_bin_offset + build_size_in_bits;
// 原来的sub binary设为不可写,因为后继空间将要被写入数据
// #MARK_B
sb->is_writable = 0; /* Make sure that no one else can write. */
erts_fprintf(stderr, "pb->size: from %ld extend to %ld\n", pb->size, NBYTES(used_size_in_bits));
// 扩展到所需大小
pb->size = NBYTES(used_size_in_bits);
pb->flags |= PB_ACTIVE_WRITER;
/*
* Reallocate the binary if it is too small.
*/
binp = pb->val;
// 如果容器的空间不足,则重新分配容器大小到所需的二倍
if (binp->orig_size < pb->size) {
Uint new_size = 2*pb->size;
binp = erts_bin_realloc(binp, new_size);
binp->orig_size = new_size;
// 注意:重新分配空间以后,pb->val指针会被改变,
// 所以此用的binary不能被外部引用
// #MARK_C
pb->val = binp;
pb->bytes = (byte *) binp->orig_bytes;
}
erts_current_bin = pb->bytes;
// 测试输出
erts_fprintf(stderr, "Binary Size:%ld, Binary Refc:%ld\n", binp->orig_size, binp->refc);
print_bin("new pb->bytes:", (char *)pb->bytes, pb->size);
/*
* Allocate heap space and build a new sub binary.
*/
reg[live] = sb->orig;
heap_need = ERL_SUB_BIN_SIZE + extra_words;
if (c_p->stop - c_p->htop < heap_need) {
(void) erts_garbage_collect(c_p, heap_need, reg, live+1);
}
// 创建一个新的sub binary,指向原二进制的开头,
// 相比原来的sub binary,这里只是把空间大小扩展到所需值
sb = (ErlSubBin *) c_p->htop; // 从堆顶写入
// 进程堆顶上升ERL_SUB_BIN_SIZE(20)字节
c_p->htop += ERL_SUB_BIN_SIZE;
sb->thing_word = HEADER_SUB_BIN;
sb->size = BYTE_OFFSET(used_size_in_bits);
sb->bitsize = BIT_OFFSET(used_size_in_bits);
sb->offs = 0;
sb->bitoffs = 0;
// 最新的sub binary,设为可写
// 也就是说,在一系列的append操作中,只有最后一个sub binary是可写的
sb->is_writable = 1;
sb->orig = reg[live];
erts_fprintf(stderr, "--- new_sub_binary_ok --- new_heap_top:%p\n\n", c_p->htop);
return make_binary(sb);
/*
* The binary is not writable. We must create a new writable binary and
* copy the old contents of the binary.
*/
not_writable:
{
Uint used_size_in_bytes; /* Size of old binary + data to be built */
Uint bin_size;
Binary* bptr;
byte* src_bytes;
Uint bitoffs;
Uint bitsize;
Eterm* hp;
/*
* Allocate heap space.
*/
heap_need = PROC_BIN_SIZE + ERL_SUB_BIN_SIZE + extra_words;
if (c_p->stop - c_p->htop < heap_need) {
(void) erts_garbage_collect(c_p, heap_need, reg, live+1);
bin = reg[live];
}
hp = c_p->htop;
/*
* Calculate sizes. The size of the new binary, is the sum of the
* build size and the size of the old binary. Allow some room
* for growing.
*/
ERTS_GET_BINARY_BYTES(bin, src_bytes, bitoffs, bitsize);
erts_bin_offset = 8*binary_size(bin) + bitsize;
if (unit > 1) {
if ((unit == 8 && (erts_bin_offset & 7) != 0) ||
(erts_bin_offset % unit) != 0) {
goto badarg;
}
}
used_size_in_bits = erts_bin_offset + build_size_in_bits;
used_size_in_bytes = NBYTES(used_size_in_bits);
bin_size = 2*used_size_in_bytes;
// 至少256字节
bin_size = (bin_size < 256) ? 256 : bin_size;
/*
* Allocate the binary data struct itself.
*/
// 创建大小为所需空间的二倍的binary(最小值为256字节),
// 它作为一个容器,存储在进程堆以外,
// 进程堆里只存放引用这个binary的refc binary
bptr = erts_bin_nrml_alloc(bin_size);
bptr->flags = 0;
bptr->orig_size = bin_size;
erts_refc_init(&bptr->refc, 1);
erts_current_bin = (byte *) bptr->orig_bytes;
erts_fprintf(stderr, "bptr:%p, bin_size:%lu\n", bptr, bin_size);
/*
* Now allocate the ProcBin on the heap.
*/
// 创建refc binary,引用上面的binary, 并存储到进程堆
pb = (ProcBin *) hp;
hp += PROC_BIN_SIZE;
pb->thing_word = HEADER_PROC_BIN;
// 当前设置为实际所需的大小,以后的append操作可扩展
pb->size = used_size_in_bytes;
pb->next = MSO(c_p).first;
MSO(c_p).first = (struct erl_off_heap_header*)pb;
pb->val = bptr;
pb->bytes = (byte*) bptr->orig_bytes;
pb->flags = PB_IS_WRITABLE | PB_ACTIVE_WRITER;
OH_OVERHEAD(&(MSO(c_p)), pb->size / sizeof(Eterm));
/*
* Now allocate the sub binary and set its size to include the
* data about to be built.
*/
// 创建sub binary,引用上面的refc binary,并设置为所需大小
sb = (ErlSubBin *) hp;
hp += ERL_SUB_BIN_SIZE;
sb->thing_word = HEADER_SUB_BIN;
sb->size = BYTE_OFFSET(used_size_in_bits);
sb->bitsize = BIT_OFFSET(used_size_in_bits);
sb->offs = 0;
sb->bitoffs = 0;
sb->is_writable = 1;
sb->orig = make_binary(pb);
c_p->htop = hp;
/*
* Now copy the data into the binary.
*/
copy_binary_to_buffer(erts_current_bin, 0, src_bytes, bitoffs, erts_bin_offset);
// 为了方便测试,仅输出前20字节
print_bin("dst_bytes:", erts_current_bin, 20);
erts_fprintf(stderr,"-------- new_heap_top:%p --------\n\n", c_p->htop);
return make_binary(sb);
}
}
实例1:在Erlang Shell中演示erlang的binary append操作过程
修改完Erlang C源码后,编译并启动Erlang Shell,运行测试代码:Bin0 = <<1>>,
Bin1 = <<Bin0/binary,2>>,
Bin2 = <<Bin1/binary,3>>,
Bin3 = <<Bin1/binary,4>>,
{Bin2,Bin3}.
结果如下(#开头的表示注释):
Eshell V5.10.2 (abort with ^G)
1> Bin0 = <<1>>,
1> Bin1 = <<Bin0/binary,2>>,
1> Bin2 = <<Bin1/binary,3>>,
1> Bin3 = <<Bin1/binary,4>>,
1> {Bin2,Bin3}.
*** append start *** build_size_in_bits:8, heap_top:0x01b1aa24
not_sub_bin, not_writable, header:64
bptr:0x01c41d18, bin_size:256
dst_bytes: {0,0,196,1,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255} (len:20)
-------- new_heap_top:0x01b1aa50 --------
*** append start *** build_size_in_bits:8, heap_top:0x01b09090
not_sub_bin, not_writable, header:64
bptr:0x01c41d18, bin_size:256
dst_bytes: {1,0,196,1,255,255,15,15,15,15,15,15,15,15,15,15,15,15,15,15} (len:20)
-------- new_heap_top:0x01b090bc --------
*** append start *** build_size_in_bits:8, heap_top:0x01b09164
pb->bytes: {1} (len:1)
writable
pb->size: from 1 extend to 2
Binary Size:256, Binary Refc:1
new pb->bytes: {1,0} (len:2)
--- new_sub_binary_ok --- new_heap_top:0x01b09178
#上面两段是 Bin1 = <<Bin0/binary, 2>> 执行过程中调用append函数的输出,
#首先看到not_sub_bin,header为64表示它是一个heap binary,不可写。
#接着创建了一个容器binary,大小为256字节,并且复制了Bin0,
#dst_bytes第二字节以后都是可被写的空间,这里看到后面有内容是因为分配空间后没有清0
#申请了binary容器后,继续调用append进行扩展ProcBin的长度,从1扩展到2,用于容纳<<Bin0/binary,2>>
*** append start *** build_size_in_bits:16, heap_top:0x01b1326c
not_sub_bin, not_writable, header:64
bptr:0x01c41e38, bin_size:256
dst_bytes: {255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,170,170,170,170} (len:20)
-------- new_heap_top:0x01b13298 --------
*** append start *** build_size_in_bits:8, heap_top:0x01b13340
pb->bytes: {1,2} (len:2)
writable
pb->size: from 2 extend to 3
Binary Size:256, Binary Refc:1
new pb->bytes: {1,2,255} (len:3)
--- new_sub_binary_ok --- new_heap_top:0x01b13354
#上面两段是 Bin2 = <<Bin1/binary, 3>> 执行过程中调用append函数的输出,
#仍然看到Bin1被复制后再进行扩展到3字节,
#按上一节内容来说,这是不应该的,这里到底发生了什么?
*** append start *** build_size_in_bits:16, heap_top:0x025bbb58
not_sub_bin, not_writable, header:64
bptr:0x01c41d18, bin_size:256
dst_bytes: {1,2,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15} (len:20)
-------- new_heap_top:0x025bbb84 --------
*** append start *** build_size_in_bits:8, heap_top:0x025bbc2c
pb->bytes: {1,2} (len:2)
writable
pb->size: from 2 extend to 3
Binary Size:256, Binary Refc:1
new pb->bytes: {1,2,15} (len:3)
--- new_sub_binary_ok --- new_heap_top:0x025bbc40
#上面两段是 Bin3 = <<Bin1/binary, 4>> 执行过程中调用append函数的输出,
#这里的Bin1被复制是正常的,但是,引起复制的原因是not_sub_bin,仍然是heap binary,
#按上一节的内容,这里应该是: not_writable is_writable==0
#为什么Bin1是heap binary?
{<<1,2,3>>,<<1,2,4>>}
实例2:将代码写入文件中编译后测试erlang的binary append操作过程
test.erl-module(test).
-export([t/4]).
-export([t/0]).
t() ->
Bin0 = <<1>>,
Bin1 = <<Bin0/binary,2>>,
Bin2 = <<Bin1/binary,3>>,
Bin3 = <<Bin1/binary,4>>,
{Bin2,Bin3}.
t(A1, A2, A3, A4) ->
Bin0 = <<A1>>,
Bin1 = <<Bin0/binary,A2>>, %% append操作1:Bin0是heap binary,不可写
Bin2 = <<Bin1/binary,A3>>, %% append操作2:Bin1可写,并设置为以后不可写
Bin3 = <<Bin1/binary,A4>>, %% append操作3:Bin1不可再写
{Bin2,Bin3}.
先看看test:t/0
Eshell V5.10.2 (abort with ^G)
1> test:t().
{<<1,2,3>>,<<1,2,4>>}
发现没有调试输出,猜测是没有调用append函数。
继续看看test:t/4的结果。Eshell V5.10.2 (abort with ^G)
1> test:t(1,2,3,4).
*** append start *** build_size_in_bits:8, heap_top:0x01b09598
not_sub_bin, not_writable, header:A4
bptr:0x01c41220, bin_size:256
dst_bytes: {1,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170} (len:20)
-------- new_heap_top:0x01b095c4 --------
*** append start *** build_size_in_bits:8, heap_top:0x01b095c4
pb->bytes: {1,2} (len:2)
writable
pb->size: from 2 extend to 3
Binary Size:256, Binary Refc:1
new pb->bytes: {1,2,170} (len:3)
--- new_sub_binary_ok --- new_heap_top:0x01b095d8
*** append start *** build_size_in_bits:8, heap_top:0x01b095d8
not_writable is_writable==0
bptr:0x01c40040, bin_size:256
dst_bytes: {1,2,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170} (len:20)
-------- new_heap_top:0x01b09604 --------
{<<1,2,3>>,<<1,2,4>>}
上面三段输出刚好对应代码中的三个append操作,而且结果也和上一节所说的内容一致了。
上面的三个测试结果,让我们产生了很多不解,下面就将这些迷团一一解开。
先将test.erl通过下面的命令编译成erlang 指令。
erlc +\'S\' test.erl
test.S
{function, t, 0, 2}.
{label,1}.
{line,[{location,"test.erl",4}]}.
{func_info,{atom,test},{atom,t},0}.
{label,2}.
{move,{literal,{<<1,2,3>>,<<1,2,4>>}},{x,0}}.
return.
{function, t, 4, 4}.
{label,3}.
{line,[{location,"test.erl",11}]}.
{func_info,{atom,test},{atom,t},4}.
{label,4}.
{line,[{location,"test.erl",12}]}.
{bs_init2,{f,0},1,0,4,{field_flags,[]},{x,4}}.
{bs_put_integer,{f,0},{integer,8},1,{field_flags,[unsigned,big]},{x,0}}.
{bs_append,{f,0},{integer,8},0,4,8,{x,4},{field_flags,[]},{x,0}}.
{bs_put_integer,{f,0},{integer,8},1,{field_flags,[unsigned,big]},{x,1}}.
{bs_append,{f,0},{integer,8},0,4,8,{x,0},{field_flags,[]},{x,1}}.
{bs_put_integer,{f,0},{integer,8},1,{field_flags,[unsigned,big]},{x,2}}.
{bs_append,{f,0},{integer,8},3,4,8,{x,0},{field_flags,[]},{x,2}}.
{bs_put_integer,{f,0},{integer,8},1,{field_flags,[unsigned,big]},{x,3}}.
{put_tuple,2,{x,0}}.
{put,{x,1}}.
{put,{x,2}}.
return.
从上面可以看到,test:t/0已经被编译器处理成{<<1,2,3>>,<<1,2,4>>},故不会再调用append,
而test:t/4是通过参数动态创建的,从中可以看到它调用的函数及执行流程。
最不解的是在Erlang Shell中执行的结果,为什么和预期的相差这么远?
Erlang的二进制append操作过程中,由于ProcBin(见#MARK_C处)所引用的binary在扩展空间时可能会被移动,此时就必段更新ProcBin的引用指针(ProcBin->val),如果这时有别的ProcBin引用了这个binary,就可能会出问题,这也违反了变量不可变的原则,
所以,当append操作的过程中,如果出中间执行了一些会影响ProcBin的指令,sub binary就会被设置为以后不可再写。
- 当作结果返回
- 当作消息被发送(PortOrPid ! Bin1)
- 当作普通变量插入ETS表
- 当作普通变量进行二进制匹配操作
在Erlang Shell中,测试代码可以写成这样:
Bin0 = <<1>>.
Bin1 = <<Bin0/binary,2>>.
Bin2 = <<Bin1/binary,3>>.
Bin3 = <<Bin1/binary,4>>.
{Bin2,Bin3}.
这就说明,每一行都像一个函数一样,执行结果将会被返回保存。
例如,在Erlang Shell中,Bin3 = <<Bin1/binary,4>> 这一句的执行过程如下:
// 节选自beam_emu.c
do_bs_init_bits_known:
// 此处省略N行。。。
erts_bin_offset = 0;
erts_writable_bin = 0;
hb = (ErlHeapBin *) HTOP;
HTOP += heap_bin_size(num_bytes);
hb->thing_word = header_heap_bin(num_bytes);
hb->size = num_bytes;
erts_current_bin = (byte *) hb->data;
new_binary = make_binary(hb);
接着进行append操作,最后读取结果:
// 节选自beam_emu.c
do_bs_get_binary_all_reuse_common:
orig = mb->orig;
sb = (ErlSubBin *) boxed_val(context_to_binary_context);
hole_size = 1 + header_arity(sb->thing_word) - ERL_SUB_BIN_SIZE;
sb->thing_word = HEADER_SUB_BIN;
sb->size = BYTE_OFFSET(size);
sb->bitsize = BIT_OFFSET(size);
sb->offs = BYTE_OFFSET(offs);
sb->bitoffs = BIT_OFFSET(offs);
// 设为不可写
sb->is_writable = 0;
sb->orig = orig;
if (hole_size) {
sb[1].thing_word = make_pos_bignum_header(hole_size-1);
}
// ...
由于Erlang Shell中每一行的执行结果都会被返回保存,所以打断了append连续优化的操作,出现了不是我们预期的结果。
小结
- 在Erlang编程中,我们要了解Binary二进制的创建场情是否会发挥append的优化特性,特别是在网络编程中的收包解包,要充分利用这一特性以提高效率。
- 编译器会对erl代码进行优化,测试时要注意这种优化是否会影响测试结果。
- Erlang Shell中执行代码时,要了解它的执程流程及与文件代码的区别,以免出现莫名的情况。
- erl代码可以通过erlc +\'E\' file.erl和erlc +\'S\' file.erl生成代码的扩展文件和指令文件,观察程序执行过程。