Erlang条款

1.匿名func曾经很慢
不过这都成为历史,现在func的花费在local func和apply之间。

2.list的comprehensions 慢
由func实现,由于之前func很慢,so....,不过现在改成了递归实现,所以。。。
不过尾递归+reverse还是会快一点的。

3.尾递归比普通递归要快
普通递归函数除了不断拷贝堆栈垃圾之外还要不断gc,so...
而尾递归不同,由头至尾重复使用。。
不过在erlang的后续版本,对此进行了优化,防止了stack的不断拷贝,以至于两者使用相同的mem,
那尾递归是否快于普通递归?用big list做了测试
x86中 快了30%
unix中 普通递归稍微快了小小

但是有一点可以很清楚的就是,如果不用在尾递归最后调用reverse,那可以保证,尾递归肯定比普通
递归要快。


4.'++'运算符
它有一个不好的名声,但是也要基于coder怎么来用,比如
naive_reverse([H|T]) ->
    naive_reverse(T)++[H];
naive_reverse([]) ->
    [].
这样做是非常低效的,导致了列表的不断不断的复制,导致了二次复杂性。

然而
naive_but_ok_reverse([H|T], Acc) ->
    naive_but_ok_reverse(T, [H]++Acc);
naive_but_ok_reverse([], Acc) ->
    Acc.
并不坏,acc作为++的操作符,只复制一次,但是最好的写法应该是
vanilla_reverse([H|T], Acc) ->
    vanilla_reverse(T, [H|Acc]);
vanilla_reverse([], Acc) ->
    Acc.


5.string模块非常慢
string模块使用的不当的话,会非常慢。另外最好使用re模块来代替regxp模块


6.dets的repair非常慢
根据record不同不而不同,虽然改进了,但是还是慢

7.beam是基于堆栈的字节流虚拟机运行
因此,比较慢,erts有1024个reg。每个指令字会转成对应的c 代码来执行,所以调度很快。
堆栈用于存放临时变量&&func call的时候参数

8.'_'变量
如果不适用的变量,可以使用'_'来提升你程序的运行速度。不过现在的版本,已经不再这样。


9.尽量少使用timer模块
使用erlang的timer函数来代替,因为timer模块使用了一个进程来管理,导致当并发大的时候,总会
出现一些异常的情况

10.list_to_atom
erlang当中atom并不回收的,而atom的默认最大值是1048576。若是不注意,将会导致atom的损耗尽,
特别是基于拒绝服务攻击的时候,这个时候可以用list_to_existing_atom来调整
另外,使用的时候最好别这样使用
apply(list_to_atom("some_prefix"++Var), foo, Args)
这是非常低效率的


11.lenth
length是bif,所以它的效率毋庸置疑的,不过有些时候为了代码外观可以这样做
foo(L) when length(L) >= 3 ->
...

改成
foo([_,_,_|_]=L) ->
...


12. setelement函数
每次修改都会copy一份,所以如果要修改一份大的tuple的时候,最好就将tuple转成一个list
然后去操作list,再转成相应的tuple即可。


13.size函数
size函数可以返回binary&tuple的大小,不过最好使用对应的bif ,tuple_size & byte_szie,
因为这样有助于解析器帮你发现bug,以及编译器的优化。

14.split_binary函数
最好直接匹配实现,少用此函数,太过昂贵。
<<Bin1:Num/binary,Bin2/binary>> = Bin,
而不是这样:
{Bin1,Bin2} = split_binary(Bin, Num)

15.'--'操作符
比如
HugeList1 -- HugeList2
非常缓慢,最好的代替方法:
HugeSet1 = ordsets:from_list(HugeList1),
HugeSet2 = ordsets:from_list(HugeList2),
ordsets:subtract(HugeSet1, HugeSet2)

不过基于ord的话可能会导致原来的顺序打乱,so...
Set = gb_sets:from_list(HugeList2),
[E || E <- HugeList1, not gb_sets:is_element(E, Set)]

当然,你使用'--'操作符删除列表的一个元素,并不会有什么性能问题
HugeList1 -- [Element]


16.大话binary的construction 和match
在r12b以后的版本,构造和匹配binary都做的优化,比之前的版本快了很多
比如
my_list_to_binary(List) ->
    my_list_to_binary(List, <<>>).

my_list_to_binary([H|T], Acc) ->
    my_list_to_binary(T, <<Acc/binary,H>>);
my_list_to_binary([], Acc) ->
    Acc
