为什么映射的查找属性比Erlang中的记录慢?

为什么映射的查找属性比Erlang中的记录慢?,erlang,Erlang,我正在读编程Erlang,在书的第5章,它说: 记录只是伪装的元组,因此它们具有相同的存储和性能 作为元组的特征。映射比元组使用更多的存储空间,并且具有 较慢的查找属性 在我以前学过的语言中,情况并非如此。映射通常实现为哈希表,因此查找时间复杂度为O(1);记录(带名称的元组)通常实现为不可变列表,查找时间复杂度为O(N) 在Erlang中实现这些数据结构有什么不同?对于少量字段,记录查找和映射查找之间没有实际的性能差异。但是,对于大量字段,确实存在,因为记录信息在编译时已知,而映射键则不需要,

我正在读编程Erlang,在书的第5章,它说:

记录只是伪装的元组,因此它们具有相同的存储和性能 作为元组的特征。映射比元组使用更多的存储空间,并且具有 较慢的查找属性

在我以前学过的语言中,情况并非如此。映射通常实现为哈希表,因此查找时间复杂度为
O(1)
;记录(带名称的元组)通常实现为不可变列表,查找时间复杂度为
O(N)


在Erlang中实现这些数据结构有什么不同?

对于少量字段,记录查找和映射查找之间没有实际的性能差异。但是,对于大量字段,确实存在,因为记录信息在编译时已知,而映射键则不需要,所以映射使用的查找机制与记录不同。但是记录和地图并不是作为可互换的替代品,所以在IMO中,将它们与只涉及少量字段的用例进行比较是毫无意义的;如果您知道编译时需要的字段,请使用记录,如果不知道,请使用映射或其他类似机制。因此,以下仅关注查找一个记录字段和一个映射键的性能差异

让我们看看汇编程序中的两个函数,一个访问记录字段,另一个访问映射键。以下是功能:

-record(foo, {f}).

