__pycache__ とは何か?(関連してPythonでのJIT Compilerについて)
前提
本記事のすべての内容はCpython実装を元にしています。
__pycache__
とは何か?
python実行時に出てくる 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を導入しようとする動きがある。
実際にpython3.13から実験的なJIT Compile機能が追加された。
ただ、このJIT Compilerは一般的に使われるJIT Compilerとは違う、CopyAndPatch JIT Compilerというものだ。
CopyAndPatch JITは既存のJITとどう違うのか?
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を使うには?
現在は初期段階で、本格的な利用は推奨されていない。
ここのREADMEを参考に、CPythonをBuildし試すことができる。
Discussion