RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

「Pythonは遅い」と言ってるけど、何がどう遅い?

あけましておめでとうございます!開発チームの下田です。

「Pythonは遅いからRustに書き直した」「パフォーマンスが必要ならC++を使うべき」
こうした議論をよく目にします。確かにPythonやrubyといった言語はRustやC++と比べて実行速度が遅いのは事実です。実際にどの程度、何ms遅いのでしょうか?

今回は、Pythonのパフォーマンス特性を具体的なベンチマークを例に、「言語特性による遅さ」が問題になるケースとならないケースを整理していきます。

Pythonはどれくらい遅いのか?

まず、具体的な数値で見てみましょう。

単純な計算処理の比較

特に意味のない1億回のループで簡単な計算を行うベンチマークを行っています。こういった計測方法はマイクロベンチマークと言ってあまり実用的ではないのですが、今回は問題をわかりやすくするために使用しています。

Python:

import time

start = time.time()
total = 0
for i in range(100_000_000):
    total += i
end = time.time()

print(f"Time: {end - start:.3f}s, Result: {total}")
# 実行結果例: Time: 4.521s

Rust(リリースビルド):

use std::time::Instant;
use std::hint::black_box;

fn main() {
    let start = Instant::now();
    let mut total: i64 = 0;
    for i in 0..100_000_000 {
        total += black_box(i); // コンパイラがループを削除しないようにする
    }
    let duration = start.elapsed();

    println!("Time: {:.3}s, Result: {}", duration.as_secs_f64(), black_box(total));
    // 実行結果例: Time: 0.048s
}

この例では、Pythonは約100倍遅いという結果になります。

ちなみにblack_box()を使わないと、Rustコンパイラはこのループを最適化で事前計算してしまいます。つまり0~100,000,000まで足した結果をプログラム実行前から計算済みの状態になります。そうなると、100倍どころじゃなく高速になります。

Pythonが遅い主な理由はどこでしょうか?主な理由を考えてみます。

1. 動的型付けによるオーバーヘッド

Pythonは動的型付け言語です。そのコードを実行する時にならないと、その変数がどの型なのか確定しません。そのため、変数にアクセスするたびに、変数が整数なのか文字列なのか判断する型チェックをしています。

x = 5
y = x + 3  # ここで x の型を確認し、適切な加算処理を選択。xは整数なので数学的に足し算する。文字列だったら "53"となる

一方、RustやC++では型がコンパイル時に確定しています。実行時に毎回確認する必要がありません。型チェックは現代の高速なコンピュータなら微々たるものですが、何億、何十億回と実行すると明確な差となります。

2. バイトコードコンパイルと実行のオーバーヘッド

Pythonは.pyファイルをバイトコードの.pycファイルへコンパイルしてから実行します。

.pycファイルが生成されていれば、キャッシュを利用するためコンパイルは不要です。

3. Global Interpreter Lock (GIL)

PythonのCPython実装には、同時に1つのスレッドしかPythonバイトコードを実行できないという制約があります。これにより、CPUの処理時間がボトルネックとなるCPUバウンドな処理ではマルチスレッドによる高速化が期待できません。

import threading
import time

def cpu_bound_task():
    total = 0
    for i in range(50_000_000):
        total += i
    return total

# シングルスレッド
start = time.time()
result1 = cpu_bound_task()
result2 = cpu_bound_task()
print(f"Sequential: {time.time() - start:.3f}s")
# 実行結果例: Sequential: 4.521s

# マルチスレッド(2スレッド)
start = time.time()
t1 = threading.Thread(target=cpu_bound_task)
t2 = threading.Thread(target=cpu_bound_task)
t1.start()
t2.start()
t1.join()
t2.join()
print(f"Multi-threaded: {time.time() - start:.3f}s")
# 実行結果例: Multi-threaded: 4.687s

SequentialとMulti-threadedの実行時間はほぼ同じになります。

マルチプロセス

GILはプロセス単位の制約なので、マルチプロセスにすることで並列実行が可能になります。

from multiprocessing import Pool
import time

def cpu_bound_task(n):
    total = 0
    for i in range(n):
        total += i
    return total

if __name__ == '__main__':
    tasks = [50_000_000, 50_000_000, 50_000_000, 50_000_000]

    # シーケンシャル実行
    start = time.time()
    results = [cpu_bound_task(n) for n in tasks]
    print(f"Sequential: {time.time() - start:.3f}s")
    # 実行結果例: Sequential: 9.042s

    # マルチプロセス実行(4プロセス)
    start = time.time()
    with Pool(processes=4) as pool:
        results = pool.map(cpu_bound_task, tasks)
    print(f"Multi-process (4 cores): {time.time() - start:.3f}s")
    # 実行結果例: Multi-process (4 cores): 2.531s