要是以前的版本,acc将在每次迭代的时候复制一份,但是在r12b版本,acc将只复制一份,而且
会另外一份额外的mem,因此在next迭代的时候H就会写到对应的额外mem,如果mem不够用的时候,
binary将会从小分配一份大的mem。
因此现在的binary的匹配也是很快的:
my_binary_to_list(<<H,T/binary>>) ->
    [H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].

17.binary
binary和bitstring都是使用相同的方式实现的,erts内部可以统称为binarys。
有4种binary,其中两种是容器,另外两种是binary data 的部分引用(块引用)。

前者两种可以称为:引用计数binary(refc-binary) && 堆binary(heap binary)

refc-binary有两部分组成
  a.procbin--存放在进程堆
  b.存储在外部的binary object
后者采用了引用计数,具体如何实现,想必都很清楚了,每个procbin使用binary object时,都会使
计数+1,反之则-1,到refc==0的时候,就会被gc。

Heap binaries 是小的二进制文件,最多64个字节,直接存储在该进程的堆。proc死前回收时或者sent msg的时候,
他们将会被copy,因此不需特殊的gc。


有两种refc object,可以用来引用部分refc binary 和 heap binary的内容。
分别为match contexts && sub binaries
sub binaries 由split_binary/2 函数 && match pattern而产生。
它只是一个标记,而不会进行数据的复制,所以不会昂贵。

match contexts  类似前者,但是是经过优化的,它直接通过指针指向binary data。对于每块binaries的match
将会导致match context对应位置的递增。

在r11b中,match contexts只用在binary match中
在r12b中,编译器为了避免产生多余代码,而创建一个sub binaries,若是需要创建match contexts时,
则会删除sub binaries ,保留后者。



18.构造binaries
r12b中:
<<Binary/binary, ...>>
<<Binary/bitstring, ...>>

这样erts将会对此进行优化,是runtime 进行优化而不是编译器。
但是有些情况下,优化不会发生的:
how it works?
接下来解析binaries如何运行的?
Bin0 = <<0>>, 对变量Bin0一个heap binary

Bin1 = <<Bin0/binary,1,2,3>>,是一个append操作,由于Bin0没有涉及到这个append操作,
则会创建一个refc binary 而且Bin0的内容将会拷贝到对应mem上。而ProcBin部分也会被设置为对应大小。

Bin2 = <<Bin1/binary,4,5,6>>,此时Bin1有一个append的操作,追加操作将会保存在额外空间里面。
Bin3 = <<Bin2/binary,7,8,9>>,同上面一直,会保存在额外空间里面。

然而
Bin4 = <<Bin1/binary,17>>,这里会产生有趣的事情,编译器会充pre的append操作获取到Bin1,并且
创建一个新的refc binary。
可以参考源码erl_bits.c

强制复制的情况:
由于procbin和binary obejct是分开的,对于一对一的时候,append操作会导致mem的realloc以及procbin本身
的update,但是多对一的时候,此操作将会变得困难。
这会导致强制复制的产生。

因此,比如
Bin = <<Bin0,...>>
如果Bin0是来自最新的append,否则将会产生新的binary object。

另外,对于一些特殊情况,也会导致binary的copy
比如当做消息发送给一个proc或者port,将会导致binary的shunrk,
任何的其他append操作都会产生copy,产生新的binary。
Bin1 = <<Bin0,...>>,
PortOrPid ! Bin1,
Bin = <<Bin1,...>>

或者你将binary 插入到ets or 使用erlang:port_command/2 发送到一个port
或者使用了match
Bin1 = <<Bin0,...>>,
<<X,Y,Z,T/binary>> = Bin1,
Bin = <<Bin1,...>> %%为什么会复制了,因为match contexts包含了直接指向data的指针

以上的情况都会导致Bin1的copy。




19.再谈binary match
上面谈过
my_binary_to_list(<<H,T/binary>>) ->
    [H|my_binary_to_list(T)];
my_binary_to_list(<<>>) -> [].

分析下,究竟发生了什么?

当函数第一次调用时,产生一个match contexts,并且指向了第一个byte。
H将会匹配第一个byte,导致match..的update,更新point指向到第二个byte。
并且match...只创建一次,到迭代完毕,而在r11b中,则完全不同,首先会创建一个sub...
然后接着创建一个sub..的match..,而且每一次的迭代都会创建一对新的,so......


再看
my_complicated_binary_to_list(Bin) ->
    my_complicated_binary_to_list(Bin, 0).

