ラベル ベンチマーク の投稿を表示しています。 すべての投稿を表示
ラベル ベンチマーク の投稿を表示しています。 すべての投稿を表示

2019年8月16日

ベンチマーク15 functools.total_ordering

functools.total_ordering を使う場合と使わない場合の動作速度について比較します。
functools.total_ordering を使う場合は、クラスは __eq__() と __lt__() のみ実装し、total_ordering で修飾されています。使わない場合は、クラスは残りの __ne__()、__le__()、__gt__()、__ge__()も実装されています。

==、!=、<、<=、>、>= の6演算子全てを呼んだの速度と、== と < だけを呼んだ場合の速度の、二通りについて比較を行います。


6演算子全てを呼ぶベンチマークのソースコードです。

rom functools import total_ordering
from benchmarker import Benchmarker

with Benchmarker(1000000, width=20, cycle=3, extra=1) as bench:
    @bench("total_ordering")
    def _(bm):
        # クラス定義
        @total_ordering  # デコレータ
        class Stored(object):  # __lt__() と __eq__() のみ定義
            def __init__(self, value):
                self.value = value

            def __lt__(self, rhs):
                return self.value < rhs.value

            def __eq__(self, rhs):
                return self.value == rhs.value

        lhs = Stored(1)
        rhs = Stored(2)
        for _ in bm:
            # 6演算子全てを呼び出し
            lhs == rhs
            lhs != rhs
            lhs < rhs
            lhs <= rhs
            lhs > rhs
            lhs >= rhs

    @bench("normal")
    def _(bm):
        # クラス定義
        class Stored(object):  # 6演算子全てを定義
            def __init__(self, value):
                self.value = value

            def __lt__(self, rhs):
                return self.value < rhs.value

            def __le__(self, rhs):
                return self.value <= rhs.value

            def __gt__(self, rhs):
                return self.value > rhs.value

            def __ge__(self, rhs):
                return self.value >= rhs.value

            def __eq__(self, rhs):
                return self.value == rhs.value

            def __ne__(self, rhs):
                return self.value != rhs.value

        lhs = Stored(1)
        rhs = Stored(2)
        for _ in bm:
            # 6演算子全てを呼び出し
            lhs == rhs
            lhs != rhs
            lhs < rhs
            lhs <= rhs
            lhs > rhs
            lhs >= rhs
測定結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.7.3
## python compiler:     Clang 6.0 (clang-600.0.57)
## python platform:     Darwin-18.7.0-x86_64-i386-64bit
...

## Ranking                real
normal                  1.1379  (100.0) ********************
total_ordering          1.7093  ( 66.6) *************
total_ordering は使わず、6つの演算子を自力で定義した方が、動作速度は速いです。
公式ドキュメントに書いてある通りの結果となりました。




続いて、total_ordering を使っているクラスでも定義されている、== と < だけを呼び出した場合の動作速度を比較します。

rom functools import total_ordering
from benchmarker import Benchmarker

with Benchmarker(1000000, width=20, cycle=3, extra=1) as bench:
    @bench("total_ordering")
    def _(bm):
        # クラス定義
        @total_ordering  # デコレータ
        class Stored(object):  # __lt__() と __eq__() のみ定義
            def __init__(self, value):
                self.value = value

            def __lt__(self, rhs):
                return self.value < rhs.value

            def __eq__(self, rhs):
                return self.value == rhs.value

        lhs = Stored(1)
        rhs = Stored(2)
        for _ in bm:
            # == と < だけ呼び出し
            lhs == rhs
            lhs < rhs

    @bench("normal")
    def _(bm):
        # クラス定義
        class Stored(object):  # 6演算子全てを定義
            def __init__(self, value):
                self.value = value

            def __lt__(self, rhs):
                return self.value < rhs.value

            def __le__(self, rhs):
                return self.value <= rhs.value

            def __gt__(self, rhs):
                return self.value > rhs.value

            def __ge__(self, rhs):
                return self.value >= rhs.value

            def __eq__(self, rhs):
                return self.value == rhs.value

            def __ne__(self, rhs):
                return self.value != rhs.value

        lhs = Stored(1)
        rhs = Stored(2)
        for _ in bm:
            # == と < だけ呼び出し
            lhs == rhs
            lhs < rhs
測定結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.7.3
## python compiler:     Clang 6.0 (clang-600.0.57)
## python platform:     Darwin-18.7.0-x86_64-i386-64bit
...

