Ich wollte ein paar ziemlich coole Sachen mit Ihnen teilen. Ich habe etwas über Python-Bytecode mit Ihnen gelernt, einschließlich der Frage, wie ich Unterstützung für Nested hinzugefügt habe funktioniert, aber mein Mann von der Druckerei meinte, ich solle die Länge unter 500 Wörtern halten.
Es ist eine Ferienwoche, zuckte er mit den Schultern. Was erwarten Sie von mir?
Ausgenommen Codeschnipsel, ich habe verhandelt.
Gut, er hat nachgegeben.
Wissen Sie, warum wir überhaupt Bytecode verwenden?
Ich bediene nur die Druckmaschine, aber ich vertraue dir.
In Ordnung. Fangen wir an.
Memphis, mein in Rust geschriebener Python-Interpreter, verfügt über zwei Ausführungs-Engines. Keiner von beiden kann den gesamten Code ausführen, aber beide können einen Teil des Codes ausführen.
Mein Treewalk-Interpreter ist das, was Sie bauen würden, wenn Sie nicht wüssten, was Sie tun. ?♂️ Sie tokenisieren den eingegebenen Python-Code, generieren einen abstrakten Syntaxbaum (AST) und gehen dann durch den Baum und bewerten jeden Knoten. Ausdrücke geben Werte zurück und Anweisungen ändern die Symboltabelle, die als eine Reihe von Bereichen implementiert ist, die den Python-Bereichsregeln entsprechen. Denken Sie nur an das einfache pneumonische LEGB: lokal, umschließend, global, eingebaut.
Meine Bytecode-VM ist das, was Sie erstellen würden, wenn Sie nicht wüssten, was Sie tun, sich aber so verhalten würden, wie Sie es tun. Auch ?♂️. Bei dieser Engine funktionieren die Token und AST gleich, aber anstatt zu laufen, beginnen wir mit dem Sprinten. Wir kompilieren den AST in eine Zwischendarstellung (IR), im Folgenden als Bytecode bekannt. Anschließend erstellen wir eine stapelbasierte virtuelle Maschine (VM), die konzeptionell wie eine CPU fungiert und Bytecode-Anweisungen nacheinander ausführt, jedoch vollständig in Software implementiert ist.
(Für eine vollständige Anleitung beider Ansätze ohne viel Geschwafel ist Crafting Interpreters ausgezeichnet.)
Warum machen wir das überhaupt? Denken Sie nur an die beiden Ps: Portabilität und Leistung. Erinnern Sie sich, dass in den frühen 2000er Jahren niemand darüber schweigen wollte, wie portierbar Java-Bytecode ist? Sie benötigen lediglich eine JVM und können ein auf jedem Computer kompiliertes Java-Programm ausführen! Python hat sich sowohl aus technischen als auch aus Marketinggründen gegen diesen Ansatz entschieden, aber theoretisch gelten dieselben Prinzipien. (In der Praxis sind die Kompilierungsschritte unterschiedlich und ich bereue es, diese Büchse voller Würmer geöffnet zu haben.)
Leistung ist jedoch das Wichtigste. Anstatt einen AST während der Lebensdauer eines Programms mehrmals zu durchlaufen, ist die kompilierte IR eine effizientere Darstellung. Wir sehen eine verbesserte Leistung durch die Vermeidung des Mehraufwands für das wiederholte Durchlaufen eines AST, und seine flache Struktur führt oft zu einer besseren Verzweigungsvorhersage und Cache-Lokalität zur Laufzeit.
(Ich mache es Ihnen nicht übel, dass Sie nicht über Caching nachdenken, wenn Sie keinen Hintergrund in der Computerarchitektur haben – zum Teufel, ich habe meine Karriere in dieser Branche begonnen und denke viel weniger über Caching nach, als darüber, wie ich es vermeiden kann Ich schreibe die gleiche Codezeile zweimal. Das ist mein Führungsstil: blindes Vertrauen.
Hey Kumpel, das sind 500 Wörter. Wir müssen den Rahmen beladen und loslegen.
Schon?! Sie haben Codeausschnitte ausgeschlossen?
Es gibt keine Codeschnipsel, mein Mann.
Okay, okay. Nur noch 500. Ich verspreche es.
Ich bin schon ziemlich weit gekommen, bevor ich vor etwa einem Jahr meine Bytecode-VM-Implementierung vorgestellt habe: Ich konnte Python-Funktionen und -Klassen definieren, diese Funktionen aufrufen und diese Klassen instanziieren. Ich habe dieses Verhalten mit einigen Tests eingedämmt. Aber ich wusste, dass meine Implementierung chaotisch war und dass ich die Grundlagen noch einmal durchgehen musste, bevor ich weitere lustige Dinge hinzufügen konnte. Jetzt ist Weihnachtswoche und ich möchte lustige Sachen hinzufügen.
Betrachten Sie dieses Snippet zum Aufrufen einer Funktion und behalten Sie dabei das TODO im Auge.
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) }
Sind Sie mit dem Nachdenken fertig? Wir laden die Funktionsargumente auf den Stack und „rufen die Funktion auf“. Im Bytecode werden alle Namen in Indizes umgewandelt (da der Indexzugriff während der VM-Laufzeit schneller ist), aber wir haben nicht wirklich eine Möglichkeit zu wissen, ob es sich hier um einen lokalen Index oder einen globalen Index handelt.
Betrachten Sie nun die verbesserte Version.
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) }
Vielen Dank, dass Sie diesen Code berücksichtigt haben.
Wir unterstützen jetzt verschachtelte Funktionsaufrufe! Was hat sich geändert?
Werfen wir einen Blick darauf, was „compile_load“ macht.
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)) } } }
Hier gelten mehrere Grundprinzipien:
Das Letzte, was ich Ihnen heute überlasse, ist ein Blick darauf, wie diese Variablennamen zugeordnet werden. Im folgenden Codeausschnitt werden Sie feststellen, dass lokale Indizes in code.varnames und nichtlokale Indizes in code.names zu finden sind. Beide basieren auf einem CodeObject, das die Metadaten für einen Block Python-Bytecode enthält, einschließlich seiner Variablen- und Namenszuordnungen.
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) }
Der Unterschied zwischen Varnames und Names hat mich wochenlang gequält (CPython nennt diese co_varnames und co_names), aber eigentlich ist er ziemlich einfach. Varnames enthält die Variablennamen für alle lokalen Variablen in einem bestimmten Bereich, und Names macht dasselbe für alle nichtlokalen Variablen.
Sobald wir dies richtig verfolgen, funktioniert alles andere einfach. Zur Laufzeit sieht die VM ein LOAD_GLOBAL oder ein LOAD_FAST und weiß, dass sie im globalen Namespace-Wörterbuch bzw. im lokalen Stack suchen muss.
Kumpel! Herr Gutenberg ist am Telefon und sagt, wir können die Presse nicht länger zurückhalten.
Okay! Bußgeld! Ich verstehe es! Lass es uns versenden. ?
Shh! Der Druckmaschinenmann weiß nicht, dass ich eine Schlussfolgerung schreibe, deshalb werde ich mich kurz fassen.
Da Variablenbereich und Funktionsaufrufe an einem festen Platz sind, wende ich mich nach und nach Funktionen wie Stack-Traces und asynchroner Unterstützung zu. Wenn Ihnen dieser Einblick in Bytecode gefallen hat oder Sie Fragen zum Erstellen Ihres eigenen Interpreters haben, würde ich gerne von Ihnen hören – hinterlassen Sie einen Kommentar!
Abonnieren und sparen [bei nichts]
Wenn Sie weitere Beiträge wie diesen direkt in Ihrem Posteingang erhalten möchten, können Sie sich hier anmelden!
Arbeite mit mir
Ich betreue Softwareentwickler bei der Bewältigung technischer Herausforderungen und der beruflichen Weiterentwicklung in einem unterstützenden, manchmal albernen Umfeld. Bei Interesse können Sie hier eine Sitzung buchen.
Woanders
Zusätzlich zum Mentoring schreibe ich auch über meine Erfahrungen beim Umgang mit der Selbstständigkeit und spät diagnostiziertem Autismus. Weniger Code und die gleiche Anzahl an Witzen.
Das obige ist der detaillierte Inhalt vonWie ich Unterstützung für verschachtelte Funktionen im Python-Bytecode hinzugefügt habe. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!