POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Anthony Shaw

本記事は、原著者の許諾のもとに翻訳・掲載しております。

(編注:2020/08/18、いただいたフィードバックをもとに記事を修正いたしました。)

Pythonは高い人気を誇り、DevOps、データサイエンス、Web開発、セキュリティの分野で使われています。

しかし、速度に関しては高い評価が全くありません。

JavaとC、C++、C#、Pythonの速度を比べるには、どうしたらいいのでしょう? 答えは、実行するアプリケーションのタイプに大きく左右されます。完璧なベンチマークはありませんが、[手始めに比べる手段](https://algs4.cs.princeton.edu/faq/)としてはThe Computer Language Benchmarks Gameが適しています。

私は10年ほどthe Computer Language Benchmarks Gameを参照していますが、Java、C#、Go、JavaScript、C++などの他言語と比べると、Pythonは 最も遅い言語の1つ です。このプロジェクトには、 実行時コンパイラ (C#、Java)や 事前コンパイラ (C、C++)、JavaScriptなどのインタプリタ言語も含まれています。

注意:本稿で「Python」と言及する場合、言語のリファレンス実装、CPythonを指します。本稿では別の実装の実行時間についても言及します。

他の言語と比べ、Pythonが同程度のアプリケーションを完了させるのに2~10倍の時間がかかる場合、その理由は何でしょう? 速くすることはできないのでしょうか? 私は、その疑問に答えたいと思います。

以下の定説が最もよく挙げられます。

  • “GIL(グローバルインタプリタロック)であるため”
  • “インタプリタ言語で、コンパイルされないため”
  • “動的型付き言語であるため”

この中で、パフォーマンスに最も大きい影響を与える要因はどれでしょう?

“GILであるため”

最近のコンピュータは、複数のコアを持つCPUを搭載し、中にはマルチプロセッサを有する製品もあります。この追加された処理能力を全て使うために、オペレーティングシステムがスレッドと呼ばれる低レベルな構造を定義します。そこでは1つのプロセスで(Chromeブラウザなど)大量のスレッドを生成し、システムへの命令を内部に有することが可能です。この方法では、特にCPUの使用率が高いプロセスの場合、複数のコアで負荷を分担できるため、大半のアプリケーションで効率的に早くタスクを完了させることができます。

本稿の執筆中、私のChromeブラウザは44のスレッドを開いていました。スレッドを処理する構造とAPIは、POSIX規格のオペレーティングシステム(Mac OSやLinuxなど)とWindows OSで異なります。また、オペレーティングシステムはスレッドのスケジューリングも管理します。

まだマルチスレッドのプログラミングをした経験がないなら、ロックという概念について急いで学ばなければいけません。シングルスレッドのプロセスと異なり、メモリ内の変数の値を変える際に、マルチスレッドが同じメモリアドレスに同時にアクセス/変更を試みないようにする必要があります。

CPythonは、変数を生成する際にメモリを割り当て、その変数への参照がいくつ存在するか数えます。これは参照カウントと呼ばれる概念です。参照の回数が0であれば、そのメモリはシステムから解放されます。このおかげで、例えばFor文の範囲内で”一時”変数が生成されても、アプリケーションのメモリ消費が爆発的に増えることはありません。

問題は、マルチスレッド内で変数が共有された時にCPythonが参照カウントをロックする方法です。”グローバルインタプリタロック”という方法では、スレッドの実行が慎重に制御されます。スレッドの数に関わらず、インタプリタは一度に1つのオペレーションしか実行できません。

GILはPythonのアプリケーションのパフォーマンスにどんな影響を及ぼすのでしょうか

シングルスレッド、シングルインタプリタのアプリケーションがあるとします。 これだと速度は全く変わりません。 GILを取り除いても、コードのパフォーマンスは影響を受けないでしょう。

マルチスレッドを使って1つのインタプリタ内(Pythonプロセス)で同時並行処理をさせようとして、それが入出力のみを行うスレッド(ネットワークI/OやディスクI/Oなど)であれば、GILの競合を招くことになるはずです。


David BeazleyによるGILの可視化に関する記事 http://dabeaz.blogspot.com/2010/01/python-gil-visualized.html

