Mathematica中函数和模式匹配之间的性能差异

时间:2010-11-15 19:01:05

标签: lisp wolfram-mathematica

所以Mathematica与lisp的其他方言不同,因为它模糊了函数和宏之间的界限。在Mathematica中,如果用户想要编写数学函数,他们可能会使用类似f[x_]:= x*x而不是f=Function[{x},x*x]的模式匹配,尽管在使用f[x]调用时两者都会返回相同的结果。我的理解是第一种方法相当于一个lisp宏,并且由于语法更简洁,我的经验受到青睐。

所以我有两个问题,执行函数与模式匹配/宏方法之间是否存在性能差异?虽然函数实际上已转换为某些版本的宏以允许实现Listable等功能,但我不会感到惊讶。

我关心这个问题的原因是因为最近的一组问题(1) (2)关于试图在大型程序中捕获Mathematica错误。如果大多数计算是根据函数定义的,那么在我看来,跟踪评估的顺序和错误发生的位置比在连续应用宏来重写输入后尝试捕获错误更容易/模式。

5 个答案:

答案 0 :(得分:15)

我理解Mathematica的方式是它是一个巨大的搜索替换引擎。所有函数,变量和其他赋值都基本上存储为规则,在评估期间,Mathematica会遍历此全局规则库并应用它们,直到结果表达式停止更改。

因此,您必须通过规则列表的次数越少,评估的速度就越快。看看使用Trace会发生什么(使用gdelfino的函数g和h)

