((请原谅我在一个线程中问了多个问题。我认为它们是相关的。)
大家好,我想知道Erlang中关于每个模块预编译数据的最佳实践是什么。
示例:我有一个模块,它对先验已知的、非常复杂的正则表达式进行了大量操作。re:compile/2的文档中写道:"编译一次并执行多次比每次都编译要高效得多"。由于re的mp()数据类型没有指定,因此如果您想要一个目标独立梁,就不能在编译时放置,因此必须在运行时编译RegEx。(注意:re:compile/2只是一个例子。任何需要记忆的复杂函数都适合我的问题。)
Erlang的模块(可以)有一个-on_load(F/A)
属性,表示在加载模块时应该执行一次的方法。因此,我可以将要编译的正则表达式放在这个方法中,并将结果保存在一个名为?MODULE
的新ets表中。
丹回答后更新
我的问题是:
- 如果我正确理解ets,那么它的数据将保存在另一个进程中(与进程字典不同),并且为ets表检索值是非常昂贵的。(如果我错了,请证明我错了!)是否应该将ets中的内容复制到流程字典中以加速?(请记住:数据永远不会更新。)
- 将所有数据作为一个记录(而不是多个表项)放入ets/process字典是否存在(相当大的)缺点
工作示例:
-module(memoization).
-export([is_ipv4/1, fillCacheLoop/0]).
-record(?MODULE, { re_ipv4 = re_ipv4() }).
-on_load(fillCache/0).
fillCacheLoop() ->
receive
{ replace, NewData, Callback, Ref } ->
true = ets:insert(?MODULE, [{ data, {self(), NewData} }]),
Callback ! { on_load, Ref, ok },
?MODULE:fillCacheLoop();
purge ->
ok
end
.
fillCache() ->
Callback = self(),
Ref = make_ref(),
process_flag(trap_exit, true),
Pid = spawn_link(fun() ->
case catch ets:lookup(?MODULE, data) of
[{data, {TableOwner,_} }] ->
TableOwner ! { replace, #?MODULE{}, self(), Ref },
receive
{ on_load, Ref, Result } ->
Callback ! { on_load, Ref, Result }
end,
ok;
_ ->
?MODULE = ets:new(?MODULE, [named_table, {read_concurrency,true}]),
true = ets:insert_new(?MODULE, [{ data, {self(), #?MODULE{}} }]),
Callback ! { on_load, Ref, ok },
fillCacheLoop()
end
end),
receive
{ on_load, Ref, Result } ->
unlink(Pid),
Result;
{ 'EXIT', Pid, Result } ->
Result
after 1000 ->
error
end
.
is_ipv4(Addr) ->
Data = case get(?MODULE.data) of
undefined ->
[{data, {_,Result} }] = ets:lookup(?MODULE, data),
put(?MODULE.data, Result),
Result;
SomeDatum -> SomeDatum
end,
re:run(Addr, Data#?MODULE.re_ipv4)
.
re_ipv4() ->
{ok, Result} = re:compile("^0*"
"([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*"
"([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*"
"([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])\.0*"
"([1-9]?\d|1\d\d|2[0-4]\d|25[0-5])$"),
Result
.
您还有另一个选项。您可以预先计算正则表达式的编译形式并直接引用它。一种方法是使用专门为此目的设计的模块,例如ct_expand
:http://dukesoferl.blogspot.com/2009/08/metaprogramming-with-ctexpand.html
您也可以通过动态生成一个模块,并使用一个函数将该值作为常量返回(利用常量池)来滚动自己的值:http://erlang.org/pipermail/erlang-questions/2011-January/056007.html
或者您甚至可以在shell中运行如果实现发生变化,这将是不可移植的。re:compile
,并将结果复制粘贴到代码中。粗糙但有效
需要明确的是:所有这些都利用了常量池来避免每次重新计算。但当然,这增加了复杂性,也有成本。
回到你最初的问题:流程字典的问题是,它只能由自己的流程使用。你确定这个模块的函数只会被同一个进程调用吗?即使是ETS表也与创建它们的进程绑定在一起(不过,ETS本身并不是通过进程和消息传递来实现的),如果该进程失效,它就会失效。
ETS不是在进程中实现的,它的数据也不在单独的进程堆中,但它的数据在所有进程之外的单独区域中。这意味着,当读取/写入ETS表时,必须将数据复制到进程中/从进程中复制数据。当然,这样做的成本取决于要复制的数据量。这就是为什么我们有像ets:match_object
和ets:select
这样的函数,它们在复制数据之前允许更复杂的选择规则的原因之一。
将数据保存在ETS表中的一个好处是,所有流程都可以访问数据,而不仅仅是拥有该表的流程。这可以使它比将数据保存在服务器中更高效。它还取决于您希望对数据执行的操作类型。ETS只是一个数据存储,并提供有限的原子性。在你的情况下,这可能没有问题。
您绝对应该将数据保存在单独的记录中,每个不同的编译正则表达式都有一个记录,因为这将大大提高访问速度。然后你可以直接得到你想要的re,否则你会得到所有的re,然后在你想要的之后再次搜索。这就违背了将他们纳入ETS的意义。
虽然您可以在on_load
函数中创建ETS表,但对于ETS表来说,这不是一个好主意。这是因为ETS由一个进程所有,并且在进程终止时被删除。您永远不知道on_load
函数是在哪个过程中调用的。您还应该避免做可能需要很长时间的事情,因为模块在完成之前不会被认为是加载的。
生成一个解析转换以将编译re的结果直接静态插入到代码中是一个很酷的想法,尤其是如果re真的是静态定义的。正如动态生成、编译模块并将其加载到系统中的想法一样。同样,如果你的数据是静态的,你可以在编译时生成这个模块。
mochiglobal通过编译一个新模块来存储常量来实现这一点。这里的优点是内存在进程之间共享,在ets中它被复制,在进程字典中它只是该进程的本地内存。
https://github.com/mochi/mochiweb/blob/master/src/mochiglobal.erl