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

Lisp Mathematica中函数和模式匹配的性能差异,lisp,wolfram-mathematica,Lisp,Wolfram Mathematica,因此Mathematica不同于lisp的其他方言,因为它模糊了函数和宏之间的界限。在Mathematica中,如果用户想要编写一个数学函数,他们可能会使用模式匹配,比如f[x]:=x*x而不是f=function[{x},x*x],尽管当使用f[x]调用时,两者都会返回相同的结果。我的理解是,第一种方法相当于lisp宏,根据我的经验,由于语法更简洁,因此更受欢迎 所以我有两个问题,执行函数与模式匹配/宏方法之间是否存在性能差异?不过,如果函数真的被转换成某种版本的宏,以实现Listable等功

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

所以我有两个问题,执行函数与模式匹配/宏方法之间是否存在性能差异?不过,如果函数真的被转换成某种版本的宏,以实现
Listable
等功能,我也不会感到惊讶


我之所以关心这个问题,是因为最近有一系列关于在大型程序中捕捉Mathematica错误的问题。如果大多数计算都是根据函数定义的,那么在我看来,跟踪计算顺序和错误产生的位置要比在输入被连续应用宏/模式重写后捕获错误容易得多

模式匹配似乎更快:

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}

您可以使用前面的函数记录步骤来查看Mathematica对函数的实际操作。它对待它就像对待其他头一样。假设你有以下几点

f = Function[{x}, x + 2];
f[2]
它首先将f[2]转化为

Function[{x}, x + 2][2]
在下一步中,
x+2
被转换为
2+2
。本质上,“函数”求值的行为类似于模式匹配规则的应用程序,因此它的速度不快也就不足为奇了

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

某些测量 根据@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]
至少对我来说,结果非常令人惊讶:

有什么解释吗?请随意编辑此答案(对于长文本而言,评论是一团糟)

编辑

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


注:对于常数函数(f[x]:=1),结果与此图非常相似

我对Mathematica的理解是它是一个巨大的搜索引擎。所有函数、变量和其他赋值基本上都存储为规则,在求值过程中,Mathematica将遍历此全局规则库并应用它们,直到生成的表达式停止更改

因此,通过规则列表的次数越少,评估的速度就越快。使用
跟踪查看发生了什么(使用的函数g和h)

很明显,为什么匿名函数最快,为什么使用
函数
会比简单的
设置延迟
带来额外的开销。我建议看一看Leonid Shifrin的优秀著作,其中详细解释了这些概念

我有时会构建一个包含所有所需函数的表,并手动将其应用于起始表达式。由于Mathematica的内置函数都不需要与我的表达式匹配,因此与正常计算相比,这提供了显著的速度提升

我的理解是,第一种方法相当于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]
(对于数组的每个元素),而快的第三个解决方案是查找符号
sqrt
的值
Function[…]
一次,然后重复应用lambda函数。相比之下,最快的第一个解决方案是用C编写的循环调用
sqrt
。因此搜索全局重写规则非常昂贵,术语重写也非常昂贵

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

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

Out[4] = {0.0500000, Null}
> let xs = [|1.0..100000.0|];;
...
> Array.map sqrt xs;;
Real: 00:00:00.006, CPU: 00:00:00.015, GC gen0: 0, gen1: 0, gen2: 0
...
> open System.Collections.Generic;;
> let fns = Dictionary<string, (obj -> obj)>(dict["sqrt", unbox >> sqrt >> box]);;
> 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
...
> type expr =
    | Float of float
    | Symbol of string
    | Packed of float []
    | Apply of expr * expr [];;
> 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
> 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
> 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
| Apply(Symbol "Sqrt", [|Packed xs|]) ->
    Packed(Array.map sqrt xs)