MENU

【Python】数十倍は速くなる?Cythonで高速化に挑戦!

Pythonはインタープリタ言語であり、柔軟なデータ型を持つため、Pythonの構文のみで構成されたループ処理などは、処理速度が遅くなりがちです。特に大量のデータを扱う場合は、処理の遅さが顕著になります。

このような問題に対し、CythonはPythonコードをC言語に変換してコンパイルすることで、インタープリタ特有のリアルタイムな解析処理を回避し、全体的なパフォーマンスを向上させることができます。

今回は、Pythonで定義された関数をCythonを用いてコンパイルし、Pythonから呼び出す方法について解説します。

目次

Cythonとは

CythonはPythonコードをCに変換し、コンパイル(ビルド)することで高速化を図る拡張言語です。以下の特徴があります:

  • Pythonの文法に近い記述が可能
  • 型指定によるコンパイル最適化
  • NumPyとの親和性が高い
  • Cライブラリとの連携が容易

実行環境によって異なりますが、例えば ythonとCythonの処理速度は以下の差があります。

処理内容純粋PythonCython使用時
1億回の加算ループ約4.2秒約0.8秒

PandasやNumpyなど、もともとC言語で作られているモジュールはCythonで改善できません。あくまでもPythonで作られた純粋なソースコードが高速化の対象となります。

Cythonの使い方

以下の手順に従って進めれば、簡単にCythonが使えるようになります。

STEP
CPythonのインストール

あらかじめ、pip コマンドで cython をインストールしておきます。

pip install cython

STEP
.pyxファイル(Python ラッパー)を作成

高速化したいPythonの関数、又はクラスを記述し、拡張子を .pyx で保存します。

def multiply(int a, int b):
    return a * b
STEP
ビルドスクリプト(setup.py) の作成

setup.py という名前でファイルを作成し、次の内容を記述します。example.pyx の部分は、STEP.2で保存したファイル名を指定してください。

from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("example.pyx")
)
STEP
コンパイル(ビルド)の実行

コマンドプロンプトから setup.pyと .pyx が置かれているフォルダに移動し、下記のコマンドを実行します。

python setup.py build_ext --inplace

ビルドに成功すると、2つのファイルと1つのフォルダが出来上がります。例えば、example.pyx という名前のファイルをコンパイルすると、以下のフォルダとファイルが出来上がります。

STEP
Pythonから呼び出して実行

ここまで問題なく成功したら、モジュール名(ここでは example)と関数名(ここでは multiply)をインポートするだけで使うことができます。

from example import multiply

result = multiply(3, 5)
print(result)  # 出力: 15

.pyxに記述するPythonプログラムの書き方

.pyx ファイルには、通常の Python コードをそのまま記述することも可能ですが、必ずしも十分な高速化が得られるとは限りません

これは、Cython が Python のコードを C/C++ に変換・コンパイルして実行速度を向上させる仕組みであるため、どのように書かれているか=変換結果によって、パフォーマンスに大きな差が出るからです。

Cython の詳細な構文や高度な記述方法については、公式ドキュメント「Welcome to Cython’s Documentation」を参照いただくとして、ここでは実践上知っておくべき重要なポイントに絞って解説します。

通常の Python より厳格な構文が必要


Python の緩い構文とは違い、Cython では型宣言の位置や文法が明確である必要があります。

cdef int i  # 明示的な型定義


Pythonの自由度に慣れていると、構文エラーで足止めを食うことが多くなりがちなので、気を付けましょう。

cdef / cpdef / def を使い分ける

通常の関数は def で定義できますが、Cythonで拡張された cdefcpdef を使うと、高速化や C 言語との連携が可能になります。この時、Python 側から関数を呼び出したい場合は、def または cpdef で定義する必要があります。cdef は高速ですが、Python からは見えないため、外部公開が必要な関数には適しません。

修飾子Python から呼び出しCython 内で使用高速性用途例
def通常のPython関数
cdef×内部専用の高速処理関数
cpdef双方から使いたい関数(公開)

ループ処理を含む関数は cdef で定義することで、C レベルで最適化され、cpdef よりも高速に実行されることが期待できます。ただし、cdef 関数は Python から直接呼び出せないため、Cython 内部専用の処理として設計する必要があります

cdef list calc_with_cdef(int n):
    cdef int i
    cdef list result = []
    for i in range(n):
        result.append(square_c(i))
    return result

型指定でパフォーマンスを引き出す

cdef は関数だけでなく、変数を定義する際にも使用されるキーワードです。Cython の大きな魅力のひとつは、明示的な型指定によって C 言語並みの高速処理が可能になる点にあります。

この恩恵を最大限に活かすには、変数に明確なデータ型を指定する必要があります

Cython では cdef を用いることで、intdoublechar のような C 言語由来の低レベルな型を利用できるようになります。一方で、型を指定しない変数(たとえば x = 1 のようなPython風の記述)は、Python の動的な型(PyObject)として扱われ、Cython 本来の高速化効果は得られません

cdef int i, j
cdef double val[5]

for i in range(1000000):
    for j in range(5):
        val[j] += 1

型指定ありと 型指定なしでは、次のような違いがあります。

特性Python 変数 (x = 1)cdef 変数 (cdef int x = 1)
動的(PyObject)静的(Cレベル int/float 等)
実行速度遅い(仮想マシン経由)速い(Cコードに直接変換)
GIL(Global Lock)必須GILなしで処理可能(条件による)
型チェック実行時に行われるコンパイル時に固定
最適化のしやすさ難しい非常に最適化しやすい
メモリ使用大きめ(PyObjectが必要)小さい(Cスタック上で済む)

Pythonの機能が一部制限される

