最近ではPythonは人工知能・深層学習・Web開発など、今流行りの様々な分野で使用されているプログラミング言語です。

しかしPythonの唯一の欠点は実行速度が遅いことだといわれています。PyPyはこの問題を解決する最適な技術です。

本記事前半ではPyPyとは何かを言語処理系の違いから説明していきます。またメリット・デメリットについても注目しましょう。

後半ではPyPyのインストール方法に始まり、ベンチマークの測定方法と結果について説明していきます。


PyPyとは

PyPy(パイパイ)はプログラミング言語Pythonの処理系です。公式サイトトップには「CPythonより4.4倍速く実行できる」と謳われています。

元々はPsycoというプロジェクトでJITコンパイラの開発が進められていました。Psycoの後継プロジェクトがPyPyです。

CPythonの代替処理系と位置付けられるこの処理系は、RPythonで記述されていますがCPythonと互換性があります。

…とここまで詳細に説明すると、知らない単語ばかりでよく分からないかもしれません。

まずは、順番に用語の定義と意味を確認していきましょう。


言語処理系とは

言語処理系はインタプリタ・コンパイラの2種類に分けられます。

PyPyとPythonで何故このような速度差が出るのかきちんと理解するためには、各々がどの言語処理系かを正しく把握する必要があるでしょう。

既に知っているのであれば飛ばしても構いません。知識が浅い方はこの機会にきちんと整理しておきましょう。


インタプリタ

あるプログラミング言語で記述されたソースコードを変換・処理した後、何らかの操作を行うソフトウェアです。

具体的には多くのスクリプト言語がこれに当てはまるでしょう。

インタプリタはソースコードを逐次的に解釈・実行します。このため実行速度はコンパイラに比べて遅くなるでしょう。


コンパイラ

コンパイラはプログラミング言語で綴られたソースコードを機械語に変換するソフトウェアです。

特に他のプログラミング言語で書かれたプログラムに変換する類のコンパイラをトランスコンパイラと呼びます。

具体的にはgcc(GNU Compiler Collection)です。

コンパイラはソースコードを一括で変換するため時間がかかりますが、変換後のプログラムはインタプリタに比べ高速で動作します。


JITコンパイラ

JITコンパイラ(Just-In-Time Compiler・実行時コンパイラ)はインタプリタのように動作するコンパイラです。

インタプリタより速く、コンパイラよりは遅く動作します。


Pythonの言語処理系

Pythonの言語処理系は基本的にはインタプリタであり、PyPyだけが少々独特なカテゴリに分類されるようです。

PyPyを含むPythonの言語処理系について確認していきましょう。


CPythonとは

C言語で実装されたPythonインタプリタです。このためPython以外にもC言語やFortranなどのライブラリを利用できます。

通常、Pythonを実行するときに使用しているのはCPythonです。

CPythonはPythonのソースコードを中間バイトコードにコンパイルし、これをCPython仮想マシンが実行します。


Jythonとは

Javaで実装されたPythonインタプリタです。

JythonはPythonからJavaのライブラリを利用したり、逆にPythonのプログラムをJavaで使用できるようにしたりできます。


IronPythonとは

C#で実装されたPythonインタプリタです。Pythonと.NET Frameworkの両方のライブラリを利用することができます。

「Python Tools for Visual Studio」という拡張機能を使用すれば、Visual Studio環境でIronPythonを扱えるため便利です。


PyPyとは

RPython(Restricted Python)で実装されたPythonインタプリタ・コンパイラツールチェーンに分類されます。

つまりCPythonはインタプリタですが、PyPyはコンパイラとしての機能も持つツールです。

「言語処理系とは」で解説した理屈でいえば、一度コンパイルさえしてしまえばPyPyの実行速度の方が原理上速くなるということになります。


RPythonとは

RPythonは動的プログラミング言語用のインタプリタ・仮想マシンを実装するためのフレームワークです。

このためPyPy以外にも多くの動的プログラミング言語の言語処理系がRPythonによって開発されています。

例えばTopazはRPythonによって書かれたRubyの言語処理系です。

Pythonの制限付きサブセット言語なので、RPythonのコードはCPythonでも実行可能になっています。


PyPyのメリット

PyPyのメリットについて見ていきましょう。

  • JITコンパイラによる高速実行
  • メモリ消費量の軽減
  • Stackless Pythonのサポート


JITコンパイラによる高速実行

JITコンパイルによる関数・モジュール単位の変換機構により、プログラムの実行速度が大幅に向上します。

大規模で複雑なPythonアプリケーションにPyPyを採用することで、より高速な動作を実現可能です。


メモリ消費量の軽減

通常PyPyを利用するとCPythonで実行したときと比べ、メモリ消費量は低く抑えられるようになります。

ただしこの減少量はプログラムにより異なるでしょう。


Stackless Pythonのサポート

Stackless Python(スタックレス・パイソン)はC言語の呼び出しスタックへの依存を回避するPythonのインタプリタです。

このためCPythonよりも効率的にスレッドを実行します。


PyPyのデメリット

PyPyのデメリットについて見ていきましょう。

  • 標準搭載ではない
  • CPythonと動作が異なる場合がある
  • CPythonより遅くなる場合がある


標準搭載ではない

PyPyはCPythonのForkではなくCPythonにマージできません。このため標準搭載ではなく、追加でインストールが必要な状態です。

