在5微秒内进行JIT编译
历史上,快速的JIT编译是一门黑暗的艺术。要编写一个快速的JIT编译器,你需要知道如何编写汇编语言。举个例子:如今没有任何一个可以投入生产使用的数据库拥有自己的JIT编译器。它们全部要么使用LLVM,要么生成C/C++代码。这两种选择都存在编译时间过长的问题,限制了它们的适用性。现在,借助AI,编写一个快速编译时间的JIT编译器变得比以往任何时候都容易,因为可以直接针对汇编。对于新的数据库来说,这是一个改善旧数据库的机会。在构建pgrust时,我最初认为实现一个JIT编译器会非常困难。最后,我发现由于AI的帮助,这比我预期的要容易得多,并且这也是pgrust快速的部分原因。pgrust的JIT编译器大约在5微秒内编译代码,这使我们能够对每个SQL查询进行JIT编译,而不仅仅是一个子集。在这篇文章中,我将带你了解如何构建自己的快速JIT编译器。我们将构建一个简单的正则表达式引擎,以JIT编译作为例子。为什么选择JIT编译JIT编译是指在运行时生成已编译代码的实践,或者称为“及时编译”。如果做得好,它可以带来巨大的性能提升,通常提升范围在2到5倍,有时甚至更多。JIT编译的主要用例是在运行时获得的信息会大幅改变程序的行为。这在编程语言解释器中尤其常见;它们在运行时接收要执行的代码。JIT编译器在编程语言之外的领域也很有用,比如数据解析。有时你在运行时才知道你要解析的数据的模式,而JIT可以帮助解决这个问题。我们来实现一个玩具正则表达式引擎。为了简单起见,我们仅支持两个特性:字面字符串和重复(即正则表达式中的*)。我们还将跳过解析器,并将正则表达式表示为已经解析的Rust结构。这意味着我们能够支持诸如:apples b(an)*这样的字符串,但不能进行交替或回溯等操作。在代码中这也是相当简单的。我们将有3种类型的节点:一个字面字符串节点、一个重复节点和一个连接节点,即两个节点的组合。最终结果如下: enum Node { Literal(&'static str), Concatenation(Box<Node>, Box<Node>), Repetition(Box<Node), } fn literal(text: &'static str) -> Node { Node::Literal(text) } fn concatenation(left: Node, right: Node) -> Node { Node::Concatenation(Box::new(left), Box::new(right)) } fn repetition(body: Node) -> Node { Node::Repetition(Box::new(body)) } 编写正则表达式引擎的解释器也很简单: fn match_node(node: &Node, input: &[u8], pos: usize, next: &dyn Fn(usize) -> bool) -> bool { match node { Node::Literal(text) => { let literal = text.as_bytes(); input[pos..].starts_with(literal) && next(pos + literal.len()) } Node::Concatenation(left, right) => { match_node(left, input, pos, &|left_end| { match_node(right, input, left_end, next) }) } Node::Repetition(body) => { match_node(body, input, pos, &|body_end| { match_node(node, input, body_end, next) }) || next(pos) } } } fn interp_match(regex: &Node, input: &str) -> bool { let bytes = input.as_bytes(); match_node(regex, bytes, 0, &|pos| pos == bytes.len()) } 现在这个正则表达式引擎相当简单。它不到20行代码,但让我们看看它在性能方面的表现。作为比较,我们将这段代码与为正则表达式特别实现的手写代码进行比较。对于我们的例子,我们将使用正则表达式b(an)*。手写代码如下: fn handwritten_b_an_star(input: &str) -> bool { let bytes = input.as_bytes(); let mut pos = 0; if pos == bytes.len() || bytes[pos] != b'b' { return false; } pos += 1; while pos < bytes.len() { if bytes[pos] != b'a' { return false; } pos += 1; if pos == bytes.len() || bytes[pos] != b'n' { return false; } pos += 1; } true } (你可以通过一些方法优化这段代码,使其更快,但就我们的目的而言,这提供了一个良好的比较。)当我对这两个示例做基准测试时,我发现手写版本比解释器快10到20倍。显然还有很大的改进空间。现在让我们看看如何利用JIT编译获得一个性能与手写版本相当的通用正则表达式引擎。如何进行JIT编译JIT编译代码分为两个步骤。首先,你生成要运行的代码的汇编。一旦你有了代码,接下来将汇编代码打包成一个函数,你可以像调用程序中的其他代码那样调用它。为了生成汇编,我们将使用一种变体的方法,称为复制-粘贴。其理念是我们有一个汇编模板系列,用于我们希望进行JIT编译的不同操作。这些模板被称为“模板”。当我们希望...
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