Webアプリケーション(Django)でWSGIを使用していて、Webアプリケーションへの各リクエストが 別々の Pythonインタプリタであれば、 1つの リクエストにつきロックは1つだけとなります。Pythonインタプリタは起動に時間がかかるため、WSGIの実装には、 Python のプロセスを継続させる “デーモンモード”がある場合があります。

他のPythonの実行時間はどうでしょうか

PyPyはGILを持ち 、一般的に速さはCPythonの3倍以上です。

JythonはGILを持ちません 。Jython内のPythonスレッドはJavaスレッドで表されているため、JVMのメモリ管理システムを利用できるためです。

JavaScriptはどうしているのでしょうか

まず、全てのJavaScriptエンジンは、マーク・アンド・スイープ・ガベージ・コレクションを採用しています。既述のとおり、GILを基本的に必要とするのは、CPythonのメモリ管理アルゴリズムです。

JavaScriptはGILを持ちませんが、 シングル スレッドなのでGILを必要としません。JavaScriptのイベントループとパターンのPromise/コールバックが、非同期プログラミングで同時並行処理を行うための手法です。Pythonにも非同期のイベントループと似た方法があります。

“インタプリタ言語であるため”

これはよく聞く理由です。CPythonの実際の動きを総合的に簡略化した説明だと思います。もしターミナルで python myscript.py と入力したら、CPythonは、コードの読み込み、字句解析、構文解析、コンパイル、逐次解釈、実行と、長い一連の処理を始めるでしょう。

プロセスがどのように機能するかに興味があれば、以前、私が書いた記事をお読みください。

[TABLE]

プロセスにおいて重要な点は、コンパイラを使う段階で .pyc ファイルが生成されることです。バイトコードは、Python 3の __pycache__/ 配下、もしくはPython 2と同じディレクトリ内のファイルに書かれます。これはスクリプトには適用されませんが、サードパーティモジュールを含む、インポートした全てのコードに適用されます。

そのためPythonは、ほとんどの時間を(一度しか実行しないコードを書くのでなければ)バイトコードをインタプリタで処理してローカル実行することに費やします。JavaやC#.NETと比較してみましょう。

Javaが”中間言語”にコンパイルし、java仮想マシンがバイトコードを読み、 実行時 に機械語へコンパイルします。.NETの共通中間言語(CIL)も同じで、.NETの共通言語ランタイム(CLR)が実行時に機械語へコンパイルします。
ところで、仮想マシンと、ある種のバイトコードを使う全ての場合において、PythonはJavaとC#の両者と比べ、どうしてこれほどベンチマークが遅いのでしょうか。まず.NETとJavaがJITコンパイラであるいうことです。

JITつまり実行時コンパイルは、コードをチャンク(あるいはフレーム)に分解するために中間言語を必要とします。事前(AOT)コンパイラはあらゆるインタラクションが実行される前に、CPUがコードの全ての行を解釈ができることを確保するように設計されています。

まだ同じバイトコードシーケンスを実行しているので、JITそれ自体で実行時間が短くなることは絶対にありません。しかし、JITは実行時に最適化を行うことができます。良いJITオプティマイザはアプリケーションのどの部分が数多く実行されているかが分かります。この部分を「ホットスポット」呼びます。この箇所をより効率的なバージョンに置き換えることにより、コードの一部を最適化します。

つまり、アプリケーションが同じことを何度も繰り返せば、かなり高速になり得るということです。またJavaやC#は厳密に型指定された言語なので、オプティマイザはコードに関して、はるかに多くの想定を行うということを、覚えておいてください。

PyPyはJITを有していますし、 前項で述べたように、CPythonに比べて非常に高速です。このパフォーマンスベンチマークの詳細については以下をご参照ください。

[TABLE]

では、どうしてCPythonはJITを使用しないのでしょう

JITに対するマイナス面が存在します。そのうちの1つが起動時間です。CPythonの起動時間は、すでに比較的遅いのですが、PyPyを起動するにはCPythonよりもさらに2~3倍の時間がかかります。Javaの仮想マシンは、ブートが遅いことで有名です。.NET CLRはシステムの立ち上げから始めることで、この遅さを回避しています。しかし、CLRの開発者は、CLRが動作するオペレーティングシステムも開発しています。

