銀月の符号

Python 使い見習いの日記・雑記

Pythonの四則演算とビット論理演算の速度を自分でも測ってみた

bit 単位でデータが詰め込まれているバイナリデータを眺める機会があったので、なんとなく「Python ビット演算」でググッてみた。そんな日のこと。

すると Python の int 型における各種演算速度を実測した人を発見。Pythonの四則演算とビット論理演算の速度 - yattの日記 。この記事の最初の1文を読んで、自分が思い浮かべた予想は「加減算とビット演算は同じくらいの速さ、乗算除算は少し劣る」というもの。しかし、その結果は、「ビット演算より加減算の方が早い」、とのこと。

ちなみに自分の持っていた前知識はこんな感じ。下に行くほど自信なし。

  • Python の int はオブジェクト。不変型。
  • Python で int 同士の演算を行うと新たな int オブジェクトが生成される。
  • Python における int 同士のビット演算は Python の int オブジェクト2つを C の long 型に直すことから始まる。これらをビット演算。結果を元に Python の int オブジェクトを生成する。(Objects/intobject.c を昔、斜め読みした記憶より)
  • Python における int 同士の加算は Python の int オブジェクト2つを C の long 型に直すことから始まる。これらを加算。結果を int の範囲内に収まっているかどうかをチェックする。収まっていれば Python の int オブジェクト、収まっていなければ Python の long オブジェクトを生成する。(Objects/intobject.c を昔、斜め読みした記憶より)
  • CPython のオブジェクトの生成コストは C の long の加減算などとは比較にならないほど重い。もちろんビット演算より重い。

ビット演算も加減算も処理が似通っている上、その大半がオブジェクト生成処理のため、差はないと思っていたのだが。うん、コードの速さを推測しきろうとすることがいかに危険かを再認識。短いコード片の速度は推測したら実測もすべし。もう何回目だろう。

反省をさっそく生かすべく、うちの環境でも測ってみる。

> func_call_time.py
op      all[sec]        per call[sec]
------------------------------------------------------------
add     2.340e-001      5.584e-008
sub     2.350e-001      5.608e-008
mul     3.280e-001      7.828e-008
div     3.910e-001      9.331e-008
mod     3.430e-001      8.186e-008
logand  2.820e-001      6.730e-008
logor   2.960e-001      7.064e-008
logxor  2.970e-001      7.088e-008

結果をソートしたもの
add     2.340e-001      5.584e-008
sub     2.350e-001      5.608e-008
logand  2.820e-001      6.730e-008
logor   2.960e-001      7.064e-008
logxor  2.970e-001      7.088e-008
mul     3.280e-001      7.828e-008
mod     3.430e-001      8.186e-008
div     3.910e-001      9.331e-008

コードをすこしいじって timeit 使用に。再挑戦。

> intcalctime.py -n 10
op      all[sec]        per calc[sec]
------------------------------------------------------------
add     2.376e+000      5.669e-008
sub     2.393e+000      5.712e-008
logand  2.899e+000      6.918e-008
logor   2.922e+000      6.973e-008
logxor  2.940e+000      7.016e-008
mul     3.372e+000      8.048e-008
mod     3.445e+000      8.221e-008
div     4.088e+000      9.756e-008

たしかに。うちの環境でも logand らビット演算より add, sub のほうが早い。しかし実装 Objects\intobject.c を読み直すと以下のようになっていて、 and のほうがシンプル。ナゾが残った。このナゾ、解ける人求む?

static PyObject *
int_add(PyIntObject *v, PyIntObject *w)
{
	register long a, b, x;
	CONVERT_TO_LONG(v, a);
	CONVERT_TO_LONG(w, b);
	x = a + b;
	if ((x^a) >= 0 || (x^b) >= 0)
		return PyInt_FromLong(x);
	return PyLong_Type.tp_as_number->nb_add((PyObject *)v, (PyObject *)w);
}

/* 中略 */

static PyObject *
int_and(PyIntObject *v, PyIntObject *w)
{
	register long a, b;
	CONVERT_TO_LONG(v, a);
	CONVERT_TO_LONG(w, b);
	return PyInt_FromLong(a & b);
}

検証に用いた func_call_time.py コードは Pythonの四則演算とビット論理演算の速度 - yattの日記 より。intcalctime.py コードはこちら。

#! /bin/env python
# coding: utf-8
# intcalctime.py

u"""int 型同士の各種演算の速度計測

original
  yatt の日記
  http://d.hatena.ne.jp/yatt/20080904
"""

import timeit
import optparse

MAX_VALUE = 2048
NAMES = ("add", "sub", "mul", "div", "mod",
         "logand", "logor", "logxor")
OPS = ("+", "-", "*", "/", "%",
       "&", "|", "^")

def make_testcode(op):
    u"""テストコード生成"""

    return """for i in xrange(1, %d):
    for j in xrange(1, %d):
        i %s j
""" % (MAX_VALUE, MAX_VALUE, op)

def count_time(op, repeat_num=1):
    u"""op 演算子によるテストコードを repeat_num 回実行し、速度計測する"""

    timer = timeit.Timer(make_testcode(op))
    return timer.timeit(repeat_num)

def main():
    usage = '%prog [-n repeat_num]'
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('-n', '--repeat_num', action='store', type='int')
    parser.set_defaults(repeat_num=1)
    options, args = parser.parse_args()

    repeat_num = options.repeat_num

    results = []
    for name, op in zip(NAMES, OPS):
        time = count_time(op, repeat_num)
        results.append((time, name, op))
    results.sort()

    print "op\tall[sec]\tper calc[sec]"
    print "-" * 60

    format = "%s\t%.3e\t%.3e"
    for time, name, op in results:
        print format % (name, time, time / repeat_num / (MAX_VALUE-1)**2)

if __name__ == '__main__':
    main()