my_complicated_binary_to_list(Bin, Skip) ->
    case Bin of
    <<_:Skip/binary,Byte,_/binary>> ->
        [Byte|my_complicated_binary_to_list(Bin, Skip+1)];
    <<_:Skip/binary>> ->
        []
    end.

此函数可以避免重复创建sub....,但是将会导致无论在r11b或者r12b中,都会创建N+1次的match...
不过不排除编译器会优化。

每次递归完毕之后,match..将会丢弃,但是如果尚未递归完就停止了呢,那会是什么情况?

after_zero(<<0,T/binary>>) ->
    T;
after_zero(<<_,T/binary>>) ->
    after_zero(T);
after_zero(<<>>) ->
    <<>>.

使用bin_opt_info选项来进行优化。
erlc +bin_opt_info Mod.erl
export ERL_COMPILER_OPTIONS=bin_opt_info




20 大话list
使用++的话只能从前端后端加入数据。
现在看看append的实现
append([H|T], Tail) ->
    [H|append(T, Tail)];
append([], Tail) ->
    Tail.
将会导致一次的复制,很明显复制了前者。
这证明了什么?告诉我们当我们递归or创建一个list的时候,确保你加入新的元素
是在前端,这样你是在构造一个新list,否则你是在种种list复制
比如
bad_fib(N) ->
    bad_fib(N, 0, 1, []).
bad_fib(0, _Current, _Next, Fibs) ->
    Fibs;
bad_fib(N, Current, Next, Fibs) ->
    bad_fib(N - 1, Next, Current + Next, Fibs ++ [Current]).
将会导致种种的复制
正确的做法是:
tail_recursive_fib(N) ->
    tail_recursive_fib(N, 0, 1, []).
tail_recursive_fib(0, _Current, _Next, Fibs) ->
    lists:reverse(Fibs);
tail_recursive_fib(N, Current, Next, Fibs) ->
    tail_recursive_fib(N - 1, Next, Current + Next, [Current|Fibs]).

列表解析:
列表解析一个字慢。它内部解析为内部func实现,比如
[Expr(E) || E <- List]
解析为:
'lc^0'([E|Tail], Expr) ->
    [Expr(E)|'lc^0'(Tail, Expr)];
'lc^0'([], _Expr) -> [].

看到了木有,创建了一个list

但是并不是一定的,比如:
[io:put_chars(E) || E <- List],
ok.
或者
case Var of
    ... ->
        [io:put_chars(E) || E <- List];
    ... ->
end,
some_function(...),
...

编译器会发现不用创建一个list,so....
解析为
'lc^0'([E|Tail], Expr) ->
    Expr(E),
    'lc^0'(Tail, Expr);
'lc^0'([], _Expr) -> [].


lists:flatten/1 将会重新构造一个新的list,毋庸置疑是昂贵的。
甚至比运算符'++'更昂贵。
所以尽量避免使用
下面几种情况可以避免使用它:
a.发送list到port,因为port会自动解析
b.使用bif函数的时候,比如list_to_binary/iolist_to_binary
c.使用append来代替。

正确:
port_command(Port, DeepList)
错误:
port_command(Port, lists:flatten(DeepList))

正确:
TerminatedStr = [String, 0],
port_command(Port, TerminatedStr)

错误:
TerminatedStr = String ++ [0],
port_command(Port, TerminatedStr)

正确:
lists:append([[1], [2], [3]]).
错误:
lists:flatten([[1], [2], [3]]).

再谈尾递归和普通递归
尾递归在固定的空间里面构造,而普通递归则使用与list长度成比例的stack。
正确:
sum(L) -> sum(L, 0).
sum([H|T], Sum) -> sum(T, Sum + H);
sum([], Sum)    -> Sum.

错误:
recursive_sum([H|T]) -> H+recursive_sum(T);
recursive_sum([])    -> 0.



21 大话fun
函数匹配,主要是针对编译器的优化,让编译器自动重排
错误写法:
atom_map1(one) -> 1;
atom_map1(two) -> 2;
atom_map1(three) -> 3;
atom_map1(Int) when is_integer(Int) -> Int;
atom_map1(four) -> 4;
atom_map1(five) -> 5;
atom_map1(six) -> 6.


正确:
atom_map2(one) -> 1;
atom_map2(two) -> 2;
atom_map2(three) -> 3;
atom_map2(four) -> 4;
atom_map2(five) -> 5;
atom_map2(six) -> 6;
atom_map2(Int) when is_integer(Int) -> Int.