## Matrix                 real    [01]    [02]
[01] total_ordering     0.4125   100.0   100.3
[02] normal             0.4136    99.7   100.0
結果、動作速度は同じでした。
total_ordering が動作速度的に不利になるのは、未定義の演算子の結果を他の演算子から推測する処理部分にあるようです。

2019年5月4日

ベンチマーク14 operator.itemgetter と lambda

operator.itemgetter と lambda の動作速度について比較します。
ここでは、それぞれを単独で呼んだ場合の速度と、sorted() の key として指定した場合の速度の、二通りについて比較を行います。


単独で呼ぶ場合のベンチマークのソースコードです。
(呼び出し速度の差を失くすため、計測対象の関数を一旦ローカル変数 f に代入するようにしています)

from operator import itemgetter
from benchmarker import Benchmarker

t = ('Python', 1990, 'Guido')

with Benchmarker(1000000, width=20, cycle=3, extra=1) as bench:
    @bench("itemgetter")
    def _(bm):
        f = itemgetter(1)  # 一旦ローカル変数で受ける
        for _ in bm:
            f(t)

    @bench("lambda")
    def _(bm):
        f = lambda x: x[1]  # 一旦ローカル変数で受ける
        for _ in bm:
            f(t)
測定結果です。
## benchmarker:      release 4.0.1 (for python)
## python version:   3.7.3
## python compiler:  Clang 6.0 (clang-600.0.57)
## python platform:  Darwin-18.5.0-x86_64-i386-64bit
...

## Ranking             real
itemgetter           0.0945  (100.0) ********************
lambda               0.1189  ( 79.5) ****************
劇的と言うほどではありませんが、itemgetter の方が速いようです。





続いて、よく使うと思われる sorted() の key に使用した場合を比較します。

from operator import itemgetter
from benchmarker import Benchmarker

t = (('Python', 1990, 'Guido'), ('Ruby', 1995, 'Matz'), ('Perl', 1987, 'Larry'))

with Benchmarker(1000000, width=20, cycle=3, extra=1) as bench:
    @bench("itemgetter")
    def _(bm):
        f = itemgetter(1)  # 一旦ローカル変数で受ける
        for _ in bm:
            sorted(t, key=f)

    @bench("lambda")
    def _(bm):
        f = lambda x: x[1]  # 一旦ローカル変数で受ける
        for _ in bm:
            sorted(t, key=f)
測定結果です。
## benchmarker:      release 4.0.1 (for python)
## python version:   3.7.3
## python compiler:  Clang 6.0 (clang-600.0.57)
## python platform:  Darwin-18.5.0-x86_64-i386-64bit
...

## Ranking             real
itemgetter           0.4499  (100.0) ********************
lambda               0.5490  ( 81.9) ****************
こちらでも、やはり itemgetter の方が若干速いという結果になりました。

2016年4月25日

ベンチマーク13 tupleとlist(2)

tuple と list の動作速度について比較します。ここでは、tuple と list の要素へのアクセスにかかる速度を比較します。


ベンチマークのソースコードです。
for文で直接回すパターンと、[i]経由でアクセスするパターンについてそれぞれ計測します。

from benchmarker import Benchmarker

with Benchmarker(10000000, width=20, cycle=3, extra=1) as bench:
    @bench("tuple for")
    def _(bm):
        t = (1, 2, 3)
        for _ in bm:
            for i in t:
                pass

    @bench("list for")
    def _(bm):
        l = [1, 2, 3]
        for _ in bm:
            for i in l:
                pass

    @bench("tuple [i]")
    def _(bm):
        t = (1, 2, 3)
        for _ in bm:
            for i in range(len(t)):
                t[i]

    @bench("list [i]")
    def _(bm):
        l = [1, 2, 3]
        for _ in bm:
            for i in range(len(l)):
                l[i]

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
list for      5.4142  ( 29.0) ******
tuple for     5.5591  ( 28.2) ******
list [i]     25.0635  (  6.3) *
tuple [i]    25.2364  (  6.2) *

計測結果では list へのアクセスの方が高速となりました。
しかし、その差は僅かであり、実際 tuple の方が高速な回もありました。なので、アクセス速度に関しては tuple と list で同等と見なす方が良さそうです(最低限、アクセス速度を意識して tuple を一旦 list に変換する、というような処理は行うべきではありません)。