4コアのCPUで理論値の4倍に近い高速化ができました。

Python 3.13から実験的にno-GIL(free-threading)モードが導入されましたが、2025年12月時点ではまだ多くのライブラリが未対応です。

4. メモリ管理のオーバーヘッド

Pythonでは整数や文字列といった小さな値でさえオブジェクトとして扱われるため、メモリ使用量が大きく、アクセスも遅くなります。

普段使うような桁の整数を使うとき、Rustのi32やC++のintなら4バイトで済みますが、pythonはもっと大きなサイズを必要とします。

import sys

# 整数1つのメモリ使用量
x = 42
print(sys.getsizeof(x))  # 28バイト(環境による)

言語特性以外の遅い処理

ここまで、Pythonの言語特性による遅さが目立つポイントをみてきました。

Rust等と比較してPythonは確かに遅いです。しかし、それが実際の問題になるかどうかはシステムのどこがボトルネックかによります。多くの場合では、言語特性による差は数ms~数十ms止まりですが、その他の処理で数百~数千msかかることはよくあります。もしPythonに優秀なライブラリがあり、現行のrust等のライブラリと比較して遅い処理が10%高速化できているとしたら、トータルではPythonのほうが速いというケースもよくあります。

言語特性以外の、その他の処理で時間がかかる代表的な処理をみていきたいと思います。

ネットワークI/O

典型的なWebアプリケーションを想定すると、1リクエストの処理時間の内訳は、以下のようになる場合が多いです。もちろん実際の処理内容によって異なります。

外部APIを想定して、このブログのトップページへのリクエストを含めた処理時間を計測してみます。

import time
import requests

# CPUバウンド
start = time.time()
total = 0
for i in range(1_000_000):
    total += i
cpu_time = time.time() - start
print(f"CPU-bound: {cpu_time:.3f}s")
# 実行結果例: CPU-bound: 0.035s

# ネットワークI/O
start = time.time()
response = requests.get("https://techblog.raccoon.ne.jp")
io_time = time.time() - start
print(f"Network I/O: {io_time:.3f}s")
# 実行結果例: Network I/O: 1.234s

このような場合、Pythonの言語特性でチューニングできる時間は3%ほどで、全体のうちごくわずかです。実行速度を頑張って10倍改善しても、全体の応答時間は数%しか改善されません。大部分を占めている外部ネットワークへのリクエストの処理を改善したほうが、高い効果を得られます。

不適切なアルゴリズムの影響

アルゴリズム自体、コードの書き方による影響も大きいです。RustにするかPythonにするかで10倍の差が出ることもありますが、いくらRustにしていても、コードの書き方が悪いと1000倍、100万倍と遅くなることもあります。10倍の差があると聞くと大きいように感じますが、ほとんどの場合では目くじらを立てるほどの差ではないように思います。

下記の例は、同じ数字がいくつ重複しているかチェックするアルゴリズムを、線形探索するアルゴリズムと、HashSetを使うアルゴリズムで比較しています。

import time

# O(n^2) アルゴリズム - 遅い実装
def find_duplicates_slow(items):
    duplicates = []
    for i in range(len(items)):
        for j in range(i + 1, len(items)):
            if items[i] == items[j] and items[i] not in duplicates:
                duplicates.append(items[i])
    return duplicates

# O(n) アルゴリズム - 速い実装
def find_duplicates_fast(items):
    seen = set()
    duplicates = set()
    for item in items:
        if item in seen:
            duplicates.add(item)
        seen.add(item)
    return list(duplicates)

# 10,000件データを用意する
data = list(range(5000)) * 2

start = time.time()
result1 = find_duplicates_slow(data)
slow_time = time.time() - start
print(f"O(n²): {slow_time:.3f}s")
# 実行結果例: O(n^2): 1.299s

start = time.time()
result2 = find_duplicates_fast(data)
fast_time = time.time() - start
print(f"O(n): {fast_time:.3f}s")
# 実行結果例: O(n): 0.002s

print(f"Speed improvement: {slow_time/fast_time:.0f}x")
# 実行結果例: Speed improvement: 863x

アルゴリズムの改善で800倍以上高速化しています。O(n^2)のアルゴリズムは指数関数的に速度が劣化していくので、データの量が10倍になったら、差は更に100倍広がり、8万倍の差が出るはずです。100,000件のデータを想定すると、言語の選択にこだわると0.02秒対0.002秒になりますが、今回のようにアルゴリズムの選択を誤るか誤らないかでは26分対0.02秒になります。