In[1]:= Trace@(#*#)&@x
Out[1]= {x x,x^2}
In[2]:= Trace@g@x
Out[2]= {g[x],x x,x^2}
In[3]:= Trace@h@x
Out[3]= {{h,Function[{x},x x]},Function[{x},x x][x],x x,x^2}

很明显为什么匿名函数最快,为什么使用Function会在简单的SetDelayed上引入额外的开销。我建议查看Leonid Shifrin的优秀书籍introduction,其中详细解释了这些概念。

我偶尔会构建一个包含我需要的所有函数的Dispatch表,并将其手动应用到我的起始表达式中。与正常评估相比,这提供了显着的速度提升,因为Mathematica的内置功能都不需要与我的表达相匹配。

答案 1 :(得分:12)

  

我的理解是第一种方法与lisp宏相当,并且由于语法更简洁,我的经验受到青睐。

不是真的。 Mathematica是一个术语重写器,Lisp宏也是如此。

  

所以我有两个问题,执行函数与模式匹配/宏方法之间是否存在性能差异?

是。请注意,在Mathematica中,您永远不会真正“执行函数”。您只是应用重写规则将一个表达式更改为另一个表达式。

考虑在填充的浮点数数组上映射Sqrt函数。 Mathematica中最快的解决方案是将Sqrt函数直接应用于打包数组,因为它恰好实现了我们想要的并且针对这种特殊情况进行了优化:

In[1] := N@Range[100000];

In[2] := Sqrt[xs]; // AbsoluteTiming

Out[2] = {0.0060000, Null}

我们可能会定义一个全局重写规则,其中sqrt[x]形式的术语会重写为Sqrt[x],以便计算平方根:

In[3] := Clear[sqrt];
         sqrt[x_] := Sqrt[x];
         Map[sqrt, xs]; // AbsoluteTiming

Out[3] = {0.4800007, Null}

请注意,这比之前的解决方案慢了约100倍。

或者,我们可以定义一个全局重写规则,用一个调用sqrt的lambda函数替换符号Sqrt

In[4] := Clear[sqrt];
         sqrt = Function[{x}, Sqrt[x]];
         Map[sqrt, xs]; // AbsoluteTiming

Out[4] = {0.0500000, Null}

请注意,这比之前的解决方案快10倍。

为什么呢?因为慢速第二个解决方案在内部循环中查找重写规则sqrt[x_] :> Sqrt[x](对于数组的每个元素),而快速第三个解决方案查找符号Function[...]的值sqrt一次然后重复应用该lambda函数。相比之下,最快的第一个解决方案是用C编写的循环调用sqrt。因此,搜索全局重写规则非常昂贵,并且术语重写非常昂贵。

如果是这样,为什么Sqrt一直很快?你可能会期望2倍减速而不是10倍,因为我们已经用Sqrt替换了sqrt的一次查找,而内部循环中只有SqrtSqrt的查找,但事实并非如此{ {1}}具有作为内置函数的特殊状态,它将在Mathematica术语重写器本身的核心中匹配,而不是通过通用全局重写表。

其他人已经描述了类似功能之间更小的性能差异。我相信这些情况下的性能差异只是Mathematica内部的确切实现方面的微小差异。 Mathematica最大的问题是全局重写表。特别是,这就是Mathematica与传统的术语级解释器不同的地方。

通过编写迷你Mathematica实现,您可以了解Mathematica的性能。在这种情况下,可以将上述解决方案编译为(例如)F#。可以像这样创建数组:

> let xs = [|1.0..100000.0|];;
...

内置的sqrt函数可以转换为闭包并赋予map函数,如下所示:

> Array.map sqrt xs;;
Real: 00:00:00.006, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
...

这需要6ms,就像Mathematica中的Sqrt[xs]一样。但这是可以预料到的,因为这个代码已被JIT编译成.NET的机器代码以便快速评估。

在Mathematica的全局重写表中查找重写规则类似于在键入其函数名称的字典中查找闭包。这样的字典可以在F#中构造如下:

> open System.Collections.Generic;;
> let fns = Dictionary<string, (obj -> obj)>(dict["sqrt", unbox >> sqrt >> box]);;

这类似于Mathematica中的DownValues数据结构,除了我们没有搜索多个结果规则以便第一个匹配函数参数。

然后该程序变为:

> Array.map (fun x -> fns.["sqrt"] (box x)) xs;;
Real: 00:00:00.044, CPU: 00:00:00.031, GC gen0: 0, gen1: 0, gen2: 0
...

请注意,由于内循环中的哈希表查找,我们得到了类似的10倍性能下降。

另一种方法是在符号本身中存储与符号相关联的DownValues,以避免哈希表查找。

我们甚至可以用几行代码编写一个完整的术语重写器。术语可以表示为以下类型的值:

> type expr =
    | Float of float
    | Symbol of string
    | Packed of float []
    | Apply of expr * expr [];;

请注意Packed实现了Mathematica的打包列表,即未装箱的数组。

以下init函数使用函数List构造n f元素,如果每个返回值为{{1},则返回Packed或者更通用的Float

Apply(Symbol "List", ...)

以下> let init n f = let rec packed ys i = if i=n then Packed ys else match f i with | Float y -> ys.[i] <- y packed ys (i+1) | y -> Apply(Symbol "List", Array.init n (fun j -> if j<i then Float ys.[i] elif j=i then y else f j)) packed (Array.zeroCreate n) 0;; val init : int -> (int -> expr) -> expr 函数使用模式匹配来标识它可以理解的表达式,并将其替换为其他表达式:

rule

请注意,此函数> let rec rule = function | Apply(Symbol "Sqrt", [|Float x|]) -> Float(sqrt x) | Apply(Symbol "Map", [|f; Packed xs|]) -> init xs.Length (fun i -> rule(Apply(f, [|Float xs.[i]|]))) | f -> f;; val rule : expr -> expr 的类型是术语重写的特征:重写将表达式替换为其他表达式,而不是将它们减少为值。

我们的程序现在可以由我们的自定义术语重写器定义和执行:

expr -> expr

我们已经恢复了Mathematica中> rule (Apply(Symbol "Map", [|Symbol "Sqrt"; Packed xs|]));; Real: 00:00:00.049, CPU: 00:00:00.046, GC gen0: 24, gen1: 0, gen2: 0 的性能!

我们甚至可以通过添加适当的规则来恢复Map[Sqrt, xs]的效果:

Sqrt[xs]

我写了一篇关于term rewriting in F#的文章。

答案 2 :(得分:6)

一些测量

基于@gdelfino回答和@rcollyer的评论,我做了这个小程序:

j = # # + # # &;
g[x_] := x x + x x ;
h = Function[{x}, x x + x x ];


anon = Table[Timing[Do[ # # + # # &[i], {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
jj   = Table[Timing[Do[ j[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
gg   = Table[Timing[Do[ g[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];
hh   = Table[Timing[Do[ h[i],           {i, k}]][[1]], {k, 10^5, 10^6, 10^5}];

ListLinePlot[ {anon,   jj,    gg,   hh}, 
 PlotStyle -> {Black, Red, Green, Blue},
 PlotRange -> All]

至少在我看来,结果非常令人惊讶: alt text

有任何解释吗?请随时编辑这个答案(评论对于长文本来说是一团糟)

修改

使用标识函数f [x] = x进行测试,以将解析与实际评估隔离开来。结果(相同颜色):

alt text

注意:结果与此常量函数的绘图非常相似(f [x]:= 1);

答案 3 :(得分:4)

模式匹配似乎更快:

In[1]:= g[x_] := x*x
In[2]:= h = Function[{x}, x*x];

In[3]:= Do[h[RandomInteger[100]], {1000000}] // Timing
Out[3]= {1.53927, Null}

In[4]:= Do[g[RandomInteger[100]], {1000000}] // Timing
Out[4]= {1.15919, Null}

模式匹配也更灵活,因为它允许您重载定义:

In[5]:= g[x_] := x * x
In[6]:= g[x_,y_] := x * y

对于简单的函数,您可以编译以获得最佳性能:

In[7]:= k[x_] = Compile[{x}, x*x]
In[8]:= Do[k[RandomInteger[100]], {100000}] // Timing
Out[8]= {0.083517, Null}

答案 4 :(得分:3)

您可以在之前的answer中使用函数recordSteps来查看Mathematica实际上对函数执行的操作。它就像任何其他头一样对待它。 IE,假设你有以下

f = Function[{x}, x + 2];
f[2]

首先将f [2]转换为

Function[{x}, x + 2][2]

在下一步,x+2转换为2+2。从本质上讲,“功能”评估的行为类似于模式匹配规则的应用,因此它不会更快,这不足为奇。

您可以将Mathematica中的所有内容都视为一个表达式,其中评估是在predefined sequence中重写表达式部分的过程,这适用于函数,就像任何其他头部一样