検索
ホームページバックエンド開発Python チュートリアルPython バイトコードでネストされた関数のサポートを追加した方法

How I added support for nested functions in Python bytecode

いくつかのかなりクールなものを共有したいと思いました。ネストされたサポートを追加した方法など、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個だけ。約束します。

Python 変数はコンテキストが重要

約 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)
}
</bytecode>

検討は終わりましたか?関数の引数をスタックにロードし、「関数を呼び出します」。バイトコードでは、すべての名前がインデックスに変換されます (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)
}
</bytecode>

そのコードをご検討いただきありがとうございます。

入れ子になった関数呼び出しをサポートするようになりました。何が変わったのでしょうか?

  1. Call オペコードは、関数のインデックスではなく、いくつかの位置引数を取るようになりました。これにより、関数を呼び出す前にスタックからポップオフする引数の数が VM に指示されます。
  2. スタックから引数をポップした後、関数自体はスタック上に残り、compile_load はすでにローカル スコープとグローバル スコープを処理しています。

LOAD_GLOBAL と LOAD_FAST の比較

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))
        }
    }
}

ここではいくつかの重要な原則が機能しています:

  1. 現在のコンテキストに基づいて照合します。 Python セマンティクスに従って、Context::Global は任意のモジュール (スクリプトのエントリポイントだけでなく) のトップレベルにあり、Context::Local は任意のブロック (つまり、関数定義またはクラス定義) 内にあると考えることができます。
  2. ここで、ローカル インデックスと非ローカル インデックスを区別します。 (インデックス 0 がさまざまな場所で何を参照しているかを解読しようとして気が狂ってしまったので、型付き整数を導入しました。LocalIndex と NonlocalIndex は、型指定されていない符号なし整数に型安全性を提供します。これについては将来書くかもしれません!)
  3. 指定された名前のローカル変数が存在するかどうかはバイトコードのコンパイル時にわかります。存在しない場合は、実行時にグローバル変数を検索します。これは、Python に組み込まれたダイナミズムを物語っています。関数の実行時までにそのモジュールのグローバル スコープに変数が存在する限り、その値は実行時に解決できます。ただし、この動的な解像度にはパフォーマンスへの影響が伴います。ローカル変数のルックアップはスタック インデックスを使用するように最適化されていますが、グローバル ルックアップではグローバル名前空間ディクショナリを検索する必要があるため、時間がかかります。この辞書は名前とオブジェクトのマッピングであり、オブジェクト自体がヒープ上に存在する可能性があります。 「グローバルに考え、ローカルに行動する」という格言を誰が知っていたでしょうか。実際には Python スコープを参照していましたか?

変数名には何が含まれますか?

今日最後に説明するのは、これらの変数名がどのようにマップされるかを見てみることです。以下のコード スニペットでは、ローカル インデックスが 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)
}
</bytecode>

varname と names の違い (CPython では、これらを co_varnames と co_names と呼びます) に何週間も悩まされましたが、実際には非常に簡単です。 varnames は、指定されたスコープ内のすべてのローカル変数の変数名を保持し、names はすべての非ローカル変数に対して同じことを行います。

これを適切に追跡すると、他のすべては問題なく機能します。実行時に、VM は LOAD_GLOBAL または LOAD_FAST を認識し、それぞれグローバル名前空間ディクショナリまたはローカル スタックを参照することを認識します。

相棒!グーテンベルク氏が電話中で、もう印刷機を保持できないと言っています。

わかりました!大丈夫!わかった!発送しましょう。 ?

メンフィスの次は何でしょうか?

しー!印刷所の人は私が結論を書いていることを知らないので、手短に書きます。

変数のスコープと関数呼び出しがしっかりと確立されたので、スタック トレースや非同期サポートなどの機能に徐々に注意を向けています。バイトコードの詳細を楽しんでいただけた場合、または独自のインタプリタの構築について質問がある場合は、ぜひコメントをお寄せください。


購読して [何もなし] で保存

このような投稿をさらに直接受信トレイに受け取りたい場合は、ここから購読できます!

一緒に働きましょう

私はソフトウェア エンジニアを指導し、協力的な、時にはばかばかしい環境の中で技術的な課題とキャリアの成長を乗り越えていきます。ご興味がございましたら、ここからセッションをご予約ください。

他の場所

私はメンタリングに加えて、自営業や晩期に診断された自閉症を乗り越えた経験についても書いています。コードは減り、ジョークの数は同じです。

  • 湖効果コーヒー、第 2 章 - スクラッチドット組織から

以上がPython バイトコードでネストされた関数のサポートを追加した方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
Pythonリストに要素をどのように追加しますか?Pythonリストに要素をどのように追加しますか?May 04, 2025 am 12:17 AM