2016年4月18日

ベンチマーク12 tupleとlist(1)

tuple と list の動作速度について比較します。ここでは、tuple と list の作成にかかる速度を比較します。


ベンチマークのソースコードです。

from benchmarker import Benchmarker

with Benchmarker(10000000, width=20, cycle=3, extra=1) as bench:
    @bench("tuple")
    def _(bm):
        for _ in bm:
            t = (1, 2, 3)

    @bench("list")
    def _(bm):
        for _ in bm:
            l = [1, 2, 3]

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
tuple       1.5684  (100.0) ********************
list        6.5198  ( 24.1) *****

tuple の方が高速となりました。しかも、決して軽視できないレベルの差です。

結果より、以下のような変更されないことが分かっている定数を作る場合、tuple の方が動作速度の面で優れているということになります。
ちょっとしたことですが、意識していきたいですね。

# tupleを使用。こちらが推奨される
def accept_color(color):
    return color in ('RGB', 'RGBA')

# listを使用。動作が若干遅い
def accept_color(color):
    return color in ['RGB', 'RGBA']

2016年1月10日

ベンチマーク11 lazy import

lazy import の3つのやり方について、繰り返し import 箇所を呼んだ場合の速度を計測します。


まず、ループの中で math.pi を一度だけ呼び出す処理のベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

# グローバル変数と専用クラスは先に定義
g_math = __import__('math')
c_math = LazyImport('math')

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        for _ in bm:
            import math
            math.pi

    @bench("global")
    def _(bm):
        for _ in bm:
            global g_math
            if g_math is None:
                g_math = __import__('math')
            g_math.pi

    @bench("class")
    def _(bm):
        for _ in bm:
            c_math.pi

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking     real
global       0.5394  (100.0) ********************
local        5.1763  ( 10.4) **
class        5.2870  ( 10.2) **
計測結果からは、グローバル変数式が圧倒的に高速となりました。
この計測では、ローカルimport と LazyImportクラスはほぼ等速です。


続けて、ループの中で math.pi を2回呼び出す処理のベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

# グローバル変数と専用クラスは先に定義
g_math = __import__('math')
c_math = LazyImport('math')

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        for _ in bm:
            import math
            math.pi
            math.pi

    @bench("global")
    def _(bm):
        for _ in bm:
            global g_math
            if g_math is None:
                g_math = __import__('math')
            g_math.pi
            g_math.pi

    @bench("class")
    def _(bm):
        for _ in bm:
            c_math.pi
            c_math.pi

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking     real
global       0.7084  (100.0) ********************
local        5.4640  ( 13.0) ***
class       10.2182  (  6.9) *
グローバル変数式が相変わらず速いです。
そして、LazyImportクラスは如実に遅くなりました。これは、LazyImportクラスは参照される度に getattr() を呼ぶことが原因です。


結論として、以下の2つが言えます。

  • import は2回目以降でもそれなりに重い処理である。よって、import 箇所に繰り返し通る可能性があるなら、グローバル変数式を使うべき。
  • LazyImportクラスは動作が遅い。元々高速化のための lazy import であるのに、これではあまり使い道がない。

2015年12月24日

ベンチマーク10 __dict__

クラスのアトリビュートへのアクセス速度について、__dict__ を使う方法と、setattr() / getattr() を使う方法の、ベンチマークを計測します。


まず、アトリビュートに値を代入する場合について、ベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

with Benchmarker(10000000, cycle=3, extra=1) as bench:
    class Sample(object):
        def __init__(self):
            self.sample = 0
    s = Sample()

    @bench("__dict__")
    def _(bm):
        for _ in bm:
            s.__dict__['sample'] = 1

    @bench("setattr")
    def _(bm):
        for _ in bm:
            setattr(s, 'sample', 1)

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
__dict__    5.6670  (100.0) ********************
setattr     8.3321  ( 68.0) **************
__dict__ を用いた方が高速です。


続いて、アトリビュートの値を取得する場合について、ベンチマークを計測します。
ベンチマークのソースコードです。

from benchmarker import Benchmarker

with Benchmarker(10000000, cycle=3, extra=1) as bench:
    class Sample(object):
        def __init__(self):
            self.sample = 0
    s = Sample()

    @bench("__dict__")
    def _(bm):
        for _ in bm:
            s.__dict__['sample']

    @bench("getattr")
    def _(bm):
        for _ in bm:
            getattr(s, 'sample')

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking                real
__dict__                5.4146  (100.0) ********************
getattr                 7.1281  ( 76.0) ***************
こちらも、__dict__ を用いた方が高速となりました。


