【问题标题】:Mathematica "linked lists" and performanceMathematica“链表”和性能
【发布时间】:2011-07-03 00:04:43
【问题描述】:

在 Mathematica 中,我像这样创建单链表:

toLinkedList[x_List] := Fold[pair[#2, #1] &, pair[], Reverse[x]];

fromLinkedList[ll_pair] := List @@ Flatten[ll];

emptyQ[pair[]] := True;
emptyQ[_pair] := False;    

对 cons 单元使用符号 pair 具有 Flatten 安全工作的优点,即使列表包含 Mathematica 样式 Lists,并且允许您使用 MakeExpression/MakeBoxes 定义自定义符号,这使一切变得更加愉快。为了避免使用$IterationLimit,我编写了使用While 循环或NestWhile 来处理这些列表的函数,而不是使用递归。自然,我想看看哪种方法更快,所以我写了两个候选人,以便我可以观看他们的战斗:

nestLength[ll_pair] := 
 With[{step = {#[[1, -1]], #[[-1]] + 1} &},
  Last@NestWhile[step, {ll, 0}, ! emptyQ@First@# &]];

whileLength[ll_pair] := 
 Module[{result = 0, current = ll},
  While[! emptyQ@current,
   current = current[[2]];
   ++result];
  result];

结果很奇怪。我在长度为 10000 的链表上测试了这些函数,whileLength 通常比nestLength 的 0.055 秒快 50%,大约是 0.035 秒。然而,偶尔whileLength 需要大约 4 秒。我认为可能存在一些缓存行为,所以我开始生成新的随机列表来检查,whileLength 在第一次运行新列表时不一定会很慢;可能需要几十次才能看到减速,但它不会再次发生(至少对于我尝试使用每个列表的 200 次运行来说不是)。

可能会发生什么?

作为参考,我用来测试的函数是这样的:

getTimes[f_, n_] :=
 With[{ll = toLinkedList@RandomInteger[100, 10000]},
  Table[Timing[f@ll], {n}][[All, 1]]]

编辑:我之前忽略了版本;我使用 Mathematica 8 得到了这些结果。

编辑第二个:当我阅读 Daniel Lichtblau's answer 时,我意识到我的“典型”运行时间省略了前导 0。它已得到修复。

编辑第三个:我认为Leonid Shifrin 将问题与Module 联系起来是正确的;通过将With 替换为Module,我可以从基于NestWhile 的版本中获得相同的行为:

nestModuleLength[ll_pair] := 
  Module[{step = {#[[1, -1]], #[[-1]] + 1} &}, 
   Last@NestWhile[step, {ll, 0}, ! emptyQ@First@# &]];

In[15]:= Select[getTimes[nestModuleLength, 100], # > 3 &]
Out[15]= {3.797}

【问题讨论】:

  • Developer'PackedArrayQ 可能相关
  • @Yaroslav Bulatov:我不明白为什么打包数组是相关的,因为除了RandomInteger 生成的初始List 之外什么都不应该打包,它会立即转换为树状表达。
  • 您使用的是版本 7 还是 8?无论如何,就其价值而言,我认为您已经发现了一些错误,或者至少是评估行为中的一个弱点。
  • 我不能把Module 归功于@belisarius 是第一个暗示它是罪魁祸首的人。
  • 这不是 With 或 Module 本身(我相信我什至重现了 Block 的问题)。正是符号之间的冲突使评估者误以为它需要检查表达式是否需要重新评估。

标签: wolfram-mathematica


【解决方案1】:

下面的例子给出了典型的结果。

一个 20 长的慢跑示例。

In[18]:= getTimes[whileLength, 20]

Out[18]= {0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, \
0.031, 0.047, 0.032, 0.031, 0.031, 3.547, 0.047, 0.031, 0.031, 0.032, \
0.031, 0.031}

我顺便注意到,时间比原来的帖子快 10 倍,除了可比较的慢速情况。不确定是什么导致了比率差异。

没有慢的例子。

In[17]:= getTimes[nestLength, 20]

Out[17]= {0.047, 0.047, 0.062, 0.047, 0.047, 0.062, 0.047, 0.047, \
0.047, 0.063, 0.046, 0.047, 0.047, 0.063, 0.047, 0.046, 0.047, 0.063, \
0.047, 0.047}

100 次跑步中的慢速示例。

In[19]:= getTimes[whileLength, 100]

Out[19]= {0.031, 0.031, 0.031, 0.032, 0.031, 3.594, 0.047, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.047, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.047, 0.031, 0.031, \
0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, \
0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, \
0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, 0.031, \
0.032, 0.031, 0.031, 0.031, 0.032, 0.031, 0.047, 0.031, 0.031, 0.032, \
0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.031, 0.032, 0.046, 0.032, \
0.031, 0.031, 0.031, 0.032, 0.031, 0.031, 0.047, 0.031, 0.032, 0.031, \
0.031, 0.031, 0.032, 0.031, 0.047, 0.031, 0.031, 0.031, 0.032, 0.031, \
0.031, 0.031}

Mathematica 不完美地实现了所谓的“无限求值”。也就是说,一个表达式会重新计算,直到它停止变化。为了使这一过程相当快,有各种优化措施试图尽可能缩短流程。

在某些情况下,这可能很难辨别(由于类似于哈希冲突的影响),并且可能会不必要地重新评估表达式。深度嵌套的表达式往往是最坏的情况。我们有更多的代码,即使在发生冲突的情况下也能经常解决这些问题。

这个例子中的罪魁祸首正是这段代码,它试图快速确定一个表达式是否需要重新计算。这很奇怪,但可能是一个线索(对某人而言),这种情况在 While 循环内的运行中最多发生一次。所以在坏情况下会发生一些事情,防止在同一个 While 内再次发生。

有一次我对重新评估检测代码很熟悉,写了一大块。但它是为第 8 版重写的。所以即使在调试器中看到这种次优行为后,这对我来说还是个谜。我现在只能说我提交了一个错误报告。

正如 Leonid Shifrin 观察到的,具有 HoldAllComplete 属性的符号不受此问题的影响。因此,使用该属性可能对这种类型的代码有益。

丹尼尔·利希特布劳 Wolfram 研究

【讨论】:

  • 仅供参考。我试图重现在 Workbench 下运行的行为以对其进行分析。但它运行完美!
  • @belisarius 这个版本是 8.0 吗?从理论上讲,这可能会有所作为。除此之外,我很困惑。 (不知道这到底是什么意思,但我怀疑这与被一头牛打过头有关。至少有些日子是这种感觉。)另外,如果你改变了事物的名称,例如pair-->flair,这可能会导致碰撞消失。
  • @belisarius 这可能会引发不良行为。在 getTimes 中将最后一行更改为 Table[Update[];时间[f@ll], {n}][[All, 1]]
  • 是的,第 8 版。我会尝试 Update[] 并返回结果。
【解决方案2】:

似乎它与模块本地符号内存管理有关。

我将展示一些运行的时序。每次运行当然都会给出一个独特的情节,但我检查了运行之间的“一致性”。看:

whileLength[l2_pair] := 
  Module[{result = 0}, current = l2; 
   While[! emptyQ@current, current = current[[2]];
    ++result];
   result];  

给出以下时序:

仅使用全局符号时:

whileLength[l2_pair] := 
  Module[{}, result = 0; current = l2; 
   While[! emptyQ@current, current = current[[2]];
    ++result];
   result];

给出:

【讨论】:

    【解决方案3】:

    免责声明:以下为推测。这似乎与搜索UpValues 有关。看起来这已经针对全局变量进行了优化(以便系统在确定可以执行此操作时跳过此步骤),但不适用于Module - 生成的局部变量。为了测试这一点,将HoldAllComplete 属性分配给pair,效果消失(从那时起,UpValues 不检查current):

    SetAttributes[pair, HoldAllComplete];
    
    In[17]:= ll = toLinkedList@RandomInteger[100, 10000];
    Max[Table[Timing[whileLength[ll]], {1000}][[All, 1]]]
    
    Out[18]= 0.047
    

    HTH

    【讨论】:

    • "从那时起,UpValues 不再检查当前";你能详细说明一下吗?
    • @acl: 当求值者遇到表达式f[a] 并且f 具有HoldAllComplete 属性时,不仅af 之前没有求值(a 会发生什么然后完全取决于f) 的规则,但也不会检查与a 关联的UpValues,而当f 具有HoldAll 时会检查它们。我不完全确定这是在这里发挥作用的原因,但是分配的右上方看起来像Part[pair[num,pair[..]],2]。评估 Part 时,评估 pair[...](搜索 UpValues 形式的 pair[___,element,___]),但如果 pairHoldAllComplete,则不评估。
    • @acl:这在版本 3 中被称为“就地”列表修改问题,但似乎在以后的版本中进行了优化(David Wagner 在他的书中,第 1 章对此进行了讨论) 7,第 211 页)。我的猜测是,就地列表(表达式)修改的优化没有涵盖(或不完全涵盖)Module 生成的局部变量的情况。正如我所说,我很可能是错的,这只是一个猜测。
    猜你喜欢
    • 1970-01-01
    • 2011-05-10
    • 1970-01-01
    • 2011-04-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多