加速 Erlang 索引功能

Posted

技术标签:

【中文标题】加速 Erlang 索引功能【英文标题】:Speeding up Erlang indexation function 【发布时间】:2010-12-01 08:36:45 【问题描述】:

所以从这个问题开始:

Erlang lists:index_of function?

我有以下代码可以正常工作:

-module(test_index_of).
-compile(export_all).

index_of(Q)->
    N=length(Q),
    Qs=lists:zip(lists:sort(Q), lists:seq(1, N)),
    IndexFn=fun(X)->
                _, _, I=lists:keysearch(X, 1, Qs),
            I
        end,     
    [IndexFn(X) || X <- Q].

test()->
    Q=[random:uniform() || _X <- lists:seq(1, 20)],
    T1, _=timer:tc(test_index_of, index_of, [Q]),
    io:format("~p~n", [T1]).

问题是,我需要在长度为 20-30 个字符的列表上运行 index_of 函数很多次 [10,000] 次; index_of 函数是我代码中的性能瓶颈。因此,尽管它看起来对我来说实施得相当有效,但我不相信它是最快的解决方案。

任何人都可以在 index_of 的当前实现上改进 [性能方面] 吗? [Zed提到gb_trees]

谢谢!

【问题讨论】:

【参考方案1】:

您正在优化对错误数据类型的操作。

如果您要对包含 20-30 个项目的同一个列表进行 10 000 次查找,那么进行预计算以加快这些查找的速度确实是值得的。例如,让我们在 key, index 的元组中创建一个按键排序的元组。

1> Ls = [x,y,z,f,o,o].
[x,y,z,f,o,o]
2> Ls2 = lists:zip(Ls, lists:seq(1, length(Ls))).
[x,1,y,2,z,3,f,4,o,5,o,6]
3> Ts = list_to_tuple(lists:keysort(1, Ls2)).         
f,4,o,5,o,6,x,1,y,2,z,3

对这个元组的键进行递归二分搜索将很快找到正确的索引。

使用 proplists:normalize 删除重复项,即如果在查找 'o' 时返回 6 而不是 5 是错误的。或者使用折叠和集合来实现您自己的删除重复项的过滤器。 尝试使用 dict:from_list/1 构建一个字典,然后在该字典上进行查找。

但这仍然引出了一个问题:为什么要将索引变成一个列表?使用 lists:nth/2 进行查找具有 O(n) 复杂度。

【讨论】:

例如,如果你想得到一个随机变量产生的事件的第一次出现,并为多次出现这样做,这似乎是一个合理的方法。 >> 如果您要在 20-30 个项目的同一个列表上进行 10 000 次查找,我可能应该更清楚;我想生成 20 个项目的 10000 个随机列表,然后对 10000 个列表中的每一个执行 index_of >> 为什么要将索引放入某个列表中?使用 lists:nth/2 进行查找具有 O(n) 复杂度。是的,我知道它效率低下。它是问题域的一部分,是蒙特卡罗模拟。我怀疑 Erlang 不是此类问题的最佳解决方案,但我想尽可能地推动它【参考方案2】:

不确定我是否完全理解这一点,但如果以上是您的实际用例,那么...

首先,你可以生成如下的Q,并且你已经保存了压缩部分。

Q=[N,random:uniform() || N <- lists:seq(1, 20)]

更进一步,您可以从一开始就生成一个按值索引的树:

Tree = lists:foldl(
              fun(T, N) -> gb_trees:enter(uniform:random(), N, T) end,
              gb_trees:empty(),
              lists:seq(1, 20)
       ).

那么查找你的索引就变成了:

index_of(Item, Tree) ->
  case gb_trees:lookup(Item, Tree) of
    value, Index -> Index;
    _ -> not_found
  end.

【讨论】:

谢谢,gb_tree 的例子很有用【参考方案3】:

我认为您需要自定义排序函数来记录它对输入列表所做的排列。例如,您可以使用列表:排序源。这应该会给你 O(N*log N) 的性能。

【讨论】:

【参考方案4】:

只有一个问题:你想做什么?

我只是找不到这个功能的实际用途。我认为你做了一些奇怪的事情。看来你刚刚从 O(NM^2) 提高到 O(NM*logM) 但还是很糟糕。

编辑

当我综合什么是目标时,您似乎正在尝试使用蒙特卡洛方法来确定球队在英超联赛中“完成位置”的概率。但我仍然不确定。您可以确定最可能的位置[1,1,2] -&gt; 1 或作为某种平均值 1.33 的小数 - 例如,最后一个可以比其他人更轻松地实现。

在函数式编程语言中,数据结构比过程或 OO 语言更重要。他们更多的是关于工作流程。你会做这个,然后做这个,然后做......在像 Erlang 这样的函数式语言中,你应该以一种方式思考,我有这个输入,我想要那个输出。我可以从这个和这个等中确定所需的输出。可能没有必要像您过去在程序方法中那样列出事物。

