いくつかのかなりクールなものを共有したいと思いました。ネストされたサポートを追加した方法など、Python バイトコード について学習してきました。関数はありますが、印刷所の人は 500 ワード以内にする必要があると言っていました。
今週は休日ですと彼は肩をすくめました。 私に何を期待していますか?
コード スニペットを除く、私は交渉しました。
わかりました、彼は譲歩しました。
そもそもなぜバイトコードを使うか知っていますか?
私は印刷機を操作しているだけですが、あなたを信頼しています。
まあまあです。始めましょう。
Rust で書かれた私の Python インタプリタである Memphis には 2 つの実行エンジンがあります。どちらもすべてのコードを実行できるわけではありませんが、一部のコードは実行できます。
私の ツリーウォーク インタプリタ は、自分が何をしているのか分からない場合に構築するものです。 ?♂️ 入力 Python コードをトークン化し、抽象構文ツリー (AST) を生成し、ツリーをたどって 各ノードを評価します。式は値を返し、ステートメントはシンボル テーブルを変更します。シンボル テーブルは、Python スコープ ルールを尊重する一連のスコープとして実装されます。簡単な肺炎 LEGB を覚えておいてください: ローカル、エンクロージング、グローバル、ビルトイン。
私の バイトコード VM は、自分が何をしているのかは分からないが、知っているように行動したい場合に構築するものです。また?♂️。このエンジンでは、トークンと AST は同じように機能しますが、歩くのではなく全力疾走を始めます。 AST を、以降バイトコードとして知られる中間表現 (IR) にコンパイルします。次に、スタックベースの仮想マシン (VM) を作成します。これは概念的には CPU のように機能し、バイトコード命令を順番に実行しますが、完全にソフトウェアで実装されます。
(とりとめのない両方のアプローチの完全なガイドについては、Crafting Interpreters が優れています。)
そもそもなぜこれを行うのでしょうか?携帯性とパフォーマンスという 2 つの P を覚えておいてください。 2000 年代初頭、Java バイトコードの移植性について誰も黙らなかったことを覚えていますか? 必要なのは JVM だけで、どのマシンでもコンパイルされた Java プログラムを実行できます! Python は技術的およびマーケティング上の理由からこのアプローチを採用しませんでしたが、理論的には同じ原則が当てはまります。 (実際には、コンパイル手順が異なり、このワームの缶を開けたことを後悔しています。)
しかし、パフォーマンスが重要です。プログラムの存続期間中に AST を何度も走査するよりも、コンパイルされた IR の方が効率的な表現となります。 AST を繰り返し走査するオーバーヘッドを回避することでパフォーマンスが向上し、そのフラットな構造により、実行時の分岐予測とキャッシュの局所性が向上することがよくあります。
(コンピューター アーキテクチャのバックグラウンドがないのにキャッシュについて考えなかったということを責めるつもりはありません。私はその業界でキャリアをスタートしましたが、キャッシュについて考えることは、回避方法について考えるよりもはるかに少ないです。同じコード行を 2 回書くので、パフォーマンスに関しては私を信頼してください。それが私のリーダーシップ スタイルです。)
やあ、これは 500 単語です。フレームをロードしてリッピングする必要があります。
もう?!コード スニペットを除外しましたか?
コード スニペットはありません。
わかりました、わかりました。あと500個だけ。約束します。
約 1 年前、バイトコードの VM 実装を表にまとめるまでにはかなりの時間がかかりました。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) }
検討は終わりましたか?関数の引数をスタックにロードし、「関数を呼び出します」。バイトコードでは、すべての名前がインデックスに変換されます (VM 実行時のインデックス アクセスの方が速いため) が、ここでローカル インデックスを扱っているのかグローバル インデックスを扱っているのかを知る方法はありません。
次に、改良版について考えてみましょう。
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) }
そのコードをご検討いただきありがとうございます。
入れ子になった関数呼び出しをサポートするようになりました。何が変わったのでしょうか?
compile_load が何をしているのか見てみましょう。
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 にあることがわかります。どちらも、変数と名前のマッピングを含む、Python バイトコードのブロックのメタデータを含む CodeObject 上に存在します。
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) }
varname と names の違い (CPython では、これらを co_varnames と co_names と呼びます) に何週間も悩まされましたが、実際には非常に簡単です。 varnames は、指定されたスコープ内のすべてのローカル変数の変数名を保持し、names はすべての非ローカル変数に対して同じことを行います。
これを適切に追跡すると、他のすべては問題なく機能します。実行時に、VM は LOAD_GLOBAL または LOAD_FAST を認識し、それぞれグローバル名前空間ディクショナリまたはローカル スタックを参照することを認識します。
相棒!グーテンベルク氏が電話中で、もう印刷機を保持できないと言っています。
わかりました!大丈夫!わかった!発送しましょう。 ?
しー!印刷所の人は私が結論を書いていることを知らないので、手短に書きます。
変数のスコープと関数呼び出しがしっかりと確立されたので、スタック トレースや非同期サポートなどの機能に徐々に注意を向けています。バイトコードの詳細を楽しんでいただけた場合、または独自のインタプリタの構築について質問がある場合は、ぜひコメントをお寄せください。
購読して [何もなし] で保存
このような投稿をさらに直接受信トレイに受け取りたい場合は、ここから購読できます!
一緒に働きましょう
私はソフトウェア エンジニアを指導し、協力的な、時にはばかばかしい環境の中で技術的な課題とキャリアの成長を乗り越えていきます。ご興味がございましたら、ここからセッションをご予約ください。
他の場所
私はメンタリングに加えて、自営業や晩期に診断された自閉症を乗り越えた経験についても書いています。コードは減り、ジョークの数は同じです。
以上がPython バイトコードでネストされた関数のサポートを追加した方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。