9

((请原谅我在一个线程中问了多个问题。我认为它们是相关的。))

您好,我想知道 Erlang 在每个模块的预编译数据方面存在哪些最佳实践。

示例:我有一个模块,它对一个已知的、非常复杂的正则表达式进行大量操作。re:compile/2 的文档说:“编译一次并执行多次比每次要匹配时都编译效率高得多”。由于没有指定 re 的 mp() 数据类型,因此如果您想要一个独立于目标的梁,则不能在编译时放置,因此必须在运行时编译 RegEx。((注意: re:compile/2 只是一个例子。任何要记忆的复杂函数都适合我的问题。))

Erlang 的模块 (can) 有一个-on_load(F/A)属性,表示加载模块时应该执行一次的方法。因此,我可以将我的正则表达式放入此方法中进行编译,并将结果保存在一个名为?MODULE.

在丹的回答后更新。

我的问题是:

  • 如果我理解 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
.
4

3 回答 3

7

你有另一个选择。您可以预先计算正则表达式的编译形式并直接引用它。一种方法是使用专门为此目的设计的模块,例如ct_expandhttp ://dukesoferl.blogspot.com/2009/08/metaprogramming-with-ctexpand.html

您还可以通过动态生成一个模块来滚动您自己,该模块使用一个函数将此值作为常量返回(利用常量池):http ://erlang.org/pipermail/erlang-questions/2011-January/ 056007.html

或者您甚至可以re:compile在 shell 中运行并将结果复制并粘贴到您的代码中。粗鲁但有效。如果实现发生变化,这将是不可移植的。

需要明确的是:所有这些都利用常量池来避免每次重新计算。但是,当然,这增加了复杂性并且需要成本。

回到你原来的问题:进程字典的问题在于,它只能被它自己的进程使用。你确定这个模块的函数只会被同一个进程调用吗?甚至 ETS 表也与创建它们的进程相关联(但 ETS本身并没有使用进程和消息传递来实现),并且如果该进程终止,它也会终止。

于 2011-05-31T03:20:51.873 回答
5

ETS 没有在进程中实现,也没有将其数据放在单独的进程堆中,但它确实将其数据放在所有进程之外的单独区域中。这意味着在读取/写入 ETS 表时,必须将数据复制到流程中/从流程中复制。当然,这样做的成本有多大取决于被复制的数据量。这就是为什么我们有类似的功能ets:match_object并且在复制数据之前ets:select允许更复杂的选择规则的原因之一。

将数据保存在 ETS 表中的一个好处是,所有进程都可以访问它,而不仅仅是拥有该表的进程。这比将数据保存在服务器中更有效。它还取决于您要对数据执行的操作类型。ETS 只是一个数据存储,提供有限的原子性。在您的情况下,这可能没问题。

您绝对应该将数据保存在单独的记录中,每个不同的编译正则表达式都有一个记录,因为它会大大提高访问速度。然后你可以直接得到你想要的re,否则你会得到它们,然后在你想要的那个之后再次搜索。这样就违背了将它们放入 ETS 的意义。

虽然您可以执行诸如在on_load函数中构建 ETS 表之类的操作,但对于 ETS 表来说并不是一个好主意。这是因为 ETS 归进程所有,并在进程终止时被删除。你永远不知道on_load函数是在哪个进程中调用的。您还应该避免做可能需要很长时间的事情,因为模块在完成之前不会被认为是加载的。

生成解析转换以将编译 re's 的结果直接静态插入到代码中是一个很酷的主意,特别是如果你的 re's 确实是静态定义的。正如动态生成、编译和加载模块到系统中的想法一样。同样,如果您的数据是静态的,您可以在编译时生成此模块。

于 2011-05-31T23:02:45.620 回答
3

mochiglobal 通过编译一个新模块来存储您的常量来实现这一点。这里的优点是内存是跨进程共享的,在 ets 中它被复制,而在进程字典中它只是该进程的本地。

https://github.com/mochi/mochiweb/blob/master/src/mochiglobal.erl

于 2011-05-31T01:23:16.310 回答