timeit – 小さな Python コードの実行時間を計る

目的:小さな Python コードの実行時間を計る
利用できるバージョン:2.3

timeit モジュールは小さな Python コードの実行時間を計るシンプルなインタフェースを提供します。なるべく正確な時間を算出するためにプロットフォーム特有の time 関数を使用します。そして対象のコードを何回も実行することで時間計算のための起動や終了による影響を少なくします。

モジュールコンテンツ

timeit はパブリッククラス Timer を定義します。 Timer のコンストラクタは時間を計測するコードと setup 処理(例えば、変数の初期化)を受け取ります。Python のコードは改行を含んだ文字列である必要があります。

timeit() メソッドは1回 setup 処理を実行してから対象となるコードを何回か実行した処理時間を返します。timeit() へ渡す引数はその対象コードを何回実行するかを制御します。デフォルトは 1,000,000 です。

基本的なサンプル

Timer へ渡される引数の使用方法を説明するために、コードが実行されるときにデバッグ出力する簡単なサンプルを作成しました。

import timeit

# setitem を使用する
t = timeit.Timer("print 'main statement'", "print 'setup'")

print 'TIMEIT:'
print t.timeit(2)

print 'REPEAT:'
print t.repeat(3, 2)

実行すると次の結果になります。

$ python timeit_example.py
TIMEIT:
setup
main statement
main statement
2.14576721191e-06
REPEAT:
setup
main statement
main statement
setup
main statement
main statement
setup
main statement
main statement
[1.9073486328125e-06, 9.5367431640625e-07, 9.5367431640625e-07]

呼び出されたときに timeit() は setup 処理を1回実行した後で対象となるコードを指定した回数で実行します。そして指定した回数分の実行したときの計測時間を小数で返します。

repeat() を使用すると、数回(このケースでは3) timeit() を実行して、その全ての計測時間をリストで返します。

ディクショナリに値を格納する

もっと複雑なサンプルに、いろんなメソッドと巨大なディクショナリを生成する処理時間を比較してみましょう。まず最初に Timer を設定する定数が必要です。そして文字列と整数を含むタプルのリストを使用します。 Timer は文字列をキーとするディクショナリに整数を格納されるようにします。

# {{{cog include('timeit/timeit_dictionary.py', 'header')}}}
import timeit
import sys

# 定数定義
range_size=1000
count=1000
setup_statement="l = [ (str(x), x) for x in range(%d) ]; d = {}" % range_size
# {{{end}}}

次に短いユーティリティ関数で結果を表示するのに便利なフォーマットを定義します。 timeit() メソッドはそのコードを何回か実行したときの計測時間を返します。 show_results() の出力はその計測時間から個々の要素単位で算出します。その後でディクショナリの1つの要素を格納するのにかかった時間をさらに分解します(もちろん平均値でね)。

# {{{cog include('timeit/timeit_dictionary.py', 'show_results')}}}
def show_results(result):
    "Print results in terms of microseconds per pass and per item."
    global count, range_size
    per_pass = 1000000 * (result / count)
    print '%.2f usec/pass' % per_pass,
    per_item = per_pass / range_size
    print '%.2f usec/item' % per_item

print "%d items" % range_size
print "%d iterations" % count
print
# {{{end}}}

基準値を設けるために、既にテストした最初の設定内容は __setitem__() を使用します。このシンプルなコードが最も速くなるように、その他の全ての変数はディクショナリに既に存在する値を上書きしないようにします。

Timer へ渡す最初の引数は、実行時に正しく構文解析されるようにインデントを保持した複数行の文字列であることに着目してください。2番目の引数は上述したディクショナリと値のリストを初期化した定数です。

# {{{cog include('timeit/timeit_dictionary.py', 'setitem')}}}
# Using __setitem__ without checking for existing values first
print '__setitem__:\t',
sys.stdout.flush()
# setitem を使用する
t = timeit.Timer("""
for s, i in l:
    d[s] = i
""",
setup_statement)
show_results(t.timeit(number=count))
# {{{end}}}

次の変更は、ディクショナリの値が上書きされないように setdefault() を使用します。

# {{{cog include('timeit/timeit_dictionary.py', 'setdefault')}}}
# setdefault を使用する
print 'setdefault:\t',
sys.stdout.flush()
t = timeit.Timer("""
for s, i in l:
    d.setdefault(s, i)
""",
setup_statement)
show_results(t.timeit(number=count))
# {{{end}}}

既存の値を上書きしない別の方法として、ディクショナリのコンテンツを明示的にチェックする has_key() を使用します。

# {{{cog include('timeit/timeit_dictionary.py', 'has_key')}}}
# has_key を使用する
print 'has_key:\t',
sys.stdout.flush()
# setitem を使用する
t = timeit.Timer("""
for s, i in l:
    if not d.has_key(s):
        d[s] = i
""",
setup_statement)
show_results(t.timeit(number=count))
# {{{end}}}

もしくは、値が存在しているかを調べたときに KeyError 例外を受け取ったらその値を追加するようにします。

