閉じる

Pythonで実行時間を計測する方法まとめ(timeitとperf_counter)

パフォーマンスの悩みは、勘ではなく実測で解決します。

Pythonでは小さな処理の比較にはtimeit実アプリに近い自由な計測にはtime.perf_counterが基本です。

この記事では、Python初心者向けに両者の使い方とコツを具体例で丁寧に解説し、正しく再現可能な計測方法を身につけます。

Pythonの実行時間を計測する基本

実行時間を計測する目的(正確な比較のため)

実行時間の計測は、処理のボトルネックを見つけたり、コードの書き方を変えたときに効果があったかを客観的に確認するために行います。

アルゴリズムの選択やデータ構造の変更は、環境や入力データによって結果が変わります。

したがって、思い込みではなく、同条件での実測比較が重要です。

特に小さな最適化は差が微小なので、ノイズに埋もれない測り方を徹底します。

time.time()ではなく高精度タイマーを使う理由

time.time()は壁時計の時刻(UNIX時間)で、NTP同期などの影響で逆行することがあり得ます

また、分解能が粗い環境があります。

これに対しtime.perf_counter()モノトニックかつ高分解能で、計測用に設計されています。

Pythonで実行時間を測るときは常にperf_counter(あるいはtimeit内部で使われるタイマー)を使いましょう。

整数ナノ秒が必要ならperf_counter_ns()も選べます。

測定は複数回行う(ばらつき対策)

OSのスケジューリング、CPU周波数変動、キャッシュの状態などで1回ごとにばらつきます。

複数回測って代表値(最小値や中央値)を見るのが実務の基本です。

小さな処理ならtimeitが自動で反復計測してくれます。

自由計測の場合も自分でループし、minstatistics.medianを使うと安定します。

ウォームアップ実行を1〜2回入れてキャッシュを温めてから本計測に入れるとさらに安定します。

timeitの使い方(小さな処理のベンチマーク)

最小コード例(Pythonのtimeit)

timeitは、短いコード片の速度を手軽に比較するための標準ライブラリです。

ガーベジコレクタを一時的に無効化するなど、ノイズを抑えた測定が自動で行われます。

Python
# timeit の最小例
# 短い処理(sum(range(1000)))の実行時間を、10,000回の合計時間として測定します。
from timeit import timeit

elapsed = timeit('sum(range(1000))', number=10_000)  # number は1回の計測で実行する回数
print(f'total: {elapsed:.4f}s, per loop: {elapsed/10_000*1e6:.2f} µs')
実行結果
# 出力例(環境により異なります)
total: 0.5201s, per loop: 52.01 µs

文字列で書く方法は最も互換性が高いです。

最近のPythonではstmtにコール可能オブジェクトも渡せますが、文字列のほうがJupyterやコマンドラインとの親和性が高いです。

関数の実行時間を測る

関数を定義して測る場合、setupでインポートや準備を行い、globalsで名前解決するのが簡単です。

Python
# 関数の実行時間を timeit で測定する例
from timeit import timeit

def add_squares(n: int) -> int:
    # 0..n-1 の二乗和を返す単純な関数
    return sum(i*i for i in range(n))

# 文字列と globals を使う方法(互換性重視)
t = timeit('add_squares(1000)', globals=globals(), number=5_000)
print(f'total: {t:.4f}s, per call: {t/5_000*1e6:.2f} µs')

# コール可能を直接渡す方法(対応するPythonなら可)
t2 = timeit(lambda: add_squares(1000), number=5_000)
print(f'(callable) total: {t2:.4f}s, per call: {t2/5_000*1e6:.2f} µs')
実行結果
total: 0.4102s, per call: 82.04 µs
(callable) total: 0.4095s, per call: 81.90 µs

number/repeatの意味と目安

  • number: 1回の計測で同じ処理を何回ループするかを決めます。処理が速すぎる場合はnumberを大きくして、1回の計測合計が0.1〜1.0秒程度になるように調整すると安定します。
  • repeat: 上記の1回の計測自体を何回繰り返すかです。最小値(=best)を採用すると、突発的な遅延の影響を受けにくくなります。
Python
# repeat を使ってばらつきを見る
from timeit import repeat

times = repeat('sum(range(1000))', number=10_000, repeat=5)
best = min(times)
avg = sum(times)/len(times)
print(f'all: {times}')
print(f'best: {best:.4f}s, avg: {avg:.4f}s')
実行結果
all: [0.5201, 0.5342, 0.5179, 0.5225, 0.5186]
best: 0.5179s, avg: 0.5227s

コマンドラインとJupyterでの使い方

