首页 > 后端开发 > Python教程 > 提高工作口译员的记忆效率

提高工作口译员的记忆效率

Susan Sarandon
发布: 2024-12-26 13:30:10
原创
364 人浏览过

Improving memory efficiency in a working interpreter

生命周期是 Rust 和人类经验的一个迷人特征。这是一个技术博客,所以我们重点关注前者。诚然,我在利用 Rust 的生命周期安全地借用数据方面采用得很慢。在孟菲斯(我用 Rust 编写的 Python 解释器)的树形行走实现中,我几乎不利用生命周期(通过不断克隆),并且尽可能地反复躲避借用检查器(通过使用内部可变性,也不断)。

我的 Rustaceans 同胞们,我今天来这里是为了告诉你们这一切现在结束了。读懂我的嘴唇……不再有捷径。

好吧好吧,说实话吧。什么是捷径,什么是正确的方法,是一个优先事项和视角的问题。我们都犯过错误,我来这里是为了为我的错误承担责任。

在我第一次安装 rustc 六周后,我开始编写一个解释器,因为我没有冷静。抛开这些长篇大论和故作姿态,让我们开始今天的讲座,了解如何使用生命周期作为我们的生命线来改进我臃肿的解释器代码库。

识别和避免克隆数据

Rust 生命周期是一种提供编译时保证的机制,确保任何引用都不会比它们所引用的对象的寿命长。它们使我们能够避免 C 和 C 的“悬空指针”问题。

这是假设您完全利用它们!当您想要避免与管理生命周期相关的复杂性时,克隆是一种方便的解决方法,但缺点是增加了内存使用量,并且每次复制数据时都会出现轻微的延迟。

使用生命周期还迫使你更惯用地思考 Rust 中的所有者和借用,这是我渴望做的。

我选择了我的第一个候选作为来自 Python 输入文件的标记。当我坐在 Amtrak 上时,我最初的实现严重依赖 ChatGPT 指导,使用了以下流程:

  1. 我们将 Python 文本传递给构建器
  2. 构建器创建一个词法分析器,它对输入流进行标记
  3. 然后,构建器创建一个解析器,它克隆令牌流以保存自己的副本
  4. 构建器用于创建一个解释器,它反复向解析器请求下一个解析语句并对其进行评估,直到我们到达令牌流的末尾

克隆令牌流的便利之处在于,在第 3 步之后可以自由删除词法分析器。通过更新我的架构,让词法分析器拥有令牌,而解析器只借用它们,现在需要保留词法分析器活得更久。 Rust 生命周期将为我们保证这一点:只要解析器存在并持有对借用令牌的引用,编译器就会保证拥有这些令牌的词法分析器仍然存在,从而确保有效的引用。

就像所有代码一样,这最终是一个比我预期更大的变化。让我们看看为什么!

新的解析器

在更新解析器以从词法分析器借用令牌之前,它看起来像这样。今天讨论的两个感兴趣的领域是 tokens 和 current_token。我们不知道 Vec 有多大。是,但它显然是我们的(即我们没有借用它)。

