我的理解是,第一种方法相当于 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 替换为调用 Sqrt 的 lambda 函数:
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 术语重写器本身的核心中匹配,而不是通过通用的全局重写表。
其他人已经描述了相似功能之间的性能差异要小得多。我相信这些情况下的性能差异只是 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
...
这需要 6 毫秒,就像 Mathematica 中的 Sqrt[xs] 一样。但这是意料之中的,因为此代码已被 .NET JIT 编译为机器代码以进行快速评估。
在 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 函数使用函数n 构造一个List 和n 元素,如果每个返回值都是Float 或更通用的Apply(Symbol "List", ...),则返回Packed,否则:
> 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 的类型是术语重写的特征:重写用其他表达式替换表达式,而不是将它们简化为值。
我们的程序现在可以由我们的自定义术语重写器定义和执行:
> 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] 在 Mathematica 中的性能!
我们甚至可以通过添加适当的规则来恢复Sqrt[xs] 的性能:
| Apply(Symbol "Sqrt", [|Packed xs|]) ->
Packed(Array.map sqrt xs)
我在term rewriting in F#写了一篇文章。