Pythonは-m timeitでコマンドラインから直接ベンチマークできます。

JupyterではIPythonマジック%timeitが使えます。

Shell
# コマンドラインからの例
python -m timeit -n 10000 -r 5 "sum(range(1000))"
python -m timeit -n 10000 -r 5 -s "from math import sqrt" "sum(sqrt(i) for i in range(1000))"
実行結果
10000 loops, best of 5: 51.9 usec per loop
10000 loops, best of 5: 245 usec per loop
Python
# Jupyter/IPython での例(セルマジック)
# 1行だけなら %timeit
%timeit sum(range(1000))

# 複数行を測るなら %%timeit (セル全体)
%%timeit
total = 0
for i in range(1000):
    total += i
実行結果
51.3 µs ± 1.1 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
40.2 µs ± 0.8 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

Jupyterの%timeitは内部で複数回回して統計を表示してくれるため、学習用途や手軽な比較にとても便利です。

小さなコードの比較に向く

timeitは短時間で終わるCPUバウンド処理(例: 内包表記、関数呼び出しのオーバーヘッド、アルゴリズム差の比較)に適しています。

GCを一時無効化し、準備コードと測定対象を分離できるため、対象の純粋な速度を測りやすいです。

一方、I/Oや外部要因に左右される処理の測定は、後述のperf_counterで自由に制御しながら行うほうが向いています。

perf_counterの使い方(自由度の高い計測)

基本の測り方(start/end)

time.perf_counter()は、モノトニックな高精度タイマーです。

開始時刻と終了時刻の差分を取るのが基本です。

Python
# perf_counter の基本
import time

start = time.perf_counter()
# 測りたい処理(例: 素朴なフィボナッチ)
def fib(n: int) -> int:
    return n if n < 2 else fib(n-1) + fib(n-2)

result = fib(20)
end = time.perf_counter()

elapsed = end - start  # 秒
print(f'fib(20) = {result}, elapsed = {elapsed:.6f}s')
実行結果
fib(20) = 6765, elapsed = 0.012345s

より高精度な整数ナノ秒が必要ならtime.perf_counter_ns()を使うと、丸め誤差を避けられます。

関数や処理全体を測る

デコレータやコンテキストマネージャを作ると、複数箇所の計測を一貫した書き方で行えます。

Python
# シンプルなコンテキストマネージャによる測定
import time
from contextlib import contextmanager

@contextmanager
def measure(label: str = ''):
    start = time.perf_counter()
    try:
        yield
    finally:
        elapsed = time.perf_counter() - start
        print(f'[{label}] {elapsed*1e3:.2f} ms')

# 使い方の例
with measure('heavy loop'):
    total = 0
    for i in range(2_000_000):
        total += i
実行結果
[heavy loop] 120.45 ms
Python
# デコレータ版(関数の実行時間を毎回測る)
import time
from functools import wraps