結果より、__dict__ を用いる方が setattr() / getattr() が高速でした。この結果だけを見ると、__dict__ を使いたくなってしまいます。
しかし! こちらで述べたように、__dict__ はクラスのインターフェースを考慮せずに内部情報に直接アクセスします。所構わず使っていると、良くない結果に繋がりかねません。
__dict__ を使うのは速度が必要になった時の最終手段、ということを忘れないようにしましょう。

2015年12月23日

ベンチマーク9 __slots__

クラスに __slots__ を定義する場合としない場合におけるアトリビュートへのアクセス速度について、ベンチマークを計測します。
__slots__ についてはこちらを参照下さい。


ベンチマークのソースコードです。__slots__ を定義する場合と定義しない場合のそれぞれについて計測します。

from benchmarker import Benchmarker

with Benchmarker(10000000, cycle=3, extra=1) as bench:
    @bench("no slots")
    def _(bm):
        class Sample(object):
            def __init__(self):
                self.sample = 0

        s = Sample()
        for _ in bm:
            s.sample = 1

    @bench("use slots")
    def _(bm):
        class Sample(object):
            __slots__ = ['sample']
            def __init__(self):
                self.sample = 0

        s = Sample()
        for _ in bm:
            s.sample = 1

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
use slots   3.8496  (100.0) ********************
no slots    4.3632  ( 88.2) ******************
__slots__ を定義する方が高速になりました。
__slots__ を定義しない通常のクラスでは、アトリビュートの追加・削除に備えるため dict を使ってアトリビュートを管理します。dict の処理が必要な分、アトリビュートの追加のないクラスよりも動作が遅くなったと考えられます。

結論としては、__slots__ を定義することで、クラスのサイズを小さくしつつ、さらにアトリビュートへのアクセスも速くすることができる、ということになります。
もちろん、アトリビュートの追加ができなくなるという副作用は忘れてはなりません。

2015年12月19日

ベンチマーク8 localとbuilt-in

こちらでローカル変数とグローバル変数の呼び出し速度について比較しました。
ここでは、ローカル関数とビルトイン関数の呼び出し速度を比較します。


ベンチマークのソースコードです。
str から int へのキャストについて、ビルトインの int() と一旦ローカル変数に代入した int() の速度を計測します。

from benchmarker import Benchmarker

with Benchmarker(1000000, cycle=3, extra=1) as bench:
    s = '1'

    @bench("builtin")
    def _(bm):
        for _ in bm:
            int(s)

    @bench("local")
    def _(bm):
        local_int = int
        for _ in bm:
            local_int(s)

計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking    real
local       1.3894  (100.0) ********************
builtin     1.4500  ( 95.8) *******************
一旦ローカル変数にした方が、若干ですが高速になりました。
何万回というオーダーで int() を呼び出すのが分かっているのであれば、ローカル変数化した方が良いようです。

2015年12月1日

内包表記の話2

リスト内包表記とジェネレーター内包表記を動作速度の観点から比較します。


以下は sum([i for i in range(10000)]) と sum(i for i in range(10000)) の動作速度を比較するベンチマークです。

from benchmarker import Benchmarker

with Benchmarker(1000, cycle=5, extra=1) as bench:
    sample = range(10000)
    @bench("list")
    def _(bm):
        for _ in bm:
            sum([i for i in sample])

    @bench("generator")
    def _(bm):
        for _ in bm:
            sum(i for i in sample)

ベンチマーク結果です。
## Ranking      real
list          2.4163  (100.0) ********************
generator     3.1262  ( 77.3) ***************
結果からは、リスト内包表記の方が高速、と言えます。
この結果だけを見ると、ジェネレーター内包表記を使う理由は見当たりません。




次に、range の値を 10000000 と先例よりもずっと大きくしてみます。

from benchmarker import Benchmarker

with Benchmarker(5, cycle=5, extra=1) as bench:
    sample = range(10000000)
    @bench("list")
    def _(bm):
        for _ in bm:
            sum([i for i in sample])

    @bench("generator")
    def _(bm):
        for _ in bm:
            sum(i for i in sample)

