Erlang二进制创建的内部机制和优化(二)

Erlang二进制创建的内部机制和优化(一)

这一节以实例分析的方式继续探索二进制创建的内部机制。

为了验证上一节的内容,首先在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>> 这一句的执行过程如下:


先创建一个大小为可以容器Bin1的heap binary:
// 节选自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连续优化的操作,出现了不是我们预期的结果。

小结
  1. 在Erlang编程中,我们要了解Binary二进制的创建场情是否会发挥append的优化特性,特别是在网络编程中的收包解包,要充分利用这一特性以提高效率。
  2. 编译器会对erl代码进行优化,测试时要注意这种优化是否会影响测试结果。
  3. Erlang Shell中执行代码时,要了解它的执程流程及与文件代码的区别,以免出现莫名的情况。
  4. erl代码可以通过erlc +\'E\' file.erl和erlc +\'S\' file.erl生成代码的扩展文件和指令文件,观察程序执行过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值