def timeit_func(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = time.perf_counter() - start
            print(f'{func.__name__} took {elapsed*1e3:.2f} ms')
    return wrapper

@timeit_func
def slow_add(a, b):
    time.sleep(0.05)  # 擬似的に遅い処理
    return a + b

print(slow_add(10, 20))
実行結果
slow_add took 50.12 ms
30

I/Oを含む処理の計測に適する

ファイル読み書き、ネットワーク、データベース、time.sleep()などのI/Oは、外部要因による揺らぎが大きく、timeitの前提(短時間・多数反復)と相性がよくありません。

必要な前処理・後処理も含めてperf_counterで1回の処理全体を測ると実運用に近い数値になります。

Python
# I/O を含む処理の計測例(ファイル書き込み→読み出し)
import time
from pathlib import Path

data = 'x' * 1_000_000  # 1MBの文字列
p = Path('tmp_io_test.txt')

start = time.perf_counter()
p.write_text(data, encoding='utf-8')       # 書き込み(I/O)
text = p.read_text(encoding='utf-8')       # 読み出し(I/O)
p.unlink(missing_ok=True)                  # 後片付け
elapsed = time.perf_counter() - start

print(f'I/O round trip: {elapsed*1e3:.2f} ms, size={len(text)} bytes')
実行結果
# 出力例(ストレージにより大きく変動)
I/O round trip: 18.73 ms, size=1000000 bytes

結果の表示(秒やmsに変換)

人間が読みやすい単位に整形しましょう。

秒をミリ秒(ms)やマイクロ秒(µs)に変換して表示すると比較しやすくなります。

Python
# 単位変換とフォーマット
import time

start = time.perf_counter()
time.sleep(0.0123)
elapsed = time.perf_counter() - start  # 秒

print(f'{elapsed:.6f} s')              # 秒
print(f'{elapsed*1e3:.2f} ms')         # ミリ秒
print(f'{elapsed*1e6:.1f} µs')         # マイクロ秒

# ナノ秒精度が必要なら perf_counter_ns を使う
ns_start = time.perf_counter_ns()
time.sleep(0.001)
ns_elapsed = time.perf_counter_ns() - ns_start
print(f'{ns_elapsed} ns')
実行結果
0.012300 s
12.30 ms
12300.0 µs
1001234 ns

timeitとperf_counterの使い分けとコツ

使い分けの目安(Pythonの実行時間計測)

どちらも標準ライブラリで信頼できますが、得意分野が異なります。

以下の表が実務での目安です。

観点timeitperf_counter
想定する処理極小〜小さなCPU処理任意の処理(関数単位、I/O含む)
反復実行自動(安定化のため多数)自分で制御
準備コードsetup引数で分離自由に書ける
ノイズ対策GC一時無効化など内蔵自前で工夫
出力総時間/反復回数(µsなど)秒/任意単位に整形
典型用途実装のマイクロベンチ実アプリの所要時間

小さなコード片の比較にはtimeit業務処理の1トランザクション全体にはperf_counterという意識で選ぶとよいです。

ウォームアップとノイズ対策

計測前に1回走らせてキャッシュやインポートを済ませるウォームアップを行い、OSのバックグラウンド負荷が少ないときに測りましょう。

可能なら以下を意識します。

  • 同じマシン・同じPythonバージョン・同じ依存ライブラリで測る。
  • 大きいノイズ源(ウイルス対策のスキャン、重いブラウザタブなど)を避ける。
  • 長めに測れるときはminや中央値を採用する。
  • 繰り返し計測で最小値(best)を参考にするのは、たまたま遅い回を避けるための実務的な指針です。

同条件で測る(入力・乱数seed固定)

同じ入力で比較しなければ公平になりません。

乱数を使う場合はシードを固定します。

Python
# 乱数シードを固定して公平に比較
import random
from timeit import timeit

random.seed(0)
data = [random.randint(0, 1_000_000) for _ in range(50_000)]

def f_sort(a):      # 組み込みソート
    return sorted(a)

def f_sort_copy(a): # コピーしてからソート(差を出すための例)
    b = list(a)
    b.sort()
    return b

t1 = timeit(lambda: f_sort(data), number=100)
t2 = timeit(lambda: f_sort_copy(data), number=100)
print(f' sorted: {t1:.3f}s, per call {t1/100*1e3:.2f} ms')
print(f' list.sort via copy: {t2:.3f}s, per call {t2/100*1e3:.2f} ms')
実行結果
 sorted: 0.842s, per call 8.42 ms
 list.sort via copy: 0.969s, per call 9.69 ms

printやログ出力を含めない

標準出力へのprintは非常に遅いため、測定対象に含めないようにします。

ログ出力も同様で、速度比較のときはロガーを無効化するか、出力しないダミーハンドラに差し替えます。

どうしても必要な場合は、perf_counterで「出力を除いた区間」と「出力を含む区間」を分けて測ると影響を把握できます。

NG例(time.time(), datetime.now()で計測)

以下は避けるべき例です。

理由はそれぞれコメントに記載しています。

Python
# NG: time.time() での計測
# 壁時計の時刻はNTP同期で逆行する可能性があり、分解能も環境依存で粗めです。
import time
start = time.time()
time.sleep(0.01)
elapsed = time.time() - start  # 値が負になる可能性も理論上はある
print(elapsed)  # たいていは動くが、perf_counter を使うべき
Python
# NG: datetime.now() での計測
# 日付時刻はタイムゾーン/夏時間/調整の影響を受け、性能計測用ではありません。
from datetime import datetime
start = datetime.now()
# ... 処理 ...
elapsed = (datetime.now() - start).total_seconds()  # これは避ける。perf_counter を使う。
print(elapsed)

正しい選択は常にtime.perf_counter()timeitです

この2つはプラットフォームごとの最適な高精度タイマーを内部で使います。

まとめ

実行時間の計測は、性能改善を定量的に進めるための第一歩です。

小さなコード片の比較はtimeitI/Oを含む実処理や自由な構成の計測はtime.perf_counterが基本方針です。

複数回測って代表値を見る同条件で比較するprintなどを測定対象に入れないといったコツを守ることで、再現性の高い結果が得られます。

最後にもう一度強調すると、time.time()やdatetime.now()では計測しないことです。

正しい道具と正しい手順で、安定したベンチマークを習慣化しましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!