Pythonは様々な分野での効率的なサンプルコードが多く、活用しやすいです。どちらの言語でも同じようにコーディングできる状況ではPythonが不利ですが、Pythonのほうが上手にコーディングできる状況ではPythonが有利になります。

Pythonがボトルネックになるケース

以下の状況では、Pythonの言語特性が本当にボトルネックになります。

1. CPU集約的な計算処理

具体例:
- 画像処理・動画処理(ピクセル単位の操作)
- 暗号化・ハッシュ計算
- 科学技術計算(ただし、NumPy等で緩和可能)
- 競技プログラミング

# 悪い例: Pythonで画像処理
def apply_filter_python(image):
    height, width = len(image), len(image[0])
    result = [[0] * width for _ in range(height)]
    for y in range(1, height - 1):
        for x in range(1, width - 1):
            # 3x3のフィルタを適用
            result[y][x] = (
                image[y-1][x-1] + image[y-1][x] + image[y-1][x+1] +
                image[y][x-1] + image[y][x] + image[y][x+1] +
                image[y+1][x-1] + image[y+1][x] + image[y+1][x+1]
            ) // 9
    return result

# 良い例: NumPy(C実装)を使用
import numpy as np
from scipy import ndimage

def apply_filter_numpy(image):
    kernel = np.ones((3, 3)) / 9
    return ndimage.convolve(image, kernel)

NumPyを使うことで、100倍以上高速化できます。

2. 大量の小さなオブジェクト操作

具体例:
- リアルタイムゲームエンジン
- 高頻度トレーディングシステム
- 大規模なグラフアルゴリズム(数百万ノード)

# 100万要素のリスト操作
import time

start = time.time()
data = list(range(1_000_000))
result = [x * 2 + 1 for x in data if x % 2 == 0]
print(f"List comprehension: {time.time() - start:.3f}s")
# 実行結果例: 0.087s

# NumPy配列での同等処理
import numpy as np

start = time.time()
data = np.arange(1_000_000)
result = data[data % 2 == 0] * 2 + 1
print(f"NumPy: {time.time() - start:.3f}s")
# 実行結果例: 0.003s

3. メモリを共有しながらの並行処理

GILのため、CPUバウンドな処理でマルチスレッドが使えません。マルチプロセスは可能ですが、プロセス間でのメモリ共有が困難です。

回避策:
- multiprocessing.shared_memory を使う(Python 3.8+)
- NumPy配列を共有メモリ上に配置
- それでも複雑な場合は、C拡張やRust/C++で実装

Pythonがボトルネックにならないケース

1. I/Oバウンドなアプリケーション

具体例:
- Webアプリケーション(Django, Flask, FastAPI)
- APIサーバー
- データベース操作が主体のアプリケーション
- ファイル処理・バッチ処理

# Webアプリケーションの典型的な処理フロー
async def handle_request(request):
    # Python実行: 5ms
    user_id = request.query_params.get('user_id')

    # データベースクエリ: 30ms
    user = await db.query("SELECT * FROM users WHERE id = ?", user_id)

    # 外部APIコール: 200ms
    recommendations = await external_api.get_recommendations(user_id)

    # Python実行: 3ms
    response = format_response(user, recommendations)

    return response
    # 合計: 約238ms(Pythonは8ms = 3%)

この場合、PythonをRustに置き換えても、全体の速度は1-2%しか改善されません。

2. 人間の入力を必要とする場合

これらは人間の指示を仰ぐ時間が数秒~数十分と圧倒的に長く、数msの差は気になりません。またPythonから他のライブラリを呼ぶ構成にすることで、弱点もカバーできます。

3. プロトタイピング・小規模アプリケーション

開発速度とメンテナンス性がパフォーマンスよりも重要な場合、Pythonの生産性の高さが大きなメリットになります。

まとめ

  1. Pythonは確かに遅く、RustやC++と比べて10〜100倍遅いケースがある
  2. I/Oバウンドなアプリケーションでは比較的影響が小さい
  3. アルゴリズムによる問題がないかチェックする
  4. 計測してから判断する

「Pythonは遅いからRustにした」では物足りなく、「ループ内で1億回の行列演算が必要で、NumPyでも要件を満たせなかったため、Rustで実装したい」と言えると、意見が通る確率がぐっと上がります。

さて、ラクーンホールディングスではエンジニアを大募集中です!
興味を持っていただいた方は是非、お話ししましょう!

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc