🙌

__pycache__ とは何か?(関連してPythonでのJIT Compilerについて)

2024/12/27に公開

前提

本記事のすべての内容はCpython実装を元にしています。

python実行時に出てくる __pycache__とは何か?

Pythonが勝手に作成するCacheディレクトリのことで、中には*.pycファイルが入っている。

その目的は名前からしてCacheファイルを想像させるため、何かの結果物をCacheにし、高速化を意図しているのではないかということが想像できる。

その実態をこれから先で掘り下げていく。

*.pyc はどのように作られるのか?

それには、Pythonがどう起動するかを知る必要がある

まず、python main.pyを実行すると、我々が書いたCodeが py_compile[1] によってpycというファイルに変換される。それを、Python Interpreterが読み取り、順番に実行していく。

*.pyc の中身は何か?

Cacheファイルであり、人が読むことを想定していないため、Binaryファイルになっている。その内部はHeaderとByteCodeの領域で分離されている。

  • Header

    この中にはByteCodeに変換する際に使われたPythonのVersionや、このByteCodeを作成する元になった.py のHashなどが入っている。(python3.7まではTimestampが使われていた)このHashやTimestampの情報を元に、Pythonは.pyが変更されたかを確認し、ByteCodeを更新させる。

  • ByteCode

    PythonのmarshalというModuleによってSerializeされたByteCodeのデータが入っている。Marshal関数でLoadし、Dis Moduleで人が読める形に変換ができる。

    これらを人が読める形にすると、下のような情報になる。この情報がPythonのInterpreterへとそのまま渡され、順番に実行されることになる。

      2           0 LOAD_FAST                0 (a)
                  2 LOAD_FAST                1 (b)
                  4 BINARY_ADD
                  6 RETURN_VALUE
    
    

    このデータを我々の書くPythonコードに変換すると下記のようになる。

    def add(a, b):
        return a + b
    

ByteCodeって機械語みたいなやつ?

見た目は似ているが、ByteCodeは高レベルで実行される。つまりCPUやアーキテクチャとは関係がなく、一つの命令語が数千の機械語に変換される可能性もある。

また、CPUやアーキテクチャとは関係がないため、ByteCodeはCrossPlatformということにもなる。

PythonにCompilerがあるの?

PythonはInterpreter言語とはよく呼ばれるが、定義そのままのInterpreter言語ではない。これによって、Pythonは毎回コードをTranslateして実行することなく、Complile済みのByteCodeからコードを実行することができる。

他の言語とは何が違う?

  • 他のInterpreter言語(JavaScriptやRubyなど)

    通常、これらの言語はコードを実行する際に文法検査や最適化などを一連の流れで行う。
    そして必要な場合、JIT Compilerなどによる最適化の作業も含まれる。
    しかし、これらの作業はすべてInterpreterの中で行われるため、「どこまでがコンパイルなのか」を明確に区分することは難しい(Rubyの場合、YetAnotherRubyVMのようなVMが存在し、ByteCodeに変換する過程は行うが、明示的にそれを扱うのではなく、そのまま実行するなど)

  • Pythonの場合

    Pythonの場合はここの区分が明確で、Interpreterを起動する前にByteCodeとしてCompileを行う。
    このCompileのタイミングで文法や、最適化などが行われる。
    Interpreterは渡されたByteCodeをそのまま実行するだけで、これによってInterpreterでの実行のタイミングにおける性能のメリットが得られる。
    その上、一回CompileされたByteCodeをファイルシステム上にCaching(__pycache__/*.pyc)しておくことによって、将来的な性能向上が可能になる。

PythonにおけるJIT Compilerの話

そもそもJIT Compilerとは?

ここで少し名前が出てきたJIT(JustInTime)Compilerとは、InterpreterからCPUに渡すための機械語を生成しながら、それらをCachingし再利用するCompilerのことである。

例えば、Loopの中で特定コードを1000回実行するとなった場合、JIT CompilerなしではLoopされる特定コードは毎回Intepreterによって機械語に翻訳されることになる。

対してJIT Compilerを使用すると、一定回数以上同じコード(HotSpot)がInterpreterに渡された場合にその機械語の翻訳結果をCachingし、この翻訳過程を将来的にスキップ可能にしてくれる。

  • メリット
    • これによって、長期的なコードの場合、性能が向上される。
  • デメリット
    • 実行タイミングでCompileを行うため、実行開始時に遅延が生じる

PythonではJIT Compileはやらないの?

十分に導入の余地がある。
世間一般的な認識として、Pythonは遅いと言われている。PythonでのByteCodeはCachingによって実行時の速度向上にはなるが、長期的な性能向上には向いていない。他のJIT Compilerを採用しているInterpreter言語と比べても確実に遅いと言える。
そこで、PythonにもJIT Compilerを導入しようとする動きがある。

https://peps.python.org/pep-0744/

実際にpython3.13から実験的なJIT Compile機能が追加された。

ただ、このJIT Compilerは一般的に使われるJIT Compilerとは違う、CopyAndPatch JIT Compilerというものだ。

CopyAndPatch JITは既存のJITとどう違うのか?

https://dl.acm.org/doi/10.1145/3485513

Interpreter言語でのJITとは一般的に実装されていくによってだんだんどういうコードが実行されているかを確認し、状況によってもっと効率的な機械語を徐々に生成していく。

CopyAndPatchはこれとは違う方法で最適化を行う。

名前通り、渡されたコードをコピーし、コピーされたところにルールベース[2] での変更(ここでのPatch)でコードを書き換え、それを実行させることになる。

例えば、ルールの一つとして、Loopの中で特定の値が頻繁に扱われる場合、その値を機械語レベルでのConstantとしてPythonのInterpreterから扱えるように書き換える(CopyAndPatch)ことで、Pythonはその変数を探し、適切な場所に移すという行動をしなくてよくなり、そこからの性能向上を目指す。

ただし、特定のパターンや条件下での最適化になるため、汎用性は少なく、力を発揮できない場合も出てくる。

なぜCopyAndPatch?

JITのPEPによると、JIT Compiler導入によるOverheadを最小限にするため、そして既存のCPythonのコードと簡単に統合できるから、と述べられている(実際、CPythonでのJIT実装は1400行(しかならない)程度のコードで実装されている)

また、既存のJIT Compilerを使う場合、JIT CompilerのBuildのためLLVMなどの重いDependencyが必要になるが、別途のDependencyなしで導入でき、性能を改善できる。(ただし、現時点ではCPython Build Timeでの一部の作業にLLVMを必要とする)

どのぐらい早くなる?

2−9%程度[3] の性能改善を見せているらしい。

ただし、現在のJIT実装は性能最適化ではなく、JITそのものを実装することに集中している。よって、将来的にもっと早くなる可能性は十分存在する。

PythonでJIT Compilerを使うには?

現在は初期段階で、本格的な利用は推奨されていない。

https://github.com/python/cpython/tree/main/Tools/jit

ここのREADMEを参考に、CPythonをBuildし試すことができる。

脚注
  1. https://docs.python.org/ja/3/library/py_compile.html ↩︎

  2. https://github.com/python/cpython/blob/ebcc578dff47b1dcffb634923bedc5361c8f29f6/Tools/jit/_stencils.py#L49 ↩︎

  3. https://github.com/python/cpython/pull/113465#issuecomment-1876225775 ↩︎

株式会社AVILEN

Discussion