結果です。
## Ranking      real
generator    26.6801  (100.0) ********************
list         27.8582  ( 95.8) *******************
今度はジェネレータ内包表記の方が高速になりました。


さらに、ベンチマーク実行中のメモリ使用量を計測してみます。

# リスト内包表記動作中
C:\>tasklist /fi "imagename eq python.exe"
イメージ名    PID セッション名  メモリ使用量
=========== ===== ============= ============
python.exe   2772 Console          210,660 K

# ジェネレータ内包表記動作中
C:\>tasklist /fi "imagename eq python.exe"
イメージ名    PID セッション名  メモリ使用量
=========== ===== ============= ============
python.exe   2772 Console           14,704 K

リスト内包表記は、200MB を超えるメモリ使用量となっています。サイズが 10000000 のリストを作成するため、メモリ上に 10000000個の int を確保しているので、メモリ使用量が増大していると思われます(と同時に、処理速度が遅い原因になっている)。
他方、ジェネレータ内包表記では、メモリ使用量は極々平常です。
メモリ使用量の比較においては、ジェネレータが遅延評価で動作することの利点が強く出ています。





結果を振り返ります。
forループの回数が小さければ、リスト内包表記の方がジェネレータ内包表記よりも高速でした。
forループの回数が大きくなると、ジェネレータ内包表記の方が高速となり、かつメモリ使用量が少なくて済みました。

この結果を踏まえ、forループのサイズによってリスト内包表記かジェネレータ内包表記かを切り替える、という手法が考えられます。
が、個人的には、この手法は早過ぎる最適化になるのではないかと思います。どこまでが小さいデータでどこからが大きなデータなのか、というのは環境に依存するので一概には言えませんし、速度に気を取られてメモリ不足が起こってしまっては元も子もありません。

「ジェネレータ内包表記で書ける場面では常にジェネレータ内包表記を使う」という方式の方が、ルールとしてシンプルで良いのではないかと思います(もちろん、処理速度が問題になれば適宜対応が必要となります)。

2015年11月25日

ベンチマーク7 Noneチェック

こちらで「a is None は a == None よりも速い」と述べました。実際にどのくらい速いのか、ベンチマークを計測してみました。


ベンチマークのソースコードです。

from benchmarker import Benchmarker

with Benchmarker(10000000, cycle=3, extra=1) as bench:
    a = 0

    @bench("a is")
    def _(bm):
        for _ in bm:
            a is None

    @bench("a ==")
    def _(bm):
        for _ in bm:
            a == None
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking     real
a is         3.3138  (100.0) ********************
a ==         3.6289  ( 91.3) ******************

確かに a is None の方が a == None よりも速かったです。劇的と言う程でもありませんが。

2015年11月18日

ベンチマーク6 リスト内包表記

リストを作成する操作について、ベンチマークを計測します。


リストを作成する操作として、以下の2つを比較します。1番目は空のリストを作ってから append していく方法、2番目はリスト内包表記、となります。

  1. list.append へのショートカットを利用
  2. リスト内包表記

ベンチマークのソースコードです。
from benchmarker import Benchmarker

with Benchmarker(1000, cycle=3, extra=1) as bench:
    sample = range(10000)

    @bench("append")
    def _(bm):
        for _ in bm:
            l = list()
            append = l.append
            for i in sample:
                append(i)

    @bench("comprehensions")
    def _(bm):
        for _ in bm:
            l = [i for i in sample]
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking                real
comprehensions          2.5492  (100.0) ********************
append                  3.2118  ( 79.4) ****************

計測結果より、リスト内包表記の方が高速であると言えます。
「list.append へのショートカットを利用」はこちらでは最速の手法でしたが、リスト内包表記はそれよりも高速でした。

さらに、リスト内包表記は記述が1行で済んでおり、高速さと簡潔さを兼ね備えた実装方法であると言えます。内包表記を使える場面では迷わず使っていきましょう。

2015年11月17日

ベンチマーク5 append

リストに要素を追加する操作について、ベンチマークを計測します。


リストを作成する操作として、以下の3つを比較します。1番目は標準的な方法、2番目はよく知られた高速化手法、3番めは可読性を重視した手法、となります。

  1. list.append() を呼ぶ
  2. list.append へのショートカットを利用
  3. list += [i] と呼ぶ

ベンチマークのソースコードです。
from benchmarker import Benchmarker

