65.9K
CodeProject 正在变化。 阅读更多。
Home

运行时编译 C++ 代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (18投票s)

2008年10月8日

CPOL

4分钟阅读

viewsIcon

79545

downloadIcon

829

使用领域特定嵌入式语言 (DSEL) 和 LLVM 在运行时优化算法。

引言

C 编程语言在编译时和运行时之间划定了一条界限:你编译程序将其转换为机器码,然后可以运行。C++ 通过模板元编程技术跨越了这条界限,这些技术利用编译器“运行”元程序。动态编程语言(例如 Python)则朝着另一个方向跨越界限,允许在运行时生成函数(“更高级别的函数”)。

以下是一个概念证明,表明可以使用 LLVM 编译器基础设施 在 C++ 中实现更高级别的函数,以及它们如何用于运行时代码优化。

背景

静态代码优化

C++ 编译器可以进行相当复杂的代码优化。然而,可以进行多少优化取决于编译器对代码的了解程度。

例如,一个简单的优化是“强度缩减”。通常,当面对如下代码时:

int divide(int x, int z) {
   return x/z;
}

编译器别无选择,只能使用昂贵的除法指令。但是,如果对 xz 有更多了解,就可以执行许多额外的优化。例如,如果我们知道 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,你可以创建一个“更高级别的函数”——或者一个用于创建其他函数的函数。

例如,下面的函数可以为给定的系数 abc 创建一个特定的二次多项式 (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_INTDEF_DOUBLE 定义局部变量

DEF_INT(tmp);

然后你可以(几乎)正常地使用这些变量

tmp = a*x+b;

请注意,此时代码不会被评估,除了“普通”C++ 变量(如 ab)。因此,在此行执行时 a = 3,b = 2,上述代码将计算为

tmp = 3*x+2;

请注意,未使用的变量或在使用前未初始化的变量将导致错误。使用以下方式从函数返回一个值

RETURN_INT(expr);

RETURN_DOUBLE(expr);

请注意,函数必须返回值。函数块以 END 结束

END

基本的控制流由 IFWHILE 提供

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 是一个概念证明,不应认真对待。

© . All rights reserved.