
Pythonである程度重い処理をしなければいけない時に、コードの効率を改善することがあります。改善は定量的に評価してこそ有効な効果を出す案に辿り着くことができます。ここではPythonのtimeやline_profilerを使ってプログラムの処理時間を計測する方法を紹介します。
こんにちは。wat(@watlablog)です。
Pythonプログラミングの基本!経過時間を計測する方法を説明します!
プログラム実行速度タイムアタックで効率改善しよう!
処理時間を計測するとアルゴリズムの比較ができる
「プログラムは動けば良い」と思っている人は少なからずいらっしゃると思いますが、その考えは最後の最後で自分の首をしめることになりかねません。
例えばcsvに保存された時刻歴波形を解析するプログラムを作って誰かに配布したとします。そのプログラムを作っている段階では1sや2sのデータで計算を検証していたとしても、ユーザによっては1分、10分のデータを解析したいというケースも出てくるでしょう。
1sのデータの解析に1sかかっていたとしたら、10分のデータは600sかかってしまいます。計算時間問題が技術的にハードルが高いものであればある程度仕方が無い部分もありますが、プログラマの怠慢で遅くなっているとしたら非常にもったいないですよね。
ユーザの数だけ使い方があるように、一人の開発者が全ケースを予め想定しておくことはできません。そのため、プログラムは出来る限りシンプルに、高速に動くようにしておく必要があります。
ある関数で、forループで書ける所をwhileループで書いていたとしたら?
どちらのアルゴリズムが効率が良いのかを正しく知るためには計測による定量評価が必要です。
Pythonでプログラム処理時間を計測するコード
ここではプログラムの処理時間を計測するコードを使って、配列の演算(X * Y)をforループ、whileループ、numpyの配列演算の3つのパターンで比較する例を示します。
配列の演算で処理時間を比較してみる
forループの場合
まずはループ計算の代表格であるforループから検証用コードを紹介します。
時間計測用のコードは、import timeでtimeパッケージ(Pythonインストール時から入っている)をインポートし、time.time()で現在時間を取得する部分です。
t0として主要のコードが始まる前の時刻を取得し、t1としてコードの最後に再度時刻取得を行います。その後、t1 - t0をすることで簡単に処理時間を算出することができます。
forループの処理ですが、ちょっとPythonをやったことがある人はこんなコードは書かないと思います。今回は時間計測のためのデモとしてあえて書いているので、その辺ご了承下さい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import numpy as np import time t0 = time.time() # 計測開始時間を取得 # ここから処理 a = 0.001 x = np.arange(0, 100, a) y = np.arange(0, 100, a) # forループを使ってxとyを乗算 z = [] for i in range(len(x)): z.append(x[i] * y[i]) # ここまで処理 t1 = time.time() # 計測終了時間 elapsed_time = float(t1 - t0) # 経過時間 print(elapsed_time) # 経過時間を表示 |
whileループ
次はwhileループで同じ結果を得る処理を書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import numpy as np import time t0 = time.time() # 計測開始時間を取得 # ここから処理 a = 0.001 x = np.arange(0, 100, a) y = np.arange(0, 100, a) # whileループを使ってxとyを乗算 z = [] i = 0 while i < len(x): z.append(x[i] * y[i]) i = i + 1 # ここまで処理 t1 = time.time() # 計測終了時間 elapsed_time = float(t1 - t0) # 経過時間 print(elapsed_time) # 経過時間を表示 |
numpy配列演算
最後にnumpyに備わっている配列演算の場合です。おそらく多くの人はこの方法で書くでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import numpy as np import time t0 = time.time() # 計測開始時間を取得 # ここから処理 a = 0.001 x = np.arange(0, 100, a) y = np.arange(0, 100, a) # numpyの行列演算を使ってxとyを乗算 z = x * y # ここまで処理 t1 = time.time() # 計測終了時間 elapsed_time = t1 - t0 # 経過時間 print(elapsed_time) # 経過時間を表示 |
各アルゴリズムの平均処理時間(n=100)
以下は上の3つのアルゴリズムパターンの処理時間計測比較結果です。
PCといっても演算ばらつきはあるので、試行回数100回の結果を平均しています。
これを見るとforループよりwhileループの方が36%ほど悪く、forループよりもnumpyの方が96%ほど良い結果を示していることがわかります。
numpyの配列演算で計算を行えるプログラムを、forループやwhileループで書くことは相当効率の悪いことだとわかりますね。

