メインコンテンツへスキップ

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

·1522 文字·4 分
目次

はじめに
#

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

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

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

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

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

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

関数と定数
#

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

def func(x):
    CONST = 3.14
    y = CONST*x
    return y

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

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

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

実行したコード
#

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

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分程度掛かります)。

実行結果
#

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

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
著者
Helve
関西在住、電機メーカ勤務のエンジニア。X(旧Twitter)で新着記事を配信中です

関連記事

Condaの仮想環境をYAMLファイルに保存する
·2095 文字·5 分
Condaで構築した仮想環境をYAML形式のファイルに保存し、再構築する方法を解説する。
scipy.interpolate.interp2dによる2次元データの補間を解説
·1625 文字·4 分
Pythonの数値解析ライブラリSciPyのinterpolate.interp2dクラスを使って、2次元形状のデータを補間する方法を解説する。補間オプションや、実際の補間例も示す。
PythonでJSONを扱う
·1099 文字·3 分
Pythonの標準ライブラリjsonを使って、JSONデータを辞書型データとして扱う方法を解説する。
SphinxでPython docstringからドキュメントを自動生成する
·2014 文字·5 分
ドキュメント生成ツールSphinxを使って、Pythonスクリプトのクラスや関数のdocstringからHTMLドキュメントを自動生成する方法を解説する。
PythonのsubprocessでWindowsコマンドを実行
·960 文字·2 分
Pythonの標準ライブラリsubprocessを使ってWindowsのコマンドを実行する方法を解説する。
Scikit-learnのPolynomialFeaturesでべき乗を求める
·1917 文字·4 分
PolynomialFeaturesクラスの引数とメソッドについて解説する。また、特徴量の数を1~3まで変化させ、オプションによって出力がどのように変化するか確認する。