# {{{cog include('timeit/timeit_dictionary.py', 'exception')}}}
# exceptions を使用する
print 'KeyError:\t',
sys.stdout.flush()
# setitem を使用する
t = timeit.Timer("""
for s, i in l:
    try:
        existing = d[s]
    except KeyError:
        d[s] = i
""",
setup_statement)
show_results(t.timeit(number=count))
# {{{end}}}

最後のメソッドは、ディクショナリがキーを持っているかを “in” で調べる(比較的)新しいやり方です。

# {{{cog include('timeit/timeit_dictionary.py', 'in')}}}
# "in" を使用する
print '"not in":\t',
sys.stdout.flush()
# setitem を使用する
t = timeit.Timer("""
for s, i in l:
    if s not in d:
        d[s] = i
""",
setup_statement)
show_results(t.timeit(number=count))
# {{{end}}}

このスクリプトを実行すると、次のような実行結果が出力されます。

$ python timeit_dictionary.py
1000 items
1000 iterations

__setitem__:    103.97 usec/pass 0.10 usec/item
setdefault:     219.30 usec/pass 0.22 usec/item
has_key:        176.95 usec/pass 0.18 usec/item
KeyError:       116.93 usec/pass 0.12 usec/item
"not in":       90.91 usec/pass 0.09 usec/item

この結果は MacBook Pro Python 2.6 での計測時間です。あなたの環境では違う結果になるでしょう。 range_sizecount 変数の値を違う組み合わせにすると、違う結果になることを実験してみてください。

コマンドラインから

プログラミングインタフェースに加えて、timeit はインストールせずにモジュールをテストするコマンドラインインタフェースを提供します。

timeit モジュールを実行するには、モジュールを探す -m を使用してメインプログラムとして扱います。

$ python -m timeit

例えば、ヘルプを確認します。

$ python -m timeit -h
Tool for measuring execution time of small code snippets.

This module avoids a number of common traps for measuring execution
times.  See also Tim Peters' introduction to the Algorithms chapter in
the Python Cookbook, published by O'Reilly.

Library usage: see the Timer class.

Command line usage:
    python timeit.py [-n N] [-r N] [-s S] [-t] [-c] [-h] [--] [statement]

Options:
  -n/--number N: how many times to execute 'statement' (default: see below)
  -r/--repeat N: how many times to repeat the timer (default 3)
  -s/--setup S: statement to be executed once initially (default 'pass')
  -t/--time: use time.time() (default on Unix)
  -c/--clock: use time.clock() (default on Windows)
  -v/--verbose: print raw timing results; repeat for more digits precision
  -h/--help: print this usage message and exit
  --: separate options from statement, use when statement starts with -
  statement: statement to be timed (default 'pass')

A multi-line statement may be given by specifying each line as a
separate argument; indented lines are possible by enclosing an
argument in quotes and using leading spaces.  Multiple -s options are
treated similarly.

If -n is not given, a suitable number of loops is calculated by trying
successive powers of 10 until the total time is at least 0.2 seconds.

The difference in default timer function is because on Windows,
clock() has microsecond granularity but time()'s granularity is 1/60th
of a second; on Unix, clock() has 1/100th of a second granularity and
time() is much more precise.  On either platform, the default timer
functions measure wall clock time, not the CPU time.  This means that
other processes running on the same computer may interfere with the
timing.  The best thing to do when accurate timing is necessary is to
repeat the timing a few times and use the best time.  The -r option is
good for this; the default of 3 repetitions is probably enough in most
cases.  On Unix, you can use clock() to measure CPU time.

Note: there is a certain baseline overhead associated with executing a
pass statement.  The code here doesn't try to hide it, but you should
be aware of it.  The baseline overhead can be measured by invoking the
program without arguments.

The baseline overhead differs between Python versions!  Also, to
fairly compare older Python versions to Python 2.3, you may want to
use python -O for the older versions to avoid timing SET_LINENO
instructions.

Timer へ渡す引数はちょっと違います。1行の長い文字列ではなく、行毎にコマンドライン引数を分割してコードを渡します。(内部ループといった)行をインデントするには、全体をクォートで囲んだ文字列の先頭にスペースを入れてください。例えば、次のようにします。

$ python -m timeit -s "d={}" "for i in range(1000):" "  d[str(i)] = i"
1000 loops, best of 3: 293 usec per loop

もっと複雑なコードで関数を定義したり、モジュールをインポートしたり、コマンドラインから関数呼び出しすることもできます。

def test_setitem(range_size=1000):
    l = [ (str(x), x) for x in range(range_size) ]
    d = {}
    for s, i in l:
        d[s] = i

このサンプルプログラムを実行します。

$ python -m timeit "import timeit_setitem; timeit_setitem.test_setitem()"
1000 loops, best of 3: 423 usec per loop

See also

timeit
本モジュールの標準ライブラリドキュメント
profile
パフォーマンス解析には profile モジュールも有効
Bookmark and Share