Python の関数呼び出しの速度

2016年2月7日

はじめに

Python の関数は呼び出しの記述の仕方によって実行時間が変わるらしいので、その調査をしてみた。元ネタは Micha Gorelick, Ian Ozsvald 著 "ハイパフォーマンス Python"。

環境

  • Intel Core i5 M450 2.4 GHz
  • Windows 7 64 bit
  • MSYS2 64 bit (MinGW w64)
  • Anaconda for Windows (Python 2.7)

※MinGW にも Python が入っているので、Anaconda のほうを使うようにパスを設定している。

コード

サンプルコードは以下の通り。

import math
from math import sin


def test(n):
    dx = 2*math.pi/(n - 1)
    r = []
    for i in range(n):
        x = i*dx
        f = math.sin(x)
        r.append((x, f))
    return r


def test2(n):
    dx = 2*math.pi/(n - 1)
    r = []
    for i in range(n):
        x = i*dx
        f = sin(x)
        r.append((x, f))
    return r


def test3(n):
    dx = 2*math.pi/(n - 1)
    r = []
    sin_l = math.sin
    for i in range(n):
        x = i*dx
        f = sin_l(x)
        r.append((x, f))
    return r


def test4(n):
    dx = 2*math.pi/(n - 1)
    r = []
    for i in xrange(n):
        x = i*dx
        f = sin(x)
        r.append((x, f))
    return r

それぞれ x = [0, 2π] の範囲で sin 関数を計算した結果をリストで返すものであるが、以下の違いがある。

  • test() : math.sin() で呼び出し
  • test2() : sin() で呼び出し
  • test3() : 前もってローカル変数に関数をセットして呼び出し
  • test4() : test2() で range() の代りに xrange() を使う

逆アセンブル

以下のようにすると、関数のバイトコードを逆アセンブルできる。

import dis
dis.dis(test)

sin 関数呼び出し部分のコードは、それぞれ以下の通りである。

test

 10          56 LOAD_GLOBAL              0 (math)
             59 LOAD_ATTR                3 (sin)
             62 LOAD_FAST                4 (x)
             65 CALL_FUNCTION            1
             68 STORE_FAST               5 (f)

test2

 20          56 LOAD_GLOBAL              3 (sin)
             59 LOAD_FAST                4 (x)
             62 CALL_FUNCTION            1
             65 STORE_FAST               5 (f)

test3

 31          65 LOAD_FAST                3 (sin_l)
             68 LOAD_FAST                5 (x)
             71 CALL_FUNCTION            1
             74 STORE_FAST               6 (f)

test() では、math を検索してから sin を検索している。test2() では sin の検索だけである。test3 はローカル変数を参照している。

実行時間の比較

以下に timeit による実行時間を示す。

$ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test(1000000)"
5 loops, best of 5: 642 msec per loop

$ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test2(1000000)"
5 loops, best of 5: 558 msec per loop

$ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test3(1000000)"
5 loops, best of 5: 578 msec per loop

余計な処理がない分、やはり test() より test2() の方が実行時間が短い。文献では test3() のほうが test2() より時間が短くなるようだが、今回はそんなことはなかった。

range() より xrange() のほうが実行速度が速いらしいが、結果は以下の通りで、確かに速くなっている。

$ python -m timeit -n 5 -r 5 -s "import call_func" "call_func.test4(1000000)"
5 loops, best of 5: 522 msec per loop

参考文献