with Benchmarker(1000, cycle=3, extra=1) as bench:
    sample = range(10000)

    @bench("append")
    def _(bm):
        for _ in bm:
            l = list()
            for i in sample:
                l.append(i)

    @bench("shortcut append")
    def _(bm):
        for _ in bm:
            l = list()
            append = l.append
            for i in sample:
                append(i)

    @bench("+= []")
    def _(bm):
        for _ in bm:
            l = list()
            for i in sample:
                l += [i]
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking          real
shortcut append   3.5932  (100.0) ********************
append            5.7150  ( 62.9) *************
+= []             8.6328  ( 41.6) ********

計測結果から、以下のことが言えます。
  • append のショートカット化はかなり有効
  • list += [i] は遅い
append のショートカット化の効果は軽視できないレベルです。そして、クラスメソッドのショートカット化は append 以外でも使えます。メソッドを繰り返し呼ぶ処理では積極的に使っていきたいところです。
list += [i] は、視覚的に分かりやすく個人的には嫌いではないのですが、動作速度の点からはイマイチのようです。

2015年11月13日

ベンチマーク4 pow

累乗演算について、ベンチマークを計測します。


標準のPythonでは、累乗を行う操作が3つ用意されています。それぞれの計算に掛かる時間を比較します。

  1. ** 演算子
  2. pow()
  3. math.pow()

ベンチマークのソースコードです。
from benchmarker import Benchmarker
import math

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("** operator")
    def _(bm):
        for _ in bm:
            3.14 ** 2.5

    @bench("pow")
    def _(bm):
        for _ in bm:
            pow(3.14, 2.5)

    @bench("math.pow")
    def _(bm):
        for _ in bm:
            math.pow(3.14, 2.5)
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking        real
** operator     0.1615  (100.0) ********************
pow             1.0156  ( 15.9) ***
math.pow        1.1458  ( 14.1) ***

ご覧の通り計測結果からは「**演算子が圧倒的に速い!」と言えます。ここまで差がつくとは正直驚きです。
(math.pow() はフルパスで呼ばないようにすればもう少し高速になります。それでも、**演算子の速さには全く敵いません。)

累乗演算には、**演算子を使うのが良いようです。

2015年11月12日

ベンチマーク3 π

mathに定義されているグローバル変数 pi へのアクセス速度について、ベンチマークを計測します。


math.pi へのアクセス方法として以下の3パターン用意し、それぞれの処理速度を比較します。

  1. フルパスで math.pi
  2. from math import piとインポート
  3. いったんローカル変数 pi にコピー

ベンチマークのソースコードです。
from benchmarker import Benchmarker
import math

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("math.pi")
    def _(bm):
        for _ in bm:
            2.0 * math.pi

    @bench("import pi")
    def _(bm):
        from math import pi
        for _ in bm:
            2.0 * pi

    @bench("local")
    def _(bm):
        pi = math.pi
        for _ in bm:
            2.0 * pi
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
local         0.2657  (100.0) ********************
import pi     0.2703  ( 98.3) ********************
math.pi       0.4347  ( 61.1) ************

計測結果は、local と import pi はほぼ等速(localの方が遅い回もありました)、math.pi は低速、となりました。

いわゆる数学アルゴリズムでは math.pi へのアクセスが多くなる場合があります。math.pi とフルパスで書くのは、速度的には若干不利となるようです。
また、ドットを含むパスを書くと遅くなるというのは、math.pi に限らず全ての変数に言えることです。

2015年11月9日

ベンチマーク2 localとglobal

ローカル変数へのアクセスと、グローバル変数へのアクセスについて、ベンチマークを計測します。
計測は、①変数の読み込み ②変数への書き込み の2種類について行います


まずは、①変数の読み込み を行うベンチマークのソースコードです。

from benchmarker import Benchmarker

global_valiable = 0