長時間実行中の単一のPythonのプロセスがあり、”ホットスポット”を含んでいて最適化できるコードがあるなら、JITで大きな成果を望めるでしょう。

しかし、CPythonは汎用の実装です。そのため、Pythonを用いてコマンドラインアプリケーションを開発していたとしたら、CLIが呼び出されるたびにJITが開始されるのを待たなければならず、とてつもなく時間がかかります。

CPythonはできるだけ多くのユースケースを試行しなければなりません。 CPython対応のJITプラグイン が生まれる可能性もありましたが、このプロジェクトはほぼ行き詰まり状態にあります。

JITの恩恵を受けたくて、それに見合うワークロードがあるなら、PyPyを使いましょう。

“動的型付き言語であるが故”

“静的型付き”言語では、変数の宣言時に、型を明示する必要があります。このような言語としてC、 C++、 Java、 C#、 Goが挙げられます。

動的型付き言語では、型の概念は存在していますが、変数の型は動的です。

a = 1
a = "foo"

この単純な例では、Pythonは同じ名前で str 型の変数をもう1つ作り、 a の最初のインスタンスのために作成されたメモリを開放します。

静的型付き言語は、苦労を強いるために設計されたのではなく、CPUを動作させる方法により設計されているのです。最終的に全てを簡単なバイナリオペレーションとして扱う必要があるなら、オブジェクトと型とを低レベルのデータ構造に変換する必要があります。

Pythonはこれをやってくれます。決して見ることもなければ、気にする必要もありません。

型の宣言が不要だからPythonが遅くなるのではなく、Python言語の設計により、ほとんどどんなことでも動的に行うことができるのです。実行時にオブジェクトのメソッドを置き換えることができます。また、実行時に宣言された値に対して低レベルのシステムコールにモンキーパッチを適用することが可能です。ほとんどどんなことでもできます。

この設計が、Pythonの最適化を 非常に難しく しているのです。

要点を説明するため、DTraceと呼ばれているMac OS上で動作するシステムコールのトレースツールを使います。CPythonのディストリビューションにはDTraceのビルトインは付属していません。そのため、CPythonをリコンパイルする必要があります。今回のデモでは3.6.6を使用します。

unzip v3..6.zip
cd v3.6.6
./configure --with-dtrace
Make

これで、 python.exe のコード全般でDTraceのトレーサが使えるようになりました。 Paul RossはDTraceに関する素晴らしいライトニングトークを書いています 。関数の呼び出しや実行時間、CPU時間、システムコール、その他あらゆるものを測定するために、Python用の DTraceのスターターファイルをダウンロード することができます。

sudo dtrace -s toolkit/.d -c ‘../cpython/python.exe script.py’

py_callflow のトレーサはアプリケーションで呼び出す関数を全て表示してくれます。

本当に、Pythonの動的型付きは遅くなるのでしょうか。

  • 型の比較と変換は高コストです。変数を読み込んだり書き込んだり、あるいは参照するたびに、型のチェックが行われます。
  • かなり動的な言語を最適化するのは難しいことです。Pythonの代替物の多くがPythonよりもかなり速い理由は、パフォーマンスという名目で、柔軟性という点において妥協しているからです。

Cython は、Cの静的型とPythonとを組み合わせ、型が既知であるコードの最適化を図っており、 パフォーマンスを84倍向上させることができます

まとめ

Pythonは、動的な性質と多用途性が要因となり、もともと時間がかかる。Pythonはあらゆる問題に対するツールとして使用される中で、より最適化され、より速い代替品がおそらく利用できるようになるだろう。

しかし、非同期性を活用したり、プロファイリングのツールを理解したり、複数のインタプリタの使用を検討するなどして、Pythonのアプリケーションを最適化する方法は存在します。

起動時間が重要でなく、コードがJITに恩恵をもたらすアプリケーションなら、PyPyを検討してみてください。

パフォーマンスが重要で、より多くの静的型付き変数を使用する一部のコードの場合には、 Cython の使用を考えてみましょう。

参考文献

Jake VDPの素晴らしい記事(少し古いです)
[https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/\]https://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/()

GILについてのDave Beazleyの講演
http://www.dabeaz.com/python/GIL.pdf

JITコンパイラの全て
https://hacks.mozilla.org/2017/02/a-crash-course-in-just-in-time-jit-compilers/