编译(Compile)是 Mathematica 里的提高运算速度的一个重要的工具,但是用过 Compile 的人都知道它并不是很好用,比如基本不能在 Compile 里用函数式的写法。我之前并不在意这个问题,一是对我来说与其用 Compile 不如直接写 C++,二是当时听说 v12.0 会有一个用全新架构的编译器叫 FunctionCompile。直到 v12.0 发布,我发现 FunctionCompile 虽然对于函数式编程的支持提升了很多,但是问题还是不少,比如 bug 太多,输出的错误信息很难看懂,而且编译之后的性能还不如原来的 Compile。
为了彻底解决编译问题,我自己实现了一个编译器
njpipeorgan/MathCompilegithub.com可以从这里下载到最近发布的版本。这个包的使用指南在这里,可编译的函数在这里。这个项目目前还在进行中,如果你有任何建议,欢迎在这里或者 github 上反馈。
MathCompile 简介
要安装 MathCompile,在这个 GitHub 发布页面上找到最新的版本,然后在 Mathematica 中运行相应的 PacletInstall 命令,就完成了。
我们用其中的一个函数 CompileToCode 测试是否安装好了,它的功能是把 Mathematica 函数编译成 C++ 代码。
<<MathCompile` (* 导入包 *)
f = Function[{Typed[x, Integer]}, x + 2]; (* 定义一个函数 *)
CompileToCode[f] (* 编译成 C++ 代码 *)
其中 Typed[x, Integer] 表示函数的参数是一个名为 x 的整数。编译输出的结果应该是一个字符串,差不多是这样的:
auto main_function(const int64_t& v75) {
return wl::val(wl::plus(WL_PASS(v75), int64_t(2)));
}
MathCompile 进一步的编译功能需要一个外部的 C++ 编译器,目前支持 GCC,Clang,ICC 和 MSVC。在这里有关于具体支持那些版本和如何设置编译器的介绍。在类 Unix 系统上一般不需要配置,Mathematica 会自动找到 GCC 或者 Clang;而在 Windows 上可以安装 MinGW-w64 然后进行相应配置。
有了 C++ 编译器后就可以用 CompileToBinary 函数了,它的作用是把 Mathematica 函数编译成二进制库然后链接回 Mathematica:
f = Function[{Typed[x, {Integer, 1}]}, Times @@ x]; (* 定义一个函数 *)
compiled = CompileToBinary[f]; (* 编译成二进制 *)
其中 Typed[x, {Integer, 1}] 表示函数的参数 x 是一个整数的列表(数组的维数为 1)。此时编译的结果可以直接当作函数调用,
compiled[{1, 2, 3, 4}] (* 输出 24 *)
MathCompile 是怎么实现的?
MathCompile 的编译过程是先把 Mathematica 代码翻译成 C++ 然后借助 C++ 编译器完成后续的工作(原生的 Compile 是通过 C,FunctionCompile 则是通过 LLVM)。
生成 C++ 代码的部分是在 Mathematica 中完成的。包括了对一些特殊的结构做转换,比如 Table,If,Module,还包括少量的语义分析。因为我的思路是把尽可能多的工作放在 C++ 的一端完成,所以我在 C++ 中实现了所有支持的函数,而且类型推导之类的工作是由 C++ 编译器完成的。这就形成了一个有趣的现象,虽然这个编译器名义上是用 Mathematica 写的,但是这个包中的 C++ 代码远多于 Mathematica 代码。
这个项目的形成完全得益于主流编译器对 C++14 和 C++17 的支持。比如考虑如下 Mathematica 代码:
...
f = If[x > y, (# + 1)&, (# - 1)&];
f[5]
有了 C++14,这段代码就可以几乎不加改动地翻译成 C++,大大降低了项目的复杂度:
...
auto f = [condition = (x > y)](auto arg) {
if (condition)
return ([](auto x) { return x + 1; })(arg);
else
return ([](auto x) { return x - 1; })(arg);
};
return f(5);
你可以在这里找到我对 If 的实现。
MathCompile 的性能如何?
在 C++ 编译器能进行充分优化的情况下,MathCompile 在理论上有和原生 C++ 基本等同的性能。我做的一些测试显示出 MathCompile 性能至少持平内置编译器,在少数测试中则快十倍以上。比如在 Mandelbrot 集的计算上,MathCompile 相比内置编译器快了 5 倍左右,具体的代码和结果可以在这里找到。
MathCompile 相比内置编译器的性能优势主要来自去除了对 Wolfram 运行时库的依赖,C++ 编译器能看见更多细节然后做优化,消除了很多函数调用。其次是 MathCompile 生成的 C++ 代码从结构上接近“正常”的代码,方便了 C++ 编译器的优化,而没有像内置编译器一样用 goto 组织循环。
一些编译的例子
- 对
进行牛顿迭代并且指出最近的根 ref/Compile
f=CompileToBinary@
Function[{Typed[z,Complex],Typed[n,Integer]},
If[Re[#]>0,1,If[Im[#]>0,2,3]]&@Nest[(2#+1/#^2)/3&,z,n]];
ArrayPlot[Table[f[x+I y,25],{y,-1,1,2./199},{x,-1,1,2./199}]]
2. 用一个复杂的方式 Flatten 一个数组
f=CompileToBinary@
Function[{Typed[x,{Integer,4}]},Flatten[x,{{1,3},{2,4}}]];
f[ArrayReshape[Range[16],{2,2,2,2}]]
3. 用递归实现阶乘 Code Compilation
f=CompileToBinary@
Function[{Typed[x,Integer]},
Module[{fac=Typed[{Integer}->Integer]},
fac=If[#<=1,1,# fac[#-1]]&;
fac[x]
]
];
f[5]
递归函数需要指定类型,Typed[{Integer} -> Integer] 表示函数 fac 的参数是一个整数,并返回一个整数。
4. 计算一组随机数的平均值
f=CompileToBinary@
Function[{Typed[x,Integer]},
Mean@RandomVariate[ChiSquareDistribution[x],10000]];
f[30]