pub struct Parser {
    state: Container<State>,
    tokens: Vec<Token>,
    current_token: Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self {
        let current_token = tokens.first().cloned().unwrap_or(Token::Eof);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
登录后复制
登录后复制

从 Lexer 借用代币后,它看起来相当相似,但现在我们看到了 LIFETIME!通过将标记连接到生命周期 'a,Rust 编译器将不允许标记的所有者(即我们的词法分析器)和标记本身被删除,而我们的解析器仍然引用它们。这感觉安全又别致!

static EOF: Token = Token::Eof;

/// A recursive-descent parser which attempts to encode the full Python grammar.
pub struct Parser<'a> {
    state: Container<State>,
    tokens: &'a [Token],
    current_token: &'a Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl<'a> Parser<'a> {
    pub fn new(tokens: &'a [Token], state: Container<State>) -> Self {
        let current_token = tokens.first().unwrap_or(&EOF);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
登录后复制
登录后复制

您可能会注意到的另一个小差异是这一行:

static EOF: Token = Token::Eof;
登录后复制

这是一个小的优化,当我的解析器朝着“内存高效”的方向发展时,我就开始考虑这个优化。新模型允许我仅实例化一个令牌并重复引用 &EOF,而不是每次解析器需要检查它是否位于文本流末尾时都实例化一个新的 Token::Eof。

同样,这是一个小优化,但它说明了每条数据在内存中只存在一次并且每个消费者在需要时引用它的更大思维方式,Rust 既鼓励你这样做,又紧握着你的手路。

说到优化,我真的应该对前后的内存使用情况进行基准测试。既然我没有,我对此事无话可说。

正如我之前提到的,将 Lexer 和 Parser 的生命周期结合在一起对我的 Builder 模式产生了很大的影响。让我们看看它是什么样子的!

新的构建器:MemphisContext

在我上面描述的流程中,还记得我如何提到一旦解析器创建了自己的标记副本就可以删除词法分析器吗?这无意中影响了我的 Builder 的设计,它的目的是成为支持编排 Lexer、Parser 和 Interpreter 交互的组件,无论您是从 Python 文本流还是 Python 文件的路径开始。

正如您在下面看到的,此设计还有一些其他不理想的方面:

  1. 需要调用危险的向下转型方法来获取解释器。
  2. 为什么我认为可以将解析器返回到每个单元测试,然后将其传回interpreter.run(&mut parser)?!
fn downcast<T: InterpreterEntrypoint + 'static>(input: T) -> Interpreter {
    let any_ref: &dyn Any = &input as &dyn Any;
    any_ref.downcast_ref::<Interpreter>().unwrap().clone()
}

fn init(text: &str) -> (Parser, Interpreter) {
    let (parser, interpreter) = Builder::new().text(text).build();

    (parser, downcast(interpreter))
}


#[test]
fn function_definition() {
     let input = r#"
def add(x, y):
    return x + y

a = add(2, 3)
"#;
    let (mut parser, mut interpreter) = init(input);

    match interpreter.run(&mut parser) {
        Err(e) => panic!("Interpreter error: {:?}", e),
        Ok(_) => {
            assert_eq!(
                interpreter.state.read("a"),
                Some(ExprResult::Integer(5.store()))
            );
        }
    }
}
登录后复制

下面是新的 MemphisContext 接口。该机制在内部管理 Lexer 生命周期(使我们的引用保持足够长的存活时间,以使我们的解析器满意!)并且仅公开运行此测试所需的内容。

pub struct Parser {
    state: Container<State>,
    tokens: Vec<Token>,
    current_token: Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl Parser {
    pub fn new(tokens: Vec<Token>, state: Container<State>) -> Self {
        let current_token = tokens.first().cloned().unwrap_or(Token::Eof);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
登录后复制
登录后复制

context.run_and_return_interpreter() 仍然有点笨重,并且涉及到我将来可能要解决的另一个设计问题:当您运行解释器时,您是否只想返回最终返回值或允许您访问任意值的东西从符号表?该方法选择后一种方法。我实际上认为有一个案例可以做到这两点,并且会不断调整我的 API 以允许这样做。

顺便说一句,这一变化提高了我评估任意一段 Python 代码的能力。如果您还记得我的 WebAssembly 传奇,我当时必须依靠我的交叉检查 TreewalkAdapter 来做到这一点。现在,我们的 Wasm 界面更加简洁。

static EOF: Token = Token::Eof;

/// A recursive-descent parser which attempts to encode the full Python grammar.
pub struct Parser<'a> {
    state: Container<State>,
    tokens: &'a [Token],
    current_token: &'a Token,
    position: usize,
    line_number: usize,
    delimiter_depth: usize,
}

impl<'a> Parser<'a> {
    pub fn new(tokens: &'a [Token], state: Container<State>) -> Self {
        let current_token = tokens.first().unwrap_or(&EOF);
        Parser {
            state,
            tokens,
            current_token,
            position: 0,
            line_number: 1,
            delimiter_depth: 0,
        }
    }
}
登录后复制
登录后复制

接口 context.evaluate_oneshot() 返回表达式结果而不是完整的符号表。我想知道是否有更好的方法来确保任何“oneshot”方法只能在上下文中运行一次,从而确保没有消费者在有状态上下文中使用它们。我会继续努力!

这值得吗?

孟菲斯首先是一次学习练习,所以这绝对值得!

除了在 Lexer 和 Parser 之间共享标记之外,我还创建了一个接口来评估 Python 代码,并且样板代码明显减少。虽然共享数据带来了额外的复杂性,但这些变化带来了明显的好处:减少内存使用,通过更严格的生命周期管理提高安全保证,以及更易于维护和扩展的简化 API。

我选择相信这是正确的方法,主要是为了维护我的自尊。最终,我的目标是编写清晰反映软件和计算机工程原理的代码。我们现在可以打开孟菲斯源,指向代币的单一所有者,晚上可以睡个好觉了!

订阅并保存[无任何]

如果您想将更多类似的帖子直接发送到您的收件箱,您可以在这里订阅!

别处

除了指导软件工程师之外,我还写了我在自营职业和晚期诊断自闭症方面的经验。更少的代码和相同数量的笑话。

  • 湖效应咖啡,第一章 - From Scratch dot org

以上是提高工作口译员的记忆效率的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板