在程序方法中,您习惯于使用数组进行存储,并具有恒定的随机访问。清单不是那样的东西。 Erlang 中没有可以编写的数组(即使是实际上是平衡树的数组模块)。您可以将元组或二进制用于只读数组,但不能进行读写。我可以写很多关于根本不存在具有恒定访问时间的数据结构(从 RAM,通过过程语言中的数组到 HASH 映射)但是这里没有足够的空间来详细解释它(来自 RAM 技术,通过 L1,2,3 个 CPU 缓存,当 key 数量增加和 key HASH 计算依赖于 key 长度时,必然增加 HASH 长度。

列表是具有 O(N) 随机访问时间的数据结构。这是存储数据的最佳结构,您希望以与存储在列表中相同的顺序一一获取。对于小N,当对应的常数很小时,它可以是对小N进行随机访问的结构。例如,当 N 是您的问题中的团队数(20)时,它可能比 O(logN) 访问某种树更快。但是你必须注意你的常数有多大。

算法的一个常见组件是键值查找。在某些情况下,可以使用数组作为过程世界中的支持数据结构。键必须是整数,可能键的空间不能稀疏等。除了这里的 N 非常小之外,List 不能很好地用作此目的的替代品。我了解到编写功能代码的最佳方法是避免在不必要的地方进行键值查找。它经常需要重新安排工作流程或重构数据结构等等。有时它看起来像是像手套一样翻转问题解决方案。

如果我忽略了您的概率模型是错误的。根据您提供的信息,您的模型团队的赛季积分似乎是独立的随机事件,这当然不是真的。不可能所有球队都获得高分,例如 82 分,仅仅因为所有球队在一个赛季中获得的分数都有一定的限制。所以暂时忘记了这个。然后我将模拟一个“路径” - 赛季并以[78,'Liverpool', 81,'Man Utd', ...] 的形式获取结果,然后我可以使用 lists:sort 对其进行排序,而不会丢失哪个团队在哪里的信息。我将使用按路径迭代收集的结果。对于每条路径,我将遍历排序的模拟结果并将其收集到字典中,其中团队是关键(来自 atom 和常量存储的常量和非常便宜的哈希计算,因为键集是固定的,有可能使用元组/记录,但似乎过早优化)。值可以是大小为 20 的元组,元组中的位置是最终位置,值是它的计数。

类似:

% Process simulation results.
% Results = [[Points, Team]]
process(Results) ->
  lists:foldl(fun process_path/2,
    dict:from_list([Team, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ||
       Team <- ['Liverpool', 'Man Utd', ...]]),
    Results).

% process simulation path result
process_path(R, D) ->
  process_path(lists:reverse(lists:sort(R)), D, 1).

process_path([], _, D) -> D;
process_path([_, Team|R], D, Pos) ->
  process_path(R, update_team(Team, Pos, D), Pos + 1).

% update team position count in dictionary
update_team(Team, Pos, D) ->
  dict:update(Team, fun(T) -> add_count(T, Pos) end, D).

% Add final position Pos to tuple T of counts
add_count(T, P) -> setelement(P, T, element(P, T) + 1).

请注意,没有什么像 lists:index_of 或 lists:nth 函数。对于少数 M 个团队,产生的复杂度看起来像 O(NM) 或 O(NMlogM),但真正的复杂度是 O(NM^2) O(M) setelement/3add_count/2。对于更大的 M,您应该将 add_count/2 更改为更合理的值。

【讨论】:

如果你不能说任何建设性的话,那就什么都不要说 贾斯汀,我已经阅读了这个问题和上一个问题。 Hynek 已经问过你需要这个函数来做什么,而你没有告诉他。您所表达的习语(列表中对象的位置具有“意义”)对于函数式编程非常不自然-它散发出非常糟糕的代码气味。在你说出你为什么需要这个功能之前,Hynek(和我自己)都不能说除了“你的代码有异味”之外的任何东西。询问“为什么”是一个建设性的陈述——如果你回答它,我很乐意做出贡献。在你这样做之前,我只能坐在我的手上。 我正在尝试根据每支球队的赛季积分分布来模拟 [football] 英超联赛中每支球队的预期排名。因此,假设有 5 条模拟路径,我对利物浦的积分分布可能是 [78, 80, 81, 76, 82],而曼联可能是 [81, 78, 82, 82, 76]。我需要将这些点分布转换为“完成位置”;所以利物浦将是 [2, 1, 2, 2, 1] 和曼联 [1, 2, 1, 1, 2]。在实践中,我有 20 个团队和 1000 条模拟路径。 index_of函数用于对模拟点进行排序,计算结束位置。 @Hynek 感谢您抽出时间考虑问题并做出周到的回应。您认为这是一个蒙特卡洛问题是正确的;当您说概率模型错误时,您也是正确的;这是一个(可接受的)hack。但是,我认为您的解决方案是计算每个团队的预期位置,即一个数字。我需要计算每支球队在每个位置上完赛的概率,即一个向量;所以利物浦可能是 [1, 0.1, 2, 0.4, 3, 0.3, 4, 0.1 ... 20, 0.0] 这实际上是建设性的评论。但有时我们有足够的智慧和勇气重新考虑采用的方法并重新设计应用程序,以便以更好、更简单的方式交付所需的结果。

以上是关于加速 Erlang 索引功能的主要内容,如果未能解决你的问题,请参考以下文章

如何在erlang中应用热代码交换功能作为补丁?

使用通用测试的模块的 Erlang 测试(非导出/私有)功能

Erlang 的隐藏特性

Erlang:用新功能“扩展”现有模块

SpaceVim 语言模块 erlang

是否有任何 erlang web 框架提供与 Dropwizard 框架相同的功能?