JavaScriptを有効にしてください

【Python】定数を持つ関数の実行を高速化する

 ·   4 min read

はじめに

Pythonの関数において、毎回決まった値(定数)を必要としたい場合があります。たとえば、物理シミュレーションを実行したいとき、パラメータなどが定数になります。
この定数をどのように記述すれば処理が速くなるか検証しました。

結論から書くと、以下の4つの方法では、上の方法ほど実行が速くなります。

  1. 定数を変数に代入せず、処理に直接埋め込む。
  2. 関数の外に、定数をグローバル変数として定義する。
  3. 関数の中に定数をローカル変数として定義する。
  4. 定数を関数の引数にする。

ただし、どの方法を使っても実行速度は1割程度しか変化しないため、ソースコードの可読性やメンテナンス性を重視したほうが良いと思います。

検証した環境は以下の通りです。

  • OS: Windows 10 Home
  • CPU: Intel Celeron G3930
  • Memory: 8GB
  • Python 3.8.8

関数と定数

この記事では、以下のように関数の中で定数を使用したいケースを考えます。

1
2
3
4
def func(x):
    CONST = 3.14
    y = CONST*x
    return y

CONSTが定数です。例のように簡単な関数の場合はCONSTという定数を定義せずに直接y = 3.14*xとした方が良いですが、定数を繰り返し使う場合や、後々値を変更する可能性がある場合は定義した方が良いです。

しかし、この例では関数を呼び出す度にCONST = 3.14の処理が実行されるのではと思ったので、どのように定数を記述すれば高速に処理できるのか測定しました。

なお、Pythonには厳密な定数(上書き不可能な変数)を記述する仕組みがありませんが、コード実行時に変更されない変数のことを便宜的に「定数」と呼んでいます。

実行したコード

処理時間を測定するコードを以下に示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import timeit

CONST1 = 3.14

def multi_global(x):
    y = CONST1*x
    return y

def multi_local(x):
    CONST2 = 3.14
    y = CONST2*x
    return y

def multi_embedded(x):
    y = 3.14*x
    return y

def multi_arg(x, c):
    y = c*x
    return y

x = 1.23
loop = int(1e8)

res1 = timeit.timeit('multi_global(x)', globals=globals(), number=loop)
res2 = timeit.timeit('multi_local(x)', globals=globals(), number=loop)
res3 = timeit.timeit('multi_embedded(x)', globals=globals(), number=loop)
res4 = timeit.timeit('multi_arg(x, CONST1)', globals=globals(), number=loop)

print(f'multi_global:   {res1/loop*1e9:.3f} [ns]')
print(f'multi_local:    {res2/loop*1e9:.3f} [ns]')
print(f'multi_embedded: {res3/loop*1e9:.3f} [ns]')
print(f'multi_arg:      {res4/loop*1e9:.3f} [ns]')

定数の記述方法が異なる以下の4つの関数を作りました。

  • multi_global: 定数をグローバル変数として記述
  • multi_local: 定数をローカル変数として記述
  • multi_embedded: 定数を定義せず、処理に直接記述
  • multi_arg: 定数を引数にとる

multi_localでは、定数に代入する処理が毎回入るので遅そうですが、可読性が高いため作りました。
multi_embeddedでは可読性やメンテナンス性が落ちますが、処理が一番速そうです。

また、実行時間の測定にはtimeit.timeit関数を用いています。引数のnumberは繰り返し回数です。

コードを実行すると、各関数の平均実行時間を表示します(処理に1分程度掛かります)。

実行結果

実行結果は以下の通りです。

1
2
3
4
multi_global:   137.317 [ns]
multi_local:    151.746 [ns]
multi_embedded: 133.943 [ns]
multi_arg:      154.224 [ns]

平均実行時間をナノ秒で示しています。

予想通り、一番高速なのは定数を埋め込んだmulti_embeddedです。
次に速いのが、グローバル変数を参照するmulti_globalです。

上位2つから時間が空いて、ローカル変数を毎回定義するmulti_localが3位となりました。定数の代入にはキャッシュが効いていないようです。
最も遅かったのが、定数を引数にとるmulti_argでした。

ただし、最も速いmulti_embeddedと最も遅いmulti_argを比較しても、差は13%程度しかありません。
よほど実行速度が問題になる状況でもない限り、可読性やメンテナンス性を考慮した方が良いと思います。

まとめ

関数内で定数を使用する場合、以下の4つの方法では、上の方法ほど実行が速くなります。

  1. 定数を変数に代入せず、処理に直接埋め込む。
  2. 関数の外に、定数をグローバル変数として定義する。
  3. 関数の中に定数をローカル変数として定義する。
  4. 定数を関数の引数にする。

ただし、どの方法を使っても実行速度は1割程度しか変化しないため、ソースコードの可読性やメンテナンス性を重視したほうが良いです。

シェアする

Helve
WRITTEN BY
Helve
関西在住、電機メーカ勤務のエンジニア。Twitterで新着記事を配信中です

サイト内検索