time.perf_counter()でもっと正確に計測する
time.pref_counter()を使う事で、短時間をさらに正確に計測する事が可能です。
1[s]以下の処理を計測する場合はこちらの方が良いかも知れません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import numpy as np import time t0 = time.perf_counter() # 計測開始時間を取得 # ここから処理 a = 0.001 x = np.arange(0, 100, a) y = np.arange(0, 100, a) # forループを使ってxとyを乗算 z = [] for i in range(len(x)): z.append(x[i] * y[i]) # ここまで処理 t1 = time.perf_counter() # 計測終了時間 elapsed_time = float(t1 - t0) # 経過時間 print(elapsed_time) # 経過時間を表示 |
line_profilerで行毎に時間を計測する
ざっくりと時間を計測したい時は上記の方法で十分だと思いますが、より詳細にコードを改善しようとする場合は行単位で処理時間を計測しボトルネックを特定する必要があります。
line_profilerというライブラリを使うと、簡単にライン(行)毎の処理時間を計測することが可能です。インストールは「line_profiler」を各自の環境でpipインストール可能です。
コード例は以下。計測したい処理を関数として用意し(クラスでも可)、.add.functionに登録します(クラスはadd_module)。
次に.runcall(関数やクラス, 関数の引数)で計測します。ここで関数の引数が複数ある場合は.runcall(func, a, b)のように増やすことで動きます。
最後に.print_stats()で計測データを表示させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import numpy as np from line_profiler import LineProfiler # 計測する関数 def func(a): x = np.arange(0, 100, a) y = np.arange(0, 100, a) # forループを使ってxとyを乗算 z = [] for i in range(len(x)): z.append(x[i] * y[i]) # 引数 a = 0.001 # 関数の処理時間を計測 prof = LineProfiler() prof.add_function(func) prof.runcall(func, a) prof.print_stats() |
結果例は以下です。Timer unitに記載の単位で処理時間がTime欄に記載されています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Timer unit: 1e-06 s Total time: 0.100757 s File: /Users/wat/PycharmProjects/vibration-opt/time-profiler.py Function: func at line 5 Line # Hits Time Per Hit % Time Line Contents ============================================================== 5 def func(a): 6 1 658.0 658.0 0.7 x = np.arange(0, 100, a) 7 1 352.0 352.0 0.3 y = np.arange(0, 100, a) 8 9 # forループを使ってxとyを乗算 10 1 0.0 0.0 0.0 z = [] 11 100001 31281.0 0.3 31.0 for i in range(len(x)): 12 100000 68466.0 0.7 68.0 z.append(x[i] * y[i]) |
行番号、実行回数、コード内容も見れてわかりやすいですね。
これでボトルネックとなっている行を特定し、改善を検討することができそうです。
まとめ
メインはプログラムの処理時間計測の方法ですが、各アルゴリズムで差がこんなにも明確に出たことが驚きですね。
処理時間計測自体はPythonに標準で備わっている方法で簡単に行うことができます。
さらに、外部ライブラリを使うことで高度な分析も可能です。
是非、自分の書いたプログラムがもっとはやくなるかどうかという視点もコーディング時に持つきっかけにして頂けたら幸いです。
尚、ここで紹介しているのはCPU時間を計測する方法です。Google ColabでGPUやTPUを使って処理している時間を計測する事には向きませんのでご注意ください。
今回の方法で様々な処理を計測することが可能になりました!
Twitterでも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!