3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

結局 Python は遅いのか Part2 〜 sin() を外して比較してみた〜

3
Last updated at Posted at 2026-03-27

結局 Python は遅いのか Part2 〜 sin() を外して比較してみた〜

この記事では

  • 被積分関数の計算負荷を段階的に変えて,実装方式ごとの速度差を比較
  • 「Python の遅さ」がどこに起因するかを定量的に考察

を行います.

※追記(2026/03/28):過去記事(結局 Python は遅いのか)もぜひご覧ください

背景

前回の記事では,モンテカルロ法で $\int_0^1 \sin(x),dx$ の近似を行い,Python の各実装と C++ の速度を比較しました.
結果として,Python の素朴な for-loop は C++ の約 24 倍遅い という結論でした.

この記事に対して,「sin() は C のライブラリ(libm)を呼んでいるのだから,実質 sin() の速度を測っているだけでは?」という趣旨のコメントをいただきました.

確かに,math.sin() の計算本体は C で実行されるため,sin() の計算コストが支配的であれば,Python 側のオーバーヘッドの寄与が見えにくくなっていた可能性があります.

そこで今回は,被積分関数の計算負荷を段階的に変えて,Python のオーバーヘッドがどの程度影響しているかを検証します.

問題設定

前回と同じモンテカルロ積分の枠組みを使い,被積分関数 $f(x)$ だけを差し替えて 4 パターンを比較します.

I = \int^{1}_{0} f(x) \, dx \approx \frac{1}{N} \sum^{N}_{i=1} f(x_i), \quad x_i \sim U(0, 1)

4 つのパターン

パターン $f(x)$ 真値 狙い
A $x$ $1/2$ 最軽量ケース
B $x^2 + x$ $5/6$ 基本的な算術演算
C $x^5 + 3x^3 + 2x^2 + x$ $25/12$ 演算回数を増やした場合
D $\sin(x)$ $1 - \cos(1)$ 前回と同じ(C ライブラリ呼出)

ポイント: A〜C は math.sin() などの C ライブラリ関数を使わず,D は前回と同一条件です.
各実装(for-loop / NumPy / Numba / C++)で実装方法は異なりますが,計算する被積分関数は全実装で統一しています.

条件

前回と同一:

  • 乱数は事前生成(seed=42)
  • 単一スレッド
  • $N$ = $1 \times 10^5$,$1 \times 10^6$,$1 \times 10^7$
  • 各 $N$ ごとに:ウォームアップ 2 回 → 本測定 200 回
  • 中央値を代表値とする

実行環境