toAppendElementStoapyThonList、usetheappend()methodforsingleelements、extend()formultipleElements、andinsert()forspecificopsitions.1)useappend()foraddingoneElementatheend.2)useextend()toaddmultipleelementseffictience.3)

Pythonリストをどのように作成しますか?例を挙げてください。Pythonリストをどのように作成しますか?例を挙げてください。May 04, 2025 am 12:16 AM

To CreateapythonList、usesquareBrackets []およびSeparateItemswithcommas.1)listsaredynamicandcanholdmixdatatypes.2)useappend()、remaid()、andslicingformanipulation.3)listcompreheNsionsionsionsionsionsionsionsionsionsionsionsionsionsionsionsionsionsientionforcreating.4)

数値データの効率的なストレージと処理が重要な実際のユースケースについて話し合います。数値データの効率的なストレージと処理が重要な実際のユースケースについて話し合います。May 04, 2025 am 12:11 AM

金融、科学研究、医療、およびAIの分野では、数値データを効率的に保存および処理することが重要です。 1)財務では、メモリマッピングされたファイルとnumpyライブラリを使用すると、データ処理速度が大幅に向上する可能性があります。 2)科学研究の分野では、HDF5ファイルはデータストレージと取得用に最適化されています。 3)医療では、インデックス作成やパーティション化などのデータベース最適化テクノロジーがデータのパフォーマンスを向上させます。 4)AIでは、データシャーディングと分散トレーニングがモデルトレーニングを加速します。システムのパフォーマンスとスケーラビリティは、適切なツールとテクノロジーを選択し、ストレージと処理速度の間のトレードオフを検討することにより、大幅に改善できます。

Pythonアレイをどのように作成しますか?例を挙げてください。Pythonアレイをどのように作成しますか?例を挙げてください。May 04, 2025 am 12:10 AM

pythonarraysarasarecreatedusingthearraymodule、notbuilt-inlikelists.1)importthearraymodule.2)specifytheTypecode、emg。、 'i'forintegers.3)Arraysofferbettermemoreefficiency forhomogeneousdatabutlasefutablethanlists。

Shebangラインを使用してPythonインタープリターを指定するための選択肢は何ですか?Shebangラインを使用してPythonインタープリターを指定するための選択肢は何ですか?May 04, 2025 am 12:07 AM

Shebangラインに加えて、Pythonインタープリターを指定するには多くの方法があります。1。コマンドラインから直接Pythonコマンドを使用します。 2。バッチファイルまたはシェルスクリプトを使用します。 3. makeやcmakeなどのビルドツールを使用します。 4. Invokeなどのタスクランナーを使用します。各方法には利点と短所があり、プロジェクトのニーズに合った方法を選択することが重要です。

リストと配列の選択は、大規模なデータセットを扱うPythonアプリケーションの全体的なパフォーマンスにどのように影響しますか?リストと配列の選択は、大規模なデータセットを扱うPythonアプリケーションの全体的なパフォーマンスにどのように影響しますか?May 03, 2025 am 12:11 AM

forhandlinglaredataSetsinpython、usenumpyArrays forbetterperformance.1)numpyarraysarememory-effictientandfasterfornumericaloperations.2)nusinnnnedarytypeconversions.3)レバレッジベクトル化は、測定済みのマネージメーシェイメージーウェイズデイタイです

Pythonのリストと配列にメモリがどのように割り当てられるかを説明します。Pythonのリストと配列にメモリがどのように割り当てられるかを説明します。May 03, 2025 am 12:10 AM

inpython、listsusedynamicmemoryallocation with allocation、whilenumpyArraysalocatefixedmemory.1)listsallocatemorememorythanneededededinitivative.2)numpyArrayasallocateexactmemoryforements、rededicablebutlessflexibilityを提供します。

Pythonアレイ内の要素のデータ型をどのように指定しますか?Pythonアレイ内の要素のデータ型をどのように指定しますか?May 03, 2025 am 12:06 AM

inpython、youcanspecthedatatypeyfelemeremodelernspant.1)usenpynernrump.1)usenpynerp.dloatp.ploatm64、フォーマーpreciscontrolatatypes。

See all articles

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

Dreamweaver Mac版

Dreamweaver Mac版

ビジュアル Web 開発ツール

VSCode Windows 64 ビットのダウンロード

VSCode Windows 64 ビットのダウンロード

Microsoft によって発売された無料で強力な IDE エディター

EditPlus 中国語クラック版

EditPlus 中国語クラック版

サイズが小さく、構文の強調表示、コード プロンプト機能はサポートされていません

AtomエディタMac版ダウンロード

AtomエディタMac版ダウンロード

最も人気のあるオープンソースエディター