Python自体は多種多様な使われ方を想定して実装せねばなりません。

PyPyのJITコンパイラの起動時間を考えても、標準機能として搭載するのは合理的ではないでしょう。

またバグ発生率もCPythonより高いため、導入時には注意が必要かもしれません。


CPythonと動作が異なる場合がある

PyPyで実行すると機能しない場合があります。CPythonと互換性があるといっても、一部の処理は非互換的です。

CPython拡張に依存するコードではオーバーヘッドが発生し、適切に実行されない場合があります。

加えて拡張モジュールのサポートがありません。


CPythonより遅くなる場合がある

PyPyを使用したからといって必ずしもCPythonより高速になるとは限らないので気を付けて下さい。

簡易なスクリプト・グルーコード・再帰処理などではPythonよりも処理が遅くなる可能性があります。

何事も適材適所ですので使い分けていきましょう。


PyPyのインストール方法

Ubuntu環境におけるPyPyのインストールは簡単です。

  1. $ python3 -V
  2. Python 3.6.8

pypy3をインストールするにはSnapを使用します。SnapはLinuxディストリビューションに寄らないパッケージ管理システムです。

16.04 LTS以降にはデフォルトでインストールされています。Docker環境には存在せず、インストールもできないためご注意下さい。

  1. $ snap install –classic pypy3
  2. pypy3 7.3.0 from The PyPy Project (pypyproject) installed
  3. $ pypy3 -V
  4. Python 3.6.9 (1608da62bfc7, Dec 23 2019, 10:50:04)
  5. [PyPy 7.3.0 with GCC 7.3.1 20180303 (Red Hat 7.3.1-5)]


PyPyとPythonの速度比較

PyPyとPythonの速度比較は公式サイトで確認して下さい。

いずれのグラフも実数値ではなく、CPythonを1.00とした場合の比較です。

グラフの見方に注意しましょう。


PyPyはどんな具合に速い?

「How fast is PyPy?」にはCPythonを1.00としたとき、代わりにPyPyを使用するとどの程度早くなるかが表現されています。

この棒グラフの値が小さいほど高速化の効果が期待でき、一部を除きほとんどのアプリケーションでは半分以下の値になっていることが分かるでしょう。


PyPyはどのような進化を辿った?

「How has PyPy performance evolved over time?」は、PyPyがバージョンアップを経てどの程度高速化されたかです。

こちらは棒グラフの値が大きいほど高速化されたことを意味しており、PyPy 7.0以降は常に4倍を超える数字を叩き出しています。


Pythonでベンチマークを取る

PyPyを使用すると一般的には高速化が実現できることが分かりました。

では実際に作成したアプリケーションで計測を行うにはどうすれば良いでしょうか。


timeモジュールを使用する

手軽な方法はtimeモジュールを使用したものです。

  1. import time
  2. start = time.time()
  3. # 処理
  4. end = time.time()
  5. result = end – start
  6. print(result)


timeitモジュールを使用する

より厳密に実行時間を計測したい場合はtimeitモジュールを使います。

  1. $ python -m timeit “処理”


Benchmarkerを利用する

BenchmarkerはPython専用のベンチマーク計測ツールです。

Benchmarkerを使用するとコンパイラのバージョン・プラットフォーム・CPUの情報などを出力してくれます。

またランキングやマトリクスの表示にも対応しており、簡単に多くの情報を得ることが可能です。


PyPyとPythonでベンチマーク比較

PyPyとPythonでベンチマークテストを行ってみましょう。今回はtimeモジュールを使用した方法を採用しました。

以降繰り返しを避けるため、「timeモジュールを使用する」に記載したコードの処理の部分のみを記載します。

自分で計測を行う場合は随時当てはめて実行下さい。


for文

for文をX回実行してみましょう。

  1. # インクリメントしながらX回ループする
  2. num = 0
  3. for i in range(X):
  4.     num+=1

純粋なfor文ではなく間に変数のインクリメントを追加してみます。

Xの箇所は適宜好みの回数に変更して下さい。あまり大きくしすぎると直ぐに終わりませんのでご注意願います。

Xを1万回にした場合は以下です。PyPyの方が遅い結果になってしまいました。

  1. $ python3 loop.py
  2. 0.0013737678527832031
  3. $ pypy3 loop.py
  4. 0.0025763511657714844

今度はXを100万回にしてみます。

  1. $ python3 loop.py
  2. 0.13154816627502441
  3. $ pypy3 loop.py
  4. 0.00431370735168457

すると今度はPyPyがとても速くなりました。

このようにPyPyは同じ処理を繰り返すほど効果を発揮する処理系です。


再帰処理(フィボナッチ数列)

今度は遅くなってしまう例として再帰処理を取り上げます。

  1. # フィボナッチ数列を求める
  2. def Fib(n):
  3.     if n==1:
  4.         return 0
  5.     elif n==2:
  6.         return 1
  7.     else:
  8.         return Fib(n-1)+Fib(n-2)
  9. Fib(20)

以下のような結果になりました。

  1. $ python3 fib.py
  2. 0.0028519630432128906
  3. $ pypy3 fib.py
  4. 0.03036952018737793


おわりに

Python周辺の実装についての理解は進んだでしょうか。またPythonにおけるベンチマークの取り方は把握できましたか。

見る機会は多くても実際に自分で計測することはあまりないため、この機会になんとなくで良いのでイメージが掴んでおきましょう。