Cython は C/C++ に変換されるため、Python の一部機能や標準ライブラリに制約が生じる場合があります。

  • with 文、ジェネレータ式、リスト内包表記などは 限定的なサポート にとどまります
  • 正規表現ライブラリ(例:re)や動的インスペクションを行う inspect モジュールは 意図通りに動作しない場面 があります
  • NumPy などの外部ライブラリを高速に扱うには、cimport を使って Cython 向けの専用 API を明示的に利用する必要があります

下記は、NumPy + cimport の基本的なサンプルです。

# distutils: language = c
# cython: boundscheck=False, wraparound=False

import numpy as np
cimport numpy as cnp

def sum_array(np.ndarray[cnp.float64_t, ndim=1] arr):
    cdef Py_ssize_t i, n = arr.shape[0]
    cdef double total = 0

    for i in range(n):
        total += arr[i]

    return total

Cython内で標準関数を呼び出す処理は速くならない

str(val)len(obj)dict.items() などの標準関数は、Python の仮想マシン(VM)を経由して実行されるため、C レベルの最適化が効きません

これらは内部的に 動的ディスパッチ(型チェック+関数呼び出し) を伴うため、cdef で型指定していても高速化の恩恵を打ち消してしまうことがあります。

また、Python オブジェクトを操作する関数は GIL(Global Interpreter Lock)に依存しやすくnogil や並列化の恩恵を受けにくいという制約もあります。

Cython 内で処理を高速化したい場合は、標準関数の使用を最小限に抑え、C レベルの処理で完結するような設計を心がけましょう。また、可能であれば C に近いデータ構造やロジックに置き換えることで、より高い性能が期待できます。

悪いプログラム例

cpdef list use_python_str(double[:] vals):
    cdef Py_ssize_t i, n = vals.shape[0]
    cdef list out = []
    for i in range(n):
        # Pythonのstr()を使用
        out.append(str(vals[i])) 
    return out

良いプログラム例

cpdef list use_c_str(double[:] vals):
    cdef Py_ssize_t i, n = vals.shape[0]
    cdef list out = []
    cdef char buf[32]
    for i in range(n):
        # Cのsprintfで文字列化
        sprintf(buf, "%.5f", vals[i])
        out.append(buf.decode())
    return out

標準関数だけでなく、list.append()dict[key] = value は、Python の内部オブジェクト構造とガベージコレクションに依存しているため、Cythonで型を付けても速くならないケースが多いです。

外部 C 関数やヘッダを利用する

Cython は Python コードを C/C++ に変換してコンパイルする機能を持つため、C/C++ の関数やヘッダファイルを直接利用することも可能です。以下は、簡単な C 関数を Cython から呼び出すサンプル構成です。

ここで .pyx という見慣れない拡張子を持つファイルが登場します。これは、C 言語(または C++)の関数や構造体、型情報などを Cython に伝えるための「宣言ファイル」です。C のヘッダーファイル(.h)と似た役割を果たし、Cython コード(.pyx)から C の機能を使えるようにするための「橋渡し」的な存在です。

ヘッダファイル(mymath.h)

// mymath.h
int multiply(int a, int b);

C実装ファイル(mymath.c)

// mymath.c
int multiply(int a, int b) {
    return a * b;
}

Cython 用 .pxd ファイル(mymath.pxd)

# mymath.pxd 
cdef extern from "mymath.h":
    int multiply(int a, int b)

.pxd には、Cythonで使いたいCの関数定義と、その定義が記述されているヘッダファイルを記述します。

Pythonラッパー(example.pyx)

# example.pyx
from mymath cimport multiply

def py_multiply(int x, int y):
    return multiply(x, y)

ビルドスクリプト(setup.py)

from setuptools import setup
from Cython.Build import cythonize
from setuptools.extension import Extension

extensions = [
    Extension(
        name="example",
        sources=["example.pyx", "mymath.c"],
        include_dirs=["."],  # ヘッダファイルの場所
    )
]

setup(
    ext_modules=cythonize(extensions)
)

setup.py では、Python ラッパーファイル(.pyx)と C 実装ファイル(.c)を明示的に記述しますが、Cython 用の .pxd ファイルは記述不要です。その理由は、.pxdCython によって自動的に読み込まれる補助的な宣言ファイルであり、ビルド対象そのものではないためです。
通常、.pyx ファイルから cimport されると、同じディレクトリ内にある .pxd ファイルが自動的に参照されます。

おすすめのベストプラクティス

  • まずはシンプルな関数やループ処理から cdef を試すのがおすすめです。変数の型指定やループの最適化など、小さなスコープから始めることで、効果や挙動を把握しやすくなります。

  • Jupyter 環境では %%cython マジックを使って素早くプロトタイピングし、動作確認後に .pyx ファイルに落とし込む流れがスムーズです。試行錯誤を高速に回せます。

  • ベンチマークは必ず取りましょう。どの部分が高速化されているのか、また依然としてボトルネックが残っている箇所がどこかを、定量的に把握することが最適化の鍵になります。

まとめ

Python は可読性や柔軟性に優れる一方で、処理速度の面では課題もあります。特にループ処理や型の曖昧さがボトルネックになる場面では、Cython を導入することで劇的な高速化が期待できます。

本記事では、Cython の基本的な使い方から、型指定による最適化、高速な関数定義(cdef/cpdef)、NumPy や C 関数との連携方法まで、実践的なポイントを解説しました。

まずはシンプルな関数やループから Cython を適用し、ベンチマークを取りながら効果を確認していくのがおすすめです。Jupyter の %cython マジックや .pyx ファイルを使った本格的なビルドまで、用途に応じて段階的に取り入れていくのがお勧めです。

「Pythonの柔軟さ」と「C言語の高速性」の“いいとこ取り”ができる Cython を活用して、ボトルネックの解消と処理性能の向上に挑戦してみてはいかがでしょうか。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次