运行时编译 C++ 代码
使用领域特定嵌入式语言 (DSEL) 和 LLVM 在运行时优化算法。
引言
C 编程语言在编译时和运行时之间划定了一条界限:你编译程序将其转换为机器码,然后可以运行。C++ 通过模板元编程技术跨越了这条界限,这些技术利用编译器“运行”元程序。动态编程语言(例如 Python)则朝着另一个方向跨越界限,允许在运行时生成函数(“更高级别的函数”)。
以下是一个概念证明,表明可以使用 LLVM 编译器基础设施 在 C++ 中实现更高级别的函数,以及它们如何用于运行时代码优化。
背景
静态代码优化
C++ 编译器可以进行相当复杂的代码优化。然而,可以进行多少优化取决于编译器对代码的了解程度。
例如,一个简单的优化是“强度缩减”。通常,当面对如下代码时:
int divide(int x, int z) { return x/z; }
编译器别无选择,只能使用昂贵的除法指令。但是,如果对 x
和 z
有更多了解,就可以执行许多额外的优化。例如,如果我们知道 z
在编译时,我们可以将已知值嵌入到函数中(或专门化一个模板函数)
int divide8(int x) { return x/8; }
现在,编译器可以优化掉除法,将其替换为廉价的右移操作
int divide8_opt(int x) { return x >> 3; }
这种以及类似的优化可以显著提高应用程序性能。
目标特定优化
额外的优化取决于目标机器:如果你预先知道应用程序将在 Core 2 机器上运行,你可以使用 SSE 4.1 指令来实现额外的优化。然而,在大多数情况下,应用程序预计将在各种机器上运行,因此采用“最小公分母”的方法,发出与 Pentium 兼容的可执行文件,从而失去潜在的性能提升。
动态代码优化
如果我们可以将编译过程的一部分推迟到运行时,那么将有更多的优化机会。
首先,我们可以生成针对应用程序正在运行的实际机器进行优化的代码——仅在 Core 2 机器上运行时使用 SSE4.1 等扩展。
其次,我们可以创建依赖于编译时不可用值的代码。例如,数据包过滤器可能扫描网络流量以查找特定模式,而这些模式仅在运行时才知道。但是,这些模式相对静态,并且很少更改(请参阅此示例)。
LLVM 编译器基础设施
LLVM 是一个高质量、平台无关的编译基础设施。它支持编译成中间“字节码”,以及高效的即时 (JIT) 编译和执行。要很好地了解 LLVM 和编写编译器,请参阅优秀的 LLVM 教程。LLVM 提供了生成字节码的工具,然后可以对其进行优化、编译和执行。
Using the Code
DynCompiler
DynCompiler 是一个概念证明(即:不要指望它能处理比玩具问题更复杂的问题,拥有漂亮的语法,或者做任何有用的事情),它是一个用于动态代码编译的 DSEL,利用 LLVM 来完成实际工作。使用 DynCompiler,你可以创建一个“更高级别的函数”——或者一个用于创建其他函数的函数。
例如,下面的函数可以为给定的系数 a、b 和 c 创建一个特定的二次多项式 (ax2+bx+c)
typedef int (*FType)(int); FType build_quad(int a, int b, int c) { DEF_FUNC(quad) RETURNS_INT ARG_INT(x); BEGIN DEF_INT(tmp); tmp = a*x*x + b*x + c; RETURN(tmp); END return (FType)FPtr; }
请注意,build_quad()
返回的是一个函数——而不是二次函数本身(就像函数模板不是“具体”函数一样)。要创建一个实际函数
FType f1 = build_quad(1, 2, 1); // f1(x) := x^2+2*x+1
现在可以像其他函数一样使用它
for(int x = 0; x < 10; x++) { std::cout << "f1(" << x << ") = " << f1(x) << std::endl; }
语法
DynCompiler 具有丑陋的语法——这是预处理器限制和惰性造成的。函数生成器有一个名称和一个返回类型(仅支持“int
”和“double
”)
DEF_FUNC(name) RETURNS_INT
或
DEF_FUNC(name) RETURNS_DOUBLE
对于返回 double
的函数。通过以下方式提供返回函数的参数
ARG_INT(x); // integer
或
ARG_DOUBLE(x); // double
实际函数代码以 BEGIN
开始
BEGIN
可以使用 DEF_INT
和 DEF_DOUBLE
定义局部变量
DEF_INT(tmp);
然后你可以(几乎)正常地使用这些变量
tmp = a*x+b;
请注意,此时代码不会被评估,除了“普通”C++ 变量(如 a
和 b
)。因此,在此行执行时 a
= 3,b = 2,上述代码将计算为
tmp = 3*x+2;
请注意,未使用的变量或在使用前未初始化的变量将导致错误。使用以下方式从函数返回一个值
RETURN_INT(expr);
或
RETURN_DOUBLE(expr);
请注意,函数必须返回值。函数块以 END
结束
END
基本的控制流由 IF
和 WHILE
提供
IF(x > 0) IF(y > 0) z = x*y; IFEND ELSE z = 0 IFEND WHILE(z > 0) z -= x; WHILEND
此外,可以使用 PRINT(expr)
打印到标准输出
PRINT(i);
最后,在 END
之后,FPtr
将指向新创建的函数。不过,你仍然需要将指针转换为实际的函数类型
f1 = (FType)FPtr;
运行代码
你需要下载并构建 LLVM(查看此链接获取 Visual Studio 特定说明)。DynCompiler 代码本身还需要 TR1 支持——例如,Visual Studio 2008 SP1。
状态
DynCompiler 是一个概念证明,不应认真对待。