項目 内容
OS macOS
CPU Intel Core i5-8257U CPU @ 1.40GHz
メモリ 8 GB
Python 3.10.19
NumPy 2.2.6
Numba 0.63.1
C++ clang++ 21.1.8(-O3

比較対象

今回は,前回のリスト内包表記・generator を省略し,以下の 4 実装で比較します.

Exp 実装 説明
1 Python for-loop 純粋な Python ループ
2 NumPy ベクトル化 内部は C で実装されたベクトル演算
3 Numba JIT LLVM による JIT コンパイル
4 C++ (-O3) ネイティブ実装

実験結果

計算速度

以下に,200 回実行した処理時間の中央値(単位:ms)を示します.

Pattern A:

$ f(x) = x $ (単純加算)

Exp 実装 $1 \times 10^5$ $1 \times 10^6$ $1 \times 10^7$
1 Python for-loop 8.920 105.988 989.969
2 NumPy 0.053 0.452 4.236
3 Numba JIT 0.110 1.152 12.545
4 C++ (-O3) 0.112 1.281 13.301

Pattern B:

$f(x) = x^2 + x$(軽量多項式)

Exp 実装 $1 \times 10^5$ $1 \times 10^6$ $1 \times 10^7$
1 Python for-loop 19.684 203.179 2054.601
2 NumPy 0.113 2.328 35.656
3 Numba JIT 0.109 1.531 15.240
4 C++ (-O3) 0.108 1.354 13.845

Pattern C:

$f(x) = x^5 + 3x^3 + 2x^2 + x$(重い多項式)

Exp 実装 $1 \times 10^5$ $1 \times 10^6$ $1 \times 10^7$
1 Python for-loop 64.838 664.547 5751.686
2 NumPy 0.398 9.939 149.944
3 Numba JIT 0.142 1.869 17.790
4 C++ (-O3) 0.155 1.725 17.018

Pattern D:

$f(x) = \sin(x)$(C ライブラリ呼出し)

Exp 実装 $1 \times 10^5$ $1 \times 10^6$ $1 \times 10^7$
1 Python for-loop 13.430 134.462 1426.537
2 NumPy 0.577 6.835 66.855
3 Numba JIT 0.631 6.693 64.912
4 C++ (-O3) 0.514 5.723 62.715

相対速度(C++ = 1.00)

ここが本題です.C++ を基準(1.00)としたときの Python for-loop の相対速度を,パターンごとに比較します.

Python for-loop / C++ の相対速度

パターン $f(x)$ $1 \times 10^5$ $1 \times 10^6$ $1 \times 10^7$
A $x$ 79.37 82.76 74.43
B $x^2 + x$ 181.51 149.97 148.37
C $x^5 + 3x^3 + 2x^2 + x$ 419.53 385.29 337.99
D $\sin(x)$ 26.12 23.49 22.74

A〜C と比べて D の差が明確に小さいことがわかります.

前回の記事では 約 24 倍 という結果でした.これ自体は正しい計測結果ですが,sin() が C ライブラリ呼出しであることにより,Python 側のオーバーヘッドの寄与が相対的に目立たなくなっていたことが示唆されます.

C ライブラリ関数を使わない場合(A〜C),C++ との差は 74〜338 倍にまで広がりました.

なぜパターンによってこれほど差が出るのか

結果を整理すると,下記の傾向が見えてきます($N = 10^7$ 基準):

軽い処理ほど Python のオーバーヘッドが相対的に目立つ傾向があり,また C ライブラリ関数呼出しのように計算本体が重い場合には,その影響が相対的に見えにくくなる

パターン 処理内容 Python for / C++
A 読み出し・加算・除算(最軽量) ~74x
B 乗算 + 加算 ~148x
C 複数の乗算 + 加算 ~338x
D C ライブラリ関数呼出(sin) ~23x

上記の表を見ると,A -> B -> C と進むにつれて Python / C++ の差は大きく拡大し,一方で D(sin)ではその差がむしろ小さくなっている.
つまり,処理内容(特に 1 要素あたりの計算量)によって,Python と C++ の相対差の現れ方は大きく異なることがわかる.

まず A〜C では,演算回数が増えるにつれて Python / C++ の差が拡大しています.
もし差の主因がループ制御の固定オーバーヘッドだけであれば,倍率は大きくは変わらないと考えられます.
実際には A から C にかけて差が大きく広がっていることから,ループ制御だけでなく,各演算ごとに発生する Python 特有のオーバーヘッド も寄与していると考えられます.
もちろん,メモリアクセスパターンや C++ 側のコンパイラ最適化(演算融合やベクトル化など)の影響も排除はできませんが,傾向としては演算回数との相関が明確に出ていると考えられます.

一方で D(sin)では,A〜C と比べて Python / C++ の差が小さいことがわかります.
これは Python のオーバーヘッドが消えたからではなく,sin の計算本体が Python 側・C++ 側の両方で重く,全体時間に占める Python 固有オーバーヘッドの割合が相対的に小さく見えるためと考えられます.

Python で 1 演算ごとに起きていること

CPython(標準の Python 実装)では,x + x のような単純な演算でも,概ね以下のような処理が発生します:

specialize 前:

  1. BINARY_OP
  2. _PyEval_BinaryOps[NB_ADD]
  3. PyNumber_Add
  4. binary_op1
  5. float_as_number.nb_add
  6. float_add
  7. CONVERT_TO_DOUBLE(v)
  8. CONVERT_TO_DOUBLE(w)
  9. a + b
  10. PyFloat_FromDouble

specialize 後:

  1. _GUARD_TOS_FLOAT
  2. _GUARD_NOS_FLOAT
  3. _BINARY_OP_ADD_FLOAT
  4. left->ob_fval + right->ob_fval
  5. PyFloat_FromDouble

要点としては Python の *+ の 1 回 1 回に「C 演算 + オブジェクト生成 + メモリ管理」相当のコストがかかります.
演算回数が多い Pattern C では,このオーバーヘッドがループのたびに積み重なるため,差が大きくなります.

sin() で差が小さい理由

一方,math.sin(x)C の libm を直接呼び出す ため:

  1. CALL / CALL_BUILTIN_O
  2. math_sin
  3. math_1(arg, sin, ...)
  4. PyFloat_AsDouble(arg)
  5. sin(x)
  6. PyFloat_FromDouble(result)

sin の内部計算は C レベルで完結 するため,sin 内部の計算過程では Python オブジェクトの生成・破棄は発生しません(呼出し前後の変換コストのみ).
そのため,sin 本体の計算コストが支配的になり,Python 側の呼出しオーバーヘッド(関数呼出し境界のコストなど)は相対的に小さくなります.結果として,Python と C++ の相対速度の差が小さくなります.

Numba・NumPy について

Numba JIT

Numba は全パターンで C++ とほぼ同等の性能を示しています:

パターン Numba / C++ ($N = 10^7$)
A 0.94
B 1.10
C 1.05
D 1.04

Numba は Python コードを LLVM で機械語にコンパイルするため,演算前後のオーバーヘッドが排除されます.パターンに関係なく安定して高速です.
パターン A に関しては,Numba と clang++ ではコード生成・最適化パス,CPU target,SIMD 化やループ展開のされ方が違うため,この程度の差は十分ありうる差と考えられます.

NumPy

NumPy は C でベクトル演算を行いますが,パターンによって傾向が異なります:

パターン NumPy / C++ ($N = 10^7$)
A 0.32
B 2.57
C 8.81
D 1.07

Pattern A では C++ より速く,D でもほぼ同等です.一方,B・C では C++ より遅くなっています.
これは xs**2 + xs のような式が演算ごとに中間配列を生成するためで,式が複雑になるほどメモリ確保・アクセスのコストが増加します.

考察

今回の検証で,前回のベンチマークでは sin() の計算コストが支配項となっており,Python のオーバーヘッドが相対的に目立ちにくくなっていたことが確認できました.
前回の結果で Python の遅さが見えていなかったわけではありませんが,支配項の切り分けが十分ではなかったと言えます.

被積分関数を算術演算のみに置き換えると:

  • Python for-loop と C++ の差は 24 倍 → 74〜338 倍 に拡大
  • 演算回数が増えるほど差が広がることから,ループ制御だけでなく各演算のオーバーヘッドも寄与が大きい

前回の結論の補足

前回の記事では,

Python は遅いのではなく,「Python のループ」が遅い

としました.この結論自体は間違いではありませんが,今回の検証を踏まえると,より正確にはこう言えます:

Python は「ループ制御」だけでなく「各演算」にもオーバーヘッドがある.sin() のような C ライブラリ呼出を使う場合,計算本体のコストが支配的になり,そのオーバーヘッドが相対的に目立ちにくくなっていた

まとめ

  • math.sin() は C ライブラリ(libm)を呼び出しており,計算本体は C で実行される
  • 前回の sin() ベンチマークでは,sin() の計算コストが支配的で,Python のオーバーヘッドの寄与が相対的に目立ちにくかった
  • C ライブラリ関数を使わない算術演算では,Python for-loop は C++ の 74〜338 倍 遅い
  • Numba を使うと,様々なパターンで C++ と同等の性能が得られた
  • NumPy は中間配列の生成により,複雑な式では若干遅くなった

さいごに

今回は被積分関数を変えることで,Python のオーバーヘッドの内訳をもう少し掘り下げることができました.

これは他の動的型付け・インタプリタ実行の言語でも同様かもしれません.

ただ,結局のところ,言語選択は速度だけで決まるものではありません.Python の強みはエコシステムの広さ,書きやすさ,プロトタイピングの速さにあると思います.速度が必要な箇所には Numba や NumPy のような道具をうまく使うのもスキルかと思います.
要件を満たすなら好きな言語で書くのが一番 だと思います.

最後に,前回コメントをくださった方にも感謝いたします.

Appendix:CPython の 1 演算あたりのコスト内訳

CPython の C API を呼出して,float 加算 1 回に含まれる(いくつかの)各処理のコストを計測しました(CPython 3.15.0a7+,-O2).

処理 時間 (ns/iter) 備考
PyFloat_FromDouble(boxing) 14.39 新しい float オブジェクトを生成
PyFloat_AS_DOUBLE(unbox) 0.35 オブジェクトから C double を取得
Py_INCREF + Py_DECREF 1.32 参照カウント操作
Raw C double add 0.35 C レベルの加算そのもの
Full float add(合計) 15.69 unbox×2 + C add + box + decref

1 回の a + b で約 15.7 ns かかり,そのうち C の加算は 0.35 ns(約 2%)です.残りの約 98% は boxing(オブジェクト生成)や参照カウント管理などの Python ランタイムのコストです.

なお,この計測は CPython の C API を直接呼び出したもので,実際の Python コード実行ではさらにバイトコードディスパッチのコストが加わります.

この結果からも,Python における演算コストの大半が実際の計算そのものではなく,オブジェクト操作などのランタイム処理に起因していることがわかります.

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?