with Benchmarker(10000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        local_valiable = 0
        for _ in bm:
            dummy = local_valiable

    @bench("global")
    def _(bm):
        for _ in bm:
            dummy = global_valiable
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
local         1.5814  (100.0) ********************
global        2.1379  ( 74.0) ***************
ローカル変数へのアクセスの方が高速のようです。



続いて、②変数への書き込み を行うベンチマークのソースコードです。

from benchmarker import Benchmarker

global_valiable = 0

with Benchmarker(10000000, cycle=3, extra=1) as bench:

    @bench("local")
    def _(bm):
        local_valiable = 0
        for _ in bm:
            local_valiable = 10

    @bench("global")
    def _(bm):
        global global_valiable
        for _ in bm:
            global_valiable = 10
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
local         1.5978  (100.0) ********************
global        2.0992  ( 76.1) ***************
こちらも、ローカル変数へのアクセスの方が高速のようです。




2つの計測結果から得られる結論は 「ローカル変数へのアクセスの方が、グローバル変数へのアクセスよりも高速である」 とであると言えます。

グローバル変数を変更することは、処理速度以前の問題として、可能な限り排除すべきです。
他方、定数のグローバル変数というのはマジカルナンバーを定義するためによく使われると思います。仮にこのベンチマークのように、何万回もグローバル変数を読み込むのであれば、いったんローカル変数にコピーする、などの工夫があっても良いかと思います。

2015年11月8日

ベンチマーク1 √2

math.sqrt() について、ベンチマークを計測します。


math.sqrt() を用いて√2の計算を行います。ここで、math.sqrt() へ渡す引数として以下の3パターン用意し、それぞれの処理速度を比較します。

  1. math.sqrt(2)
  2. math.sqrt(2.0)
  3. math.sqrt(float(2))

ベンチマークのソースコードです。
from benchmarker import Benchmarker
import math

with Benchmarker(1000000, cycle=3, extra=1) as bench:

    @bench("int")
    def _(bm):
        for _ in bm:
            math.sqrt(2)

    @bench("float")
    def _(bm):
        for _ in bm:
            math.sqrt(2.0)

    @bench("cast")
    def _(bm):
        for _ in bm:
            math.sqrt(float(2))
計測結果です。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking      real
float         0.8217  (100.0) ********************
int           1.4161  ( 58.0) ************
cast          2.2811  ( 36.0) *******


計測結果からは以下のことが言えます。
  • float を渡す方が、int を渡すよりも高速
  • 渡す前に自力で float に変換するのは全くの逆効果


この傾向は math.sqrt() に限らず、mathライブラリの多くの関数に当てはまります。
リテラル値を math の関数へ渡す際には、たとえ整数値であっても、math.sqrt(2) ではなく math.sqrt(2.0) のように float で渡す方が高速になるようです。本当にちょっとしたことですけどね。

2015年11月7日

Benchmarker.py

ベンチマークツール「Benchmarker.py」を紹介します。

公式ページからの抜粋によると、"Benchmarker.py is an awesome benchmarking tool for Python."とのことですが、使っているとまさにそのように感じます。



実際の使用例として、公式ページで説明されているサンプルに若干手を加えたソースを走らせてみます。

from benchmarker import Benchmarker

with Benchmarker(1000 * 1000, width=20) as bench:
    s1, s2, s3, s4, s5 = (
        'Haruhi', 'Mikuru', 'Yuki', 'Itsuki', 'Kyon'
    )

    @bench('join')
    def _(bm):
        for _ in bm:
            s = ''.join((s1, s2, s3, s4, s5))

    @bench('+ operator')
    def _(bm):
        for _ in bm:
            s = s1 + s2 + s3 + s4 + s5

    @bench('% operator')
    def _(bm):
        fmt = '%s%s%s%s%s'
        for _ in bm:
            s = fmt % (s1, s2, s3, s4, s5)

    @bench('format')
    def _(bm):
        fmt = '{0}{1}{2}{3}{4}'
        for _ in bm:
            s = fmt.format(s1, s2, s3, s4, s5)

以下のような結果が表示されます(一部抜粋)。
''.join()が高速であるというよく知られた事実を確認することが出来ました。
## benchmarker:         release 4.0.1 (for python)
## python version:      3.4.0
## python compiler:     MSC v.1600 32 bit (Intel)
## python platform:     Windows-8-6.2.9200
...

## Ranking                real
join                    1.1875  (100.0) ********************
+ operator              1.6094  ( 73.8) ***************
% operator              2.0491  ( 58.0) ************
format                  3.5278  ( 33.7) *******
...


私が感じた Benchmarker.py の長所について列挙します。
  • 計測用のソースコードが書きやすい
  • 出力結果が見やすい
  • 計測方法の細かなチューニングが可能(ここでは使っていませんが)
  • 簡単に使用する分には、大半のオプションは未使用のままでもOK


本ブログではベンチマークの測定に、この Benchmarker.py を使っていきます。