Cython・Codon・Mojo実測比較|Pythonコードは何倍速くなるか
Pythonの実行速度を上げる手段として名前が挙がるのが、Cython・Codon・Mojoの3つです。ただ、どれを選ぶと何倍速くなるのかを、同じマシン・同じコード・バージョン明記で比べた資料はほとんどありません。この記事では素数カウントを題材にCythonとCodonを実際にビルドして計測し、Mojoについては動作要件から今回の計測機で起動できなかった理由と現況を示します。処理時間と倍率に加えて、数値が合わなくなる互換性の落とし穴まで実測値で扱います。
まとめ|計測結果と選び分けの結論
200万件の素数カウント(CPython 3.14.6で31.7秒)を、書き換え量の少ない順に並べると次のようになりました。Codonはソースを1文字も変えずに23.3倍、Cythonは型宣言を書いて20.8倍、並列化まで踏み込むと100〜200倍に届きます。
| 実行方法 | 中央値 | 対CPython | コード変更 |
|---|---|---|---|
| CPython 3.14.6 | 31.708 秒 | 1.0 倍 | — |
| Cython 3.2.9(型宣言なし) | 28.899 秒 | 1.10 倍 | なし |
| Cython 3.2.9(cdef で型宣言) | 1.523 秒 | 20.8 倍 | 型宣言を追加 |
| Codon 0.19.6(codon run -release) | 1.361 秒 | 23.3 倍 | なし |
| Codon 0.19.6(@par で並列化) | 0.285 秒 | 111 倍 | 1行追加 |
| Cython 3.2.9(prange で並列化) | 0.150 秒 | 211 倍 | 関数分割+型宣言 |
Mojoはこの表に載せていません。macOSでの動作要件がApple silicon(M1〜M5)に限定されており、計測に使ったIntel Macでは起動できないためです。詳細は後段で扱います。
選び分けの結論はこうなります。既存のPythonスクリプトを書き換えたくないならCodon、CPythonの資産(pipで入れたライブラリ)と同居させるならCython、Mojoは2026年8月時点でまだ1.0.0b2のベータであり本番採用は見送りが妥当です。以下、計測条件から順に根拠を示します。
計測条件|再現できる形での測定方法
実行環境と各ツールのバージョン
ベンチマークの数字は環境を書かなければ意味を持ちません。今回の計測環境は次のとおりです。
| 項目 | 内容 |
|---|---|
| 機種 | MacBook Pro 16,1 |
| CPU | Core i9-9880H 2.30GHz(8コア16スレッド) |
| OS | macOS 26.6.1(x86_64) |
| Python | CPython 3.14.6(JIT非搭載・GIL有効) |
| Cython | 3.2.9(2026-07-24公開) |
| Codon | 0.19.6(2026-03-04公開) |
| Cコンパイラ | Apple clang 21.0.0 |
Pythonはコンパイル言語ではなくインタプリタ言語なので、ループ1周ごとにバイトコードの解釈とオブジェクト操作が挟まります。ここで比べる3つは、その層を機械語に置き換える手段という点で共通しています。なおCPython 3.14には実験的なJITコンパイラが入りましたが、今回のビルドでは sys._jit.is_available() がFalseを返し、JITは無効でした。純粋なインタプリタ実行との比較になります。JITの詳細はPython 3.14の新機能まとめ|JITコンパイラ・フリースレッド版(GIL撤廃)・t-stringを最新版で解説で扱っています。
計測に使ったコードと手順
題材は試し割り法による素数カウントです。整数演算とループだけで構成され、外部ライブラリに依存しないため、インタプリタのオーバーヘッドがそのまま差として出ます。
import time, sys
def count_primes(limit):
n = 0
for x in range(2, limit):
i = 2
is_p = True
while i * i <= x:
if x % i == 0:
is_p = False
break
i += 1
if is_p:
n += 1
return n
limit = int(sys.argv[1])
t = time.perf_counter()
r = count_primes(limit)
print(r, time.perf_counter() - t)
limit は 2,000,000 とし、正解は 148,933 個です。各方式で5回以上(Cythonの2方式は7回)実行し、中央値を採用しました。以降に載せるコードはすべて、掲載した形のままビルドして計測しています。
実測結果|31.7秒が0.15秒になるまでの内訳
逐次実行での処理時間と倍率
まず並列化なしの結果です。ここで最も注意すべきなのは、Cythonの「型宣言なし」の行です。
| 実行方法 | 最小〜最大(秒) | 中央値 | 対CPython |
|---|---|---|---|
| CPython 3.14.6 | 31.218 〜 32.253 | 31.708 | 1.0 倍 |
| Cython(型宣言なし) | 26.092 〜 29.057 | 28.899 | 1.10 倍 |
| Cython(cdef 型宣言) | 1.491 〜 1.540 | 1.523 | 20.8 倍 |
| Codon(run -release) | 1.262 〜 1.397 | 1.361 | 23.3 倍 |
Pythonコードをそのまま .pyx に置いてCythonでコンパイルしただけでは、31.7秒が28.9秒になっただけでした。1.10倍。体感できる差ではありません。Cコードに変換されても、変数がPythonオブジェクトのままなら整数の足し算1回ごとにオブジェクト生成と参照カウント操作が走るため、インタプリタ実行とほとんど変わらないわけです。
一方、同じコードをCodonに渡すと、1行も書き換えずに1.361秒まで落ちました。Codonは実行前にプログラム全体の型を推論して機械語まで落とすため、Pythonオブジェクトを介さずネイティブの64ビット整数で計算します。「コンパイルすれば速くなる」のではなく「型が確定すれば速くなる」ということが、この2行の対比に出ています。
並列化した場合の処理時間
16スレッドを使い切ったときの結果です。Cythonの prange が0.150秒、Codonの @par が0.285秒でした。
| 実行方法 | 最小〜最大(秒) | 中央値 | 対CPython |
|---|---|---|---|
| Cython prange(OpenMP) | 0.148 〜 0.154 | 0.150 | 211 倍 |
| Codon @par | 0.265 〜 0.298 | 0.285 | 111 倍 |
Cythonのほうが約1.9倍速い結果ですが、この差は書く量とトレードオフです。Codon側で必要だったのは、最初に載せた count_primes のループ直前に @par を1行足すことだけでした。
def count_primes(limit):
n = 0
@par
for x in range(2, limit):
i = 2
is_p = True
while i * i <= x:
if x % i == 0:
is_p = False
break
i += 1
if is_p:
n += 1
return n
対してCython側は、後述するリダクション変数の制約のためループ本体を別関数へ切り出す必要があります。1行で済ませたいならCodon、最後の1.9倍を取りにいくならCython、という判断になります。
スレッド数はどちらも環境変数 OMP_NUM_THREADS で制御できます。上のコードを1/4/16スレッドで各3回計測した中央値は 1.538秒/0.759秒/0.285秒でした。4スレッドで2.0倍、16スレッドで5.4倍です。物理8コアの機械なので、スレッド数に比例した伸びにはなりません。Cythonのprange版も同様に測ると1スレッドで1.204秒、16スレッドで0.150秒で、こちらは8.0倍でした。
NumPyを使う処理で差が出る条件
「NumPyで書いてあるコードもコンパイラで速くなるのか」は、実務でよく引っかかる論点です。結論から言うと、処理の中身によって結果が正反対になります。
| 処理 | CPython + NumPy 2.5.2 | Codon 0.19.6 | 差 |
|---|---|---|---|
| 4000×4000 行列積 | 1.851 秒 | 1.948 秒 | ほぼ同じ |
| 2000万要素の x*x + 2x の総和 | 0.907 秒 | 0.331 秒 | 2.7 倍 |
行列積で差が出ないのは当然で、どちらも最終的に同じBLAS実装を呼んでいるからです。本機のNumPy 2.5.2は numpy.show_config() でBLAS/LAPACKともにaccelerateとなっており、Pythonの層はその呼び出しを渡しているだけです。そこを速くしても意味がありません。
差が出たのは要素ごとの演算を連ねた式のほうです。CPythonのNumPyは x * x で1つ、2.0 * x でもう1つ、計2000万要素の一時配列を作ってから足し合わせます。Codon 0.19系はNumPyをコンパイラ組み込みで実装し直しており、こうした式を1つのループへ融合して中間配列を作りません。NumPy中心のコードでCodonを試す価値があるのは、行列積のような単発の重い呼び出しではなく、要素演算を何段も重ねている箇所だと判断できます。
Cythonの速度を決めるのは型宣言|宣言なしは1.1倍止まり
cdef宣言の有無で20倍差がつく理由
CythonはPythonの文法にC言語の型宣言を足した言語で、書いたコードをCへ変換してからPythonの拡張モジュールにビルドします。先の表で1.10倍だったコードに型宣言を足したものが次です。追加したのは引数の型指定 int limit と cdef の3行だけで、アルゴリズムは1文字も変えていません。
# cython: language_level=3, boundscheck=False, wraparound=False
def count_primes_typed(int limit):
cdef int n = 0
cdef int x, i
cdef bint is_p
for x in range(2, limit):
i = 2
is_p = True
while i * i <= x:
if x % i == 0:
is_p = False
break
i += 1
if is_p:
n += 1
return n
これで28.899秒が1.523秒になりました。19.0倍の短縮です。Cythonを入れたのに速くならないという相談の大半は、この型宣言が無いことが原因です。ビルドは次の3行を setup.py として置き、python setup.py build_ext --inplace を実行するだけです。
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("primes_cy.pyx", language_level=3))
成果物はmacOS・Linuxなら .so、Windowsなら .pyd という拡張モジュールになります。ビルドしたPythonのバージョン向けのバイナリなので、配布先のPythonが違えばビルドし直しが必要です。型宣言の効かせ方やNumPy連携はCythonとは?Pythonを高速化する使い方・インストール・NumPy連携を実例で解説で詳しく扱っています。
prangeでの並列化とmacOSで必要になる追加設定
Cythonの並列化は cython.parallel.prange で行いますが、macOSでは2つ壁があります。
1つ目はコンパイラです。Apple clang 21.0.0 に -fopenmp を渡すと clang: error: unsupported option '-fopenmp' で止まります。OpenMPランタイムが同梱されていないためで、brew install libomp で導入したうえで、コンパイル時に -Xpreprocessor -fopenmp とインクルードパス、リンク時に -lomp を明示する必要があります。パスはIntel Macなら /usr/local/opt/libomp、Apple siliconなら /opt/homebrew/opt/libomp になるので、brew --prefix libomp で確認してください。
from setuptools import setup, Extension
from Cython.Build import cythonize
ext = Extension(
"primes_omp", ["primes_omp.pyx"],
extra_compile_args=["-Xpreprocessor", "-fopenmp",
"-I/usr/local/opt/libomp/include"],
extra_link_args=["-L/usr/local/opt/libomp/lib", "-lomp"],
)
setup(ext_modules=cythonize([ext], language_level=3))
2つ目はリダクション変数の扱いです。逐次版のループ本体をそのまま prange に入れると、次のエラーでコンパイルが通りません(同じ原因で複数行が出るうちの1行を抜粋しています)。
bench_omp.pyx:13:18: Cannot read reduction variable in loop body
原因は内側のカウンタ i です。i += 1 があるためCythonがOpenMPのリダクション変数と推論し、ループ本体からの読み取りを禁じます。回避策は、判定処理を noexcept nogil のcdef関数へ切り出すことです。
# cython: language_level=3, boundscheck=False, wraparound=False
from cython.parallel import prange
cdef bint is_prime(int x) noexcept nogil:
cdef int i = 2
while i * i <= x:
if x % i == 0:
return False
i += 1
return True
def count_primes_omp(int limit):
cdef int n = 0
cdef int x
for x in prange(2, limit, schedule='dynamic', nogil=True):
if is_prime(x):
n += 1
return n
これで0.150秒まで到達します。逐次のCythonから10.2倍、CPythonから211倍です。ただしここまで来ると、書いているものはPythonというより型付きのCです。
Codonの互換性の壁|整数演算が黙って別の答えを返す
2の63乗が負の値になる仕様差
Codonの最大のリスクは、動かないことではなく、動いたうえで違う答えを返すことです。実際に確認した例を挙げます。
print(2 ** 63)
print(10 ** 30)
| 式 | CPython 3.14.6 | Codon 0.19.6 |
|---|---|---|
| 2 ** 63 | 9223372036854775808 | -9223372036854775808 |
| 10 ** 30 | 1000000000000000000000000000000 | 5076944270305263616 |
CPythonの int は桁数に上限がありませんが、Codonの int は符号付き64ビット固定です。上限を超えると警告も例外もなく巻き戻ります。10 ** 30 にいたっては、桁あふれの結果として無関係な数値が返り、それが正常な戻り値として後続の計算に流れ込みます。
この挙動は、ハッシュ計算・累積積・大きな階乗・暗号関連の実装を移植するときに致命的です。Codonを既存コードへ適用する前に、64ビットを超え得る整数演算が無いかを先に洗ってください。速度検証の前にこの確認をしないと、速くなったコードが間違った答えを出していることに気づけません。桁あふれが避けられない箇所では、Codonの採用自体を見送るのが正しい判断です。
pipで入れたライブラリを読み込めない範囲
Codonは独自の標準ライブラリを持っており、CPythonのパッケージをそのままimportできません。import requests は no module named 'requests' で弾かれます。
逃げ道として from python import でCPythonのモジュールを呼び出す機能がありますが、これはCodonがlibpythonを動的ロードして実行する仕組みです。環境変数 CODON_PYTHON で共有ライブラリの場所を指定しないと、実行時に dlopen(libpython.dylib) の失敗として落ちます。そして呼び出した部分はCPythonが動くので、その区間に高速化はかかりません。
細かい構文の違いもあります。"%d %.3f" % (r, t) のようなタプルによる文字列書式化は unsupported operand type(s) for % となり、f-stringへの書き換えが必要でした。Codon単体のインストール手順や対応済み機能の一覧はCodon(コドン)とは – Python互換でC/C++並みの実行速度と多機能を実現する次世代高性能コンパイラにまとめています。
配布バイナリはlibompの同梱が前提
Codonは codon build -release -exe で単体の実行ファイルを作れます。ただし、そのまま配布すると起動しません。
dyld[45204]: Library not loaded: @loader_path/libomp.dylib
otool -L で確認すると、生成物は @loader_path/libomp.dylib を参照していました。実行ファイルと同じディレクトリにlibompが置かれている前提のビルドです。Codonインストール先の lib/codon/libomp.dylib を実行ファイルの隣にコピーすると、別ディレクトリへ移した実行ファイルでも正常に起動することを確認しています。ビルド時間も無視できず、上に載せた21行のスクリプトで約9秒かかりました。CIで毎回ビルドする構成にするなら、この時間を織り込んでください。
Mojoの現況|1.0.0b2とApple silicon限定という制約
対応プラットフォームと必要なCPU世代
Mojoを今回の比較表に載せられなかったのは、計測機で動かないからです。公式ドキュメントの要件は次のとおりです。
| OS | 要件 |
|---|---|
| macOS | Sequoia(15)以降・Apple silicon(M1〜M5) |
| Linux(x86) | glibc 2.34以降・x86-64-v3 以上 |
| Linux(ARM) | ARM64 Neoverse N1 以降 |
| Windows | ネイティブ非対応(WSL経由) |
Intel MacはmacOSの要件から外れています。x86-64はLinux側の要件にしか現れず、しかもx86-64-v3(Haswell世代・2013年前後以降)が下限です。ARM64はAWS GravitonのようなNeoverse N1以降が対象になります。手元のIntel Macで試したい場合、選択肢はLinux仮想環境かWSLです。インストールは公式サイトがuvとpixiの2通りを案内しており、uvなら uv pip install mojo --prerelease allow で導入できます。
バージョン体系とドキュメントの移管
2026年8月時点の最新はMojo 1.0.0b2で、2026-06-18公開のMAX 26.4に同梱されています。beta 2、つまりまだ1.0の正式版ではありません。前身の1.0.0b1は2026-05-07公開なので、ベータ段階の更新が続いている状況です。
調べる際の注意点として、ドキュメントの所在が変わっています。docs.modular.com/mojo は mojolang.org へ301リダイレクトされます。検索で古いURLを引いた場合はリダイレクト先を確認してください。開発元のModularはリポジトリのスター数が26,000を超えており開発は活発ですが、言語自体のオープンソース化は公式サイト上で「近日」と告知されている段階で、まだ完了していません。
用途別の選び分け|書き換え量と実行環境で決まる
既存コードを変更したくない場合
Codonが第一候補です。今回の計測でも、ソースを1文字も変えずに23.3倍が出ました。ただし前段で示した64ビット整数の制約があるため、適用前に大きな整数演算の有無を必ず確認してください。バッチ処理やアルゴリズム計算のように、入出力が単純で外部ライブラリ依存が薄いスクリプトが最も相性のよい領域です。
CPythonの資産と併用する場合
Cythonを選びます。生成物がPythonの拡張モジュールなので、pipで入れた既存ライブラリと同じプロセスで同居でき、重い関数だけを部分的に置き換えられます。代わりに型宣言を書く手間が発生し、書かなければ1.10倍しか出ません。既存アプリのボトルネックが数関数に閉じているなら、この方式が最も導入コストが低く済みます。なお本記事は事前コンパイル型の3つに絞っており、実行時にコンパイルするJIT型は対象外です。デコレーター1つで動くNumbaとは?Pythonを高速化するJITコンパイラの使い方とインストールも、数値計算主体なら比較対象になります。
Mojoを今は採用すべきでない条件
次のいずれかに当てはまるなら、2026年8月時点でのMojo採用は見送るべきです。第一に、実行環境にIntel MacまたはWindowsネイティブが含まれる場合。要件上そもそも動きません。第二に、本番システムに載せる場合。1.0.0b2はベータであり、破壊的変更を受け入れる体制が必要です。第三に、既存のPythonコードをそのまま速くしたい場合。MojoはPythonのスーパーセットを志向する新言語であり、無改変で動く前提の道具ではありません。
逆に、Apple siliconかLinux x86-64の開発機で、MLIRベースの言語設計やSIMD記述を検証目的で触るなら、今から慣れておく価値はあります。判断を分けるのは性能ではなく、動作環境と安定性です。
よくある質問
CythonとCodonはどちらが速いですか?
今回の素数カウント(200万件)では、逐次実行でCodonが1.361秒、Cython(型宣言あり)が1.523秒とCodonがわずかに上でした。並列化するとCythonのprangeが0.150秒、Codonの@parが0.285秒でCythonが約1.9倍速くなります。ただしCython側は型宣言に加えてループ本体の関数分割が必要で、Codon側は@parを1行足すだけです。実務上の違いは速度よりも、書き換え量とビルド環境の準備にあります。
Pythonのコードを書き換えずに高速化できますか?
Codonなら可能です。今回はソースを1文字も変えずに codon run -release を実行するだけで23.3倍になりました。ただし無条件ではありません。CPythonのintは無限精度ですがCodonは64ビット固定のため、大きな整数を扱う処理では警告なしに誤った値を返します。またpipで導入したライブラリはimportできません。書き換え不要なのは、外部依存が少なく整数の桁が収まる計算処理に限られます。
NumPyを使っていればコンパイラを入れる意味はありますか?
処理の種類によります。4000×4000の行列積はCPython + NumPyで1.851秒、Codonで1.948秒とほぼ同じでした。どちらも同じBLASを呼ぶだけなので差が出ません。一方、2000万要素に対する x * x + 2.0 * x の総和ではCPythonの0.907秒に対しCodonが0.331秒と2.7倍でした。Codonは要素演算を1つのループへ融合し、中間配列を作らないためです。要素演算を何段も重ねている箇所なら効果があります。
Mojoは今すぐ実務で使えますか?
2026年8月時点の最新はMojo 1.0.0b2(MAX 26.4・2026-06-18公開)で、まだベータ段階です。加えて動作要件が厳しく、macOSはSequoia 15以降かつApple silicon(M1〜M5)限定、Linuxはglibc 2.34以降でCPUがx86-64-v3以上またはARM64 Neoverse N1以降、Windowsはネイティブ非対応でWSL経由となります。Intel Macでは動きません。検証や学習で触るぶんには問題ありませんが、本番採用は破壊的変更を許容できる体制が前提になります。
素数計算以外でも同じ倍率になりますか?
なりません。今回の題材は整数演算とループだけで構成されており、インタプリタのオーバーヘッドが最も表に出る条件です。プログラミング言語ごとの処理速度の差が最大に開く形とも言えます。実際、同じCodonでも行列積では差がほぼゼロでした。処理時間の大半がC実装のライブラリ内部やI/O待ちで占められている場合、Python層を速くしても全体はほとんど変わりません。まずプロファイラでPythonのバイトコード実行に時間が使われていることを確認してから、コンパイラの導入を検討してください。