or

atom_map3(Int) when is_integer(Int) -> Int;
atom_map3(one) -> 1;
atom_map3(two) -> 2;
atom_map3(three) -> 3;
atom_map3(four) -> 4;
atom_map3(five) -> 5;
atom_map3(six) -> 6.


错误:
map_pairs1(_Map, [], Ys) ->
    Ys;
map_pairs1(_Map, Xs, [] ) ->
    Xs;
map_pairs1(Map, [X|Xs], [Y|Ys]) ->
    [Map(X, Y)|map_pairs1(Map, Xs, Ys)].

正确:
map_pairs2(_Map, [], Ys) ->
    Ys;
map_pairs2(_Map, [_|_]=Xs, [] ) ->
    Xs;
map_pairs2(Map, [X|Xs], [Y|Ys]) ->
    [Map(X, Y)|map_pairs2(Map, Xs, Ys)].

缘由很简单,由于第二参数任意匹配参数,所以编译器无法自动重排。



函数调用的效率
a.本地函数外部函数效率最快(local function)
b.匿名函数Fun()以及apply(Fun,[])调用,大概是前两者的3倍时间。
c.执行mod:name或者apply(mod,fun,[])大概是local function的6倍

执行local函数,不用hash lookup,直接是func pointer
而apply/3则需要查找hash table
其实
Module:Name(A)和apply(Module,Name,[A])都是一样的
最终编译器会重写。

apply(Module, Function, Arguments)会稍微慢点,因为参数在编译器并不可知


递归函数的内存使用
使用递归函数的时候,最好是使用尾递归函数,因为在固定的mem中执行。
list_length(List) ->
    list_length(List, 0).
list_length([], AccLen) ->
    AccLen;
list_length([_|Tail], AccLen) ->
    list_length(Tail, AccLen + 1).


22 进程
erlang的进程是轻量级的,正常情况下是309字大小
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<...>
2> {_,Bytes} = process_info(spawn(Fun), memory).
{memory,1232}
3> Bytes div erlang:system_info(wordsize).
309

进程里面的循环必须是尾调用
正确:
loop() ->
      receive
         {sys, Msg} ->
            handle_sys_msg(Msg),
            loop();
         {From, Msg} ->
            Reply = handle_msg(Msg),
            From ! Reply,
            loop()
    end.

错误:
loop() ->
  receive
     {sys, Msg} ->
         handle_sys_msg(Msg),
         loop();
     {From, Msg} ->
          Reply = handle_msg(Msg),
          From ! Reply,
          loop()
  end,
  io:format("Message is processed~n", []).

进程默认heap size 是233字
可以通过erl +h来调整或者
spawn_opt/4函数min_heap_size 选项
对于一些临时task,我们可以spawn一个进程来处理。

进程之间的消息的data都是进行复制的,除了同一节点下refc binaries
如果发送到别的节点,则首先会encode,然后通过tcp/ip,然后对于的node
会进行decode,然后派发到对应的进程。

erlang下的常量--字符量,会保存在constant pool里面,
每个module都有它自己的constant pool
days_in_month(M) ->
    element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).

但是如果将constant发送给其他的proc,或者保存到ets里面,那将会产生复制。
若是一个module给unload的话,那refer了对应constant的proc将会产生复制。



23 高级话题
erlang中不同类型所占的mem大小
Small integer    1字  
32bit 使用了28bit  -134217729 < i < 134217728 (28 bits)
64bit 使用了60bit -576460752303423489 < i < 576460752303423488 (60 bits)

Big integer   3..N words

Atom 1字 atom保存在atom table中,并且不gc

Float
32-bit 4 字
64-bit 3 字

Binary 3..6 + data (共享的data)

List  1字 + element*size *word

String 1 字+ 2 字每个字符

Tuple 2字+ element*2word

Pid  1字 identity + 5字 node + 指向其他元素需要消耗内存

port 和pid一样

Reference 5字 identity + 7字 node + 指向其他元素所需要消耗内存

Fun      9..13 字 + 环境 + fun table

Ets table  初始化是768字节,自动递增

Erlang process 默认327 包含了233 heap


一些限制:
默认最大proc 的数量:32768
可以通过erl +P 设置,最大可以是268435456

atom的长度 255个字符
atom默认最大限制 1048576
erl +t 可以设置

ets 默认1400
ERL_MAX_ETS_TABLES 可以设置

....

具体可以参考erl


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值