我想分享一些非常酷的東西我一直在和你學習Python字節碼,包括我如何添加對嵌套的支援函數,但我的印刷工說我需要將其控制在500 字以內。
這是一個假期週,他聳聳肩。 你希望我做什麼?
不包含程式碼片段,我討價還價了。
好吧,他讓出。
你知道我們為什麼要使用字節碼嗎?
我只是操作印刷機,不過我相信你。
很公平。讓我們開始吧。
Memphis,我用 Rust 寫的 Python 解釋器,有兩個執行引擎。兩者都不能運行所有程式碼,但都可以運行部分程式碼。
我的treewalk 解釋器 是您在不知道自己在做什麼的情況下建造的。 ?♂️ 將輸入的 Python 程式碼標記,產生抽象語法樹 (AST),然後遍歷樹並評估每個節點。表達式傳回值和語句修改符號表,符號表實作為一系列遵守 Python 作用域規則的作用域。只要記住簡單的肺炎 LEGB:局部、封閉、全域、內建。
我的字節碼虛擬機是如果你不知道自己在做什麼但想像你一樣行事時你會建構的。還有? ♂️。對於這個引擎,令牌和 AST 的工作方式相同,但我們不是步行,而是衝刺。我們將 AST 編譯為中間表示(IR),以下稱為字節碼。然後,我們建立一個基於堆疊的虛擬機器 (VM),它在概念上類似於 CPU,按順序執行字節碼指令,但它完全由軟體實作。
(對於這兩種方法的完整指南,沒有漫無目的,《Crafting Interpreters》非常好。)
我們首先為什麼要這樣做?只要記住兩個 P:便攜性和性能。還記得在 2000 年代初期,沒有人會對 Java 字節碼的可移植性閉嘴嗎? 您只需要一個 JVM,就可以執行在任何機器上編譯的 Java 程式! 出於技術和行銷原因,Python 選擇不採用這種方法,但理論上同樣的原則適用。 (實際上,編譯步驟是不同的,我很遺憾打開了這罐蠕蟲。)
性能才是最重要的。編譯後的 IR 是一種更有效的表示形式,而不是在程式的生命週期內多次遍歷 AST。我們看到,由於避免了重複遍歷 AST 的開銷,效能得到了提高,而且其扁平結構通常會在運行時帶來更好的分支預測和快取局部性。
(如果你沒有電腦架構背景,我不會責怪你沒有考慮快取——哎呀,我的職業生涯是從這個行業開始的,我對緩存的考慮遠遠少於我對如何避免的考慮編寫同一行程式碼兩次。
嘿夥計,這是 500 字。我們需要加載框架並讓其撕裂。
已經? !您排除了程式碼片段嗎?
沒有程式碼片段,老兄。
好吧好吧。只剩下500多了。我保證。
上下文對於 Python 變數很重要考慮使用此程式碼片段來呼叫函數,並密切注意 TODO。
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let (_, index) = self.get_local_index(name); // TODO how does this know if it is a global or local index? this may not be the right // approach for calling a function opcodes.push(Opcode::Call(index)); Ok(opcodes) }
現在考慮改進版本。
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![self.compile_load(name)]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let argc = opcodes.len() - 1; opcodes.push(Opcode::Call(argc)); Ok(opcodes) }
我們現在支援巢狀函數呼叫!發生了什麼變化?
fn compile_load(&mut self, name: &str) -> Opcode { match self.ensure_context() { Context::Global => Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)), Context::Local => { // Check locals first if let Some(index) = self.get_local_index(name) { return Opcode::LoadFast(index); } // If not found locally, fall back to globals Opcode::LoadGlobal(self.get_or_set_nonlocal_index(name)) } } }
今天我要留給您的最後一件事是看看這些變數名稱是如何映射的。在下面的程式碼片段中,您會注意到在 code.varnames 中找到本機索引,在 code.names 中找到非本地索引。兩者都存在於 CodeObject 上,其中包含 Python 字節碼塊的元數據,包括其變數和名稱映射。
fn compile_function_call( &mut self, name: &str, args: &ParsedArguments) ) -> Result<Bytecode, CompileError> { let mut opcodes = vec![]; // We push the args onto the stack in reverse call order so that we will pop // them off in call order. for arg in args.args.iter().rev() { opcodes.extend(self.compile_expr(arg)?); } let (_, index) = self.get_local_index(name); // TODO how does this know if it is a global or local index? this may not be the right // approach for calling a function opcodes.push(Opcode::Call(index)); Ok(opcodes) }
varnames 和 name 之間的差異折磨了我好幾個星期(CPython 稱這些為 co_varnames 和 co_names),但它實際上相當簡單。 varnames 保存給定範圍內所有局部變數的變數名稱,names 保存所有非局部變數的變數名稱。
一旦我們正確追蹤這一點,其他一切都會正常工作。在運行時,VM 會看到 LOAD_GLOBAL 或 LOAD_FAST,並知道分別查看全域命名空間字典或本機堆疊。
老兄!古騰堡先生打電話說我們不能再按了。
好的!美好的!我得到它!我們發貨吧。 ?
噓!印刷工不知道我在寫結論,所以我會簡短地說。
透過固定的變數作用域和函數調用,我逐漸將注意力轉向堆疊追蹤和非同步支援等功能。如果您喜歡深入了解字節碼或對構建自己的解釋器有疑問,我很樂意聽取您的意見 - 發表評論!
訂閱並節省[無任何]
如果您想將更多類似的貼文直接發送到您的收件匣,您可以在這裡訂閱!
跟我一起工作
我指導軟體工程師在一個有時有些愚蠢的支持性環境中應對技術挑戰和職業發展。如果您有興趣,可以在這裡預約課程。
其他地方
除了指導之外,我還寫了我在自營職業和晚期診斷自閉症方面的經驗。更少的程式碼和相同數量的笑話。
以上是我如何在 Python 字節碼中添加對嵌套函數的支持的詳細內容。更多資訊請關注PHP中文網其他相關文章!