r(#foo{f=X}) ->
    X.

m(#{f := X}) ->
    X.
两者都使用模式匹配从给定的类型实例中提取值

这是
r/1
的组件:

{function, r, 1, 2}.
  {label,1}.
    {line,[{location,"f2.erl",6}]}.
    {func_info,{atom,f2},{atom,r},1}.
  {label,2}.
    {test,is_tuple,{f,1},[{x,0}]}.
    {test,test_arity,{f,1},[{x,0},2]}.
    {get_tuple_element,{x,0},0,{x,1}}.
    {get_tuple_element,{x,0},1,{x,2}}.
    {test,is_eq_exact,{f,1},[{x,1},{atom,foo}]}.
    {move,{x,2},{x,0}}.
    return.
这里有趣的部分从
{label,2}
开始。代码验证参数是否为元组,然后验证元组的arity,并从中提取两个元素。在验证元组的第一个元素是否等于原子
foo
之后,它返回第二个元素的值,即记录字段
f

现在让我们看一下
m/1
函数的组装:

{function, m, 1, 4}.
  {label,3}.
    {line,[{location,"f2.erl",9}]}.
    {func_info,{atom,f2},{atom,m},1}.
  {label,4}.
    {test,is_map,{f,3},[{x,0}]}.
    {get_map_elements,{f,3},{x,0},{list,[{atom,f},{x,0}]}}.
    return.
1> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
237559.02
2> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
235871.94
此代码验证参数是否为映射,然后提取与映射键
f
关联的值

每个功能的成本归结为装配说明的成本。record函数有更多的指令,但它们可能比map函数中的指令便宜,因为所有记录信息在编译时都是已知的。当map的键计数增加时尤其如此,因为这意味着
get\u map\u elements
调用可能需要遍历更多的map数据才能找到它要查找的内容

我们可以编写函数多次调用这些访问器,然后对新函数计时。下面是两组递归函数,它们调用访问器
N
次:

call_r(N) ->
    call_r(#foo{f=1},N).
call_r(_,0) ->
    ok;
call_r(F,N) ->
    1 = r(F),
    call_r(F,N-1).

call_m(N) ->
    call_m(#{f => 1},N).
call_m(_,0) ->
    ok;
call_m(M,N) ->
    1 = m(M),
    call_m(M,N-1).
我们可以使用
timer:tc/3
调用这些函数来检查每个函数的执行时间。让我们调用每一个一千万次,但这样做50次,取平均执行时间。首先,记录功能:

{function, m, 1, 4}.
  {label,3}.
    {line,[{location,"f2.erl",9}]}.
    {func_info,{atom,f2},{atom,m},1}.
  {label,4}.
    {test,is_map,{f,3},[{x,0}]}.
    {get_map_elements,{f,3},{x,0},{list,[{atom,f},{x,0}]}}.
    return.
1> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
237559.02
2> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
235871.94

1>列表:sum([element(1,timer:tc(f2,call_r,[10000000]))| | | | |列表:sum([element(1,timer:tc(f2,call_m,[10000000]))| |(我在使用Erlang/OTP 22[erts-10.6]重复测试时有不同的结果

r/1的反汇编代码不同:

记录查找速度快了1.5倍以上

{function, r, 1, 2}.
  {label,1}.
    {line,[{location,"f2.erl",9}]}.
    {func_info,{atom,f2},{atom,r},1}.
  {label,2}.
    {test,is_tagged_tuple,{f,1},[{x,0},2,{atom,foo}]}.
    {get_tuple_element,{x,0},1,{x,0}}.
    return.

{function, m, 1, 4}.
  {label,3}.
    {line,[{location,"f2.erl",12}]}.
    {func_info,{atom,f2},{atom,m},1}.
  {label,4}.
    {test,is_map,{f,3},[{x,0}]}.
    {get_map_elements,{f,3},{x,0},{list,[{atom,f},{x,1}]}}.
    {move,{x,1},{x,0}}.
    return.


9> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
234309.04
10> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
341411.9

After I declared -compile({inline, [r/1, m/1]}).

13> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
199978.9
14> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
356002.48
-module(f22).

-compile({inline, [r/1, m/1]}).

-export([call_r/1, call_r/2, call_m/1, call_m/2]).

-define(I, '2').
-define(V,  2 ).

-record(foo, {
    '1',
    '2',
    '3',
    '4',
    '5',
    '6',
    '7',
    '8',
    '9',
    '0'
    }).

r(#foo{?I = X}) ->
    X.

m(#{?I := X}) ->
    X.

call_r(N) ->
    call_r(#foo{
    '1' = 1,
    '2' = 2,
    '3' = 3,
    '4' = 4,
    '5' = 5,
    '6' = 6,
    '7' = 7,
    '8' = 8,
    '9' = 9,
    '0' = 0
    }, N).
call_r(_,0) ->
    ok;
call_r(F,N) ->
    ?V = r(F),
    call_r(F,N-1).

call_m(N) ->
    call_m(#{
    '1' => 1,
    '2' => 2,
    '3' => 3,
    '4' => 4,
    '5' => 5,
    '6' => 6,
    '7' => 7,
    '8' => 8,
    '9' => 9,
    '0' => 0
    }, N).
call_m(_,0) ->
    ok;
call_m(F,N) ->
    ?V = m(F),
    call_m(F,N-1).


% lists:sum([element(1,timer:tc(f22,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
% 229777.3
% lists:sum([element(1,timer:tc(f22,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
% 395897.68

% After declaring 
% -compile({inline, [r/1, m/1]}).
% lists:sum([element(1,timer:tc(f22,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
% 130859.98
% lists:sum([element(1,timer:tc(f22,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
% 306490.6
% 306490.6 / 130859.98 .
% 2.34212629407401
{函数,r,1,2}。
{标签,1}。
{line,[{location,“f2.erl”,9}]}。
{func_info,{atom,f2},{atom,r},1}。
{标签,2}。
{test,{f,1},[{x,0},2,{atom,foo}]}的元组。
{get_tuple_元素,{x,0},1,{x,0}。
返回。
{函数,m,1,4}。
{标签,3}。
{line,[{location,“f2.erl”,12}]}。
{func_info,{atom,f2},{atom,m},1}。
{标签,4}。
{test,是_映射,{f,3},[{x,0}]}。
{get_map_元素,{f,3},{x,0},{list,[{atom,f},{x,1}]}。
{move,{x,1},{x,0}。
返回。

9> 列表:sum([element(1,timer:tc(f2,call_r,[10000000])))|列表:sum([element(1,timer:tc(f2,call_m,[10000000]))|列表:sum([element(1,timer:tc(f2,call_r,[10000000])))|列表:sum([element(1,timer:tc(f2,call___m,[10000000]))| | |我将记录与10个元素进行了比较,得出了相同大小的地图。在这种情况下,记录的速度被证明快了两倍多

{function, r, 1, 2}.
  {label,1}.
    {line,[{location,"f2.erl",9}]}.
    {func_info,{atom,f2},{atom,r},1}.
  {label,2}.
    {test,is_tagged_tuple,{f,1},[{x,0},2,{atom,foo}]}.
    {get_tuple_element,{x,0},1,{x,0}}.
    return.

{function, m, 1, 4}.
  {label,3}.
    {line,[{location,"f2.erl",12}]}.
    {func_info,{atom,f2},{atom,m},1}.
  {label,4}.
    {test,is_map,{f,3},[{x,0}]}.
    {get_map_elements,{f,3},{x,0},{list,[{atom,f},{x,1}]}}.
    {move,{x,1},{x,0}}.
    return.


9> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
234309.04
10> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
341411.9

After I declared -compile({inline, [r/1, m/1]}).

13> lists:sum([element(1,timer:tc(f2,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
199978.9
14> lists:sum([element(1,timer:tc(f2,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
356002.48
-module(f22).

-compile({inline, [r/1, m/1]}).

-export([call_r/1, call_r/2, call_m/1, call_m/2]).

-define(I, '2').
-define(V,  2 ).

-record(foo, {
    '1',
    '2',
    '3',
    '4',
    '5',
    '6',
    '7',
    '8',
    '9',
    '0'
    }).

r(#foo{?I = X}) ->
    X.

m(#{?I := X}) ->
    X.

call_r(N) ->
    call_r(#foo{
    '1' = 1,
    '2' = 2,
    '3' = 3,
    '4' = 4,
    '5' = 5,
    '6' = 6,
    '7' = 7,
    '8' = 8,
    '9' = 9,
    '0' = 0
    }, N).
call_r(_,0) ->
    ok;
call_r(F,N) ->
    ?V = r(F),
    call_r(F,N-1).

call_m(N) ->
    call_m(#{
    '1' => 1,
    '2' => 2,
    '3' => 3,
    '4' => 4,
    '5' => 5,
    '6' => 6,
    '7' => 7,
    '8' => 8,
    '9' => 9,
    '0' => 0
    }, N).
call_m(_,0) ->
    ok;
call_m(F,N) ->
    ?V = m(F),
    call_m(F,N-1).


% lists:sum([element(1,timer:tc(f22,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
% 229777.3
% lists:sum([element(1,timer:tc(f22,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
% 395897.68

% After declaring 
% -compile({inline, [r/1, m/1]}).
% lists:sum([element(1,timer:tc(f22,call_r,[10000000])) || _ <- lists:seq(1,50)])/50.
% 130859.98
% lists:sum([element(1,timer:tc(f22,call_m,[10000000])) || _ <- lists:seq(1,50)])/50.
% 306490.6
% 306490.6 / 130859.98 .
% 2.34212629407401
-模块(f22)。
-编译({inline[r/1,m/1]})。
-导出([调用r/1、调用r/2、调用m/1、调用m/2])。
-定义(I,'2')。
-定义(V,2)。
-记录(foo{
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'0'
}).
r(#foo{?I=X})->
十,。
m(#{?I:=X})->
十,。
呼叫\u r(N)->
打电话给#r(#foo{
'1' = 1,
'2' = 2,
'3' = 3,
'4' = 4,
'5' = 5,
'6' = 6,
'7' = 7,
'8' = 8,
'9' = 9,
'0' = 0
},N)。
调用\r(\u0)->
好啊
呼叫\u r(F,N)->
?V=r(F),
呼叫_r(F,N-1)。
呼叫\u m(N)->
打电话给我(#{
'1' => 1,
'2' => 2,
'3' => 3,
'4' => 4,
'5' => 5,
'6' => 6,
'7' => 7,
'8' => 8,
'9' => 9,
'0' => 0
},N)。
调用\u m(\u,0)->
好啊
呼叫_m(F,N)->
?V=m(F),
呼叫_m(F,N-1)。

%列表:sum([元素(1,计时器:tc(f22,调用[10000000]))| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Geezus.回答case,因为实际上记录不是这样使用的。映射速度较慢。对于记录,您确切地知道要使用元组的哪个元素,这取决于编译时间,而对于映射,您必须搜索映射以找到键。无论使用什么算法实现映射,这都较慢。@对于一般情况,rvirding是,但增益我在回答中特别指出,它侧重于单场情况,着眼于实用性。记录和地图并不意味着完全可互换,