生命週期是 Rust 和人類經驗的一個迷人特徵。這是一個技術博客,所以我們專注於前者。誠然,我在利用 Rust 的生命週期安全地借用資料方面採用得很慢。在孟菲斯(我用Rust 編寫的Python 解釋器)的樹形行走實現中,我幾乎不利用生命週期(通過不斷克隆),並且盡可能地反复躲避借用檢查器(通過使用內部可變性,也不斷) 。
我的 Rustaceans 同胞們,我今天來這裡是為了告訴你們這一切現在結束了。讀懂我的嘴唇……不再有捷徑。
好吧好吧,說實話。什麼是捷徑,什麼是正確的方法,是優先事項和視角的問題。我們都犯過錯誤,我來這裡是為了為我的錯誤負責。
在我第一次安裝 rustc 六週後,我開始寫一個解釋器,因為我沒有冷靜。撇開這些長篇大論和故作姿態,讓我們開始今天的講座,了解如何使用生命週期作為我們的生命線來改進我臃腫的解釋器代碼庫。
Rust 生命週期是一種提供編譯時保證的機制,確保任何引用都不會比它們所引用的物件的壽命長。它們使我們能夠避免 C 和 C 的「懸空指標」問題。
這是假設您完全利用它們!當您想要避免與管理生命週期相關的複雜性時,克隆是一種方便的解決方法,但缺點是增加了記憶體使用量,並且每次複製資料時都會出現輕微的延遲。
使用生命週期也迫使你更慣用地思考 Rust 中的所有者和借用,這是我渴望做的。
我選擇了我的第一個候選作為來自 Python 輸入檔的標記。當我坐在 Amtrak 上時,我最初的實現嚴重依賴 ChatGPT 指導,使用了以下流程:
複製令牌流的便利之處在於,在步驟 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 模式產生了很大的影響。讓我們看看它是什麼樣子的!
在我上面描述的流程中,還記得我如何提到一旦解析器創建了自己的標記副本就可以刪除詞法分析器嗎?這無意中影響了我的 Builder 的設計,它的目的是成為支援編排 Lexer、Parser 和 Interpreter 互動的元件,無論您是從 Python 文字流還是 Python 檔案的路徑開始。
正如您在下面看到的,此設計還有一些其他不理想的方面:
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。
我選擇相信這是正確的方法,主要是為了維護我的自尊。最終,我的目標是編寫清晰反映軟體和電腦工程原理的程式碼。我們現在可以打開孟菲斯來源,指向代幣的單一所有者,晚上可以睡個好覺了!
如果您想將更多類似的貼文直接發送到您的收件匣,您可以在這裡訂閱!
除了指導軟體工程師之外,我還寫了我在自營職業和晚期診斷自閉症方面的經驗。更少的程式碼和相同數量的笑話。
以上是提高工作口譯員的記憶效率的詳細內容。更多資訊請關注PHP中文網其他相關文章!