コードの性能評価と改善方法¶
目的¶
画像処理では、単位時間あたり膨大な量の計算が必要となるので,正しい答えを得るだけではなく、高速に計算する必要がある.そこでこの章では
- 自分が書いたコードの性能を計測する方法を学ぶ
- 自分が書いたコードを改善する秘訣を学ぶ
- 次の関数の使い方を学ぶ : cv2.getTickCount, cv2.getTickFrequency
処理時間の計測のため関数・モジュールとして、OpenCVが用意しているもの以外にPython自体には time モジュールがある。また, profile モジュールを使えば,コード内のそれぞれの関数が何回実行されたか,何回呼び出されたかの詳細なレポートを取得できる.さらに,IPythonを使えば,もっと易しい形でこれらの機能を使うことができる.ここでは、これらの内で重要なものを紹介する.詳しくは 補足資料 に載っている資料を参照すること.
OpenCVの機能を使って性能を計測¶
cv2.getTickCount 関数は(コンピュータがON状態になったというような)「参照イベント」後からこの関数が呼ばれるまでの経過時間をクロック数で返す.つまり,ある関数を実行する前と後でこの関数を呼び、差を取れば,その関数の処理時間をクロック数で知ることができる.
cv2.getTickFrequency 関数は、1秒あたりのクロック数(クロック周波数)を返す関数である.したがって、ある関数の処理時間を秒単位で知るには,以下のようにする:
e1 = cv2.getTickCount()
# ここに処理時間を測りたい関数呼び出しを書く
e2 = cv2.getTickCount()
time = (e2 - e1)/ cv2.getTickFrequency()
次の例ではカーネルサイズを5から49と変化させながら中央値フィルタを適用した時にかかる処理速度を表示する. (結果がどのようなものかはここでは重要ではないので、気にしないこと):
img1 = cv2.imread('messi5.jpg')
e1 = cv2.getTickCount()
for i in xrange(5,49,2):
img1 = cv2.medianBlur(img1,i)
e2 = cv2.getTickCount()
t = (e2 - e1)/cv2.getTickFrequency()
print t
# Result I got is 0.521107655 seconds
Note
Pythonのtime
モジュールを使って同じことをするには cv2.getTickCount
の代わりに time.time()
関数を使う.
OpenCVのデフォルトでの最適化¶
多くのOpenCVの関数はSSE2やAVX(コンピュータの並列化の仕組み、ベクトル演算の高速化が可能)による最適化を取り入れているが,中には最適化されていない関数もある.もしもシステムで最適化機能をサポートしているのであれば、それを利用しない手はない(今のプロセッサであれば、その大半はサポートしているはず)。最適化機能はデフォルトではコンパイル時に有効化される.そして有効化されていればOpenCVは最適化されたコードを実行し,そうでなければ最適化されていないコードを実行することになる.最適化が有効になっているか無効なのかは cv2.useOptimized() 関数を使って調べられ、cv2.setUseOptimized() 関数を使えば有効・無効を切り替えられる.そこで,以下のコードを見てみよう.
# check if optimization is enabled
In [5]: cv2.useOptimized()
Out[5]: True
In [6]: %timeit res = cv2.medianBlur(img,49)
10 loops, best of 3: 34.9 ms per loop
# Disable it
In [7]: cv2.setUseOptimized(False)
In [8]: cv2.useOptimized()
Out[8]: False
In [9]: %timeit res = cv2.medianBlur(img,49)
10 loops, best of 3: 64.1 ms per loop
最適化が有効になっていると,中央値フィルタの処理速度は無効時に比べおよそ2倍ほど速くなる.関数の実装を見れば中央値フィルタがSIMD最適化されていることが分かる.この例が示すように,ライブラリの中身を変更すること無く、コードの先頭からコード最適化を有効にできるす(繰り返しになるが,デフォルトではコードの最適化が有効になっている).
IPythonの機能を使った性能の計測¶
プログラミングをしていると,二つの似たような処理をするコードの性能を比較することがある.IPythonはこの目的のため %timeit
というマジック・コマンドを用意している.このコマンドでは,処理速度計測を高精度に行うため,コードを数回実行する.ただし,この機能は主に1行の命令を計測する作業に向いている.
例えば,以下に示す加算処理の内,どの処理が良い処理だと考えるだろうか. x = 5; y = x**2
, x = 5; y = x*x
, x = np.uint8([5]); y = x*x
or y = np.square(x)
? IPython上で %timeit
コマンドを使えば比較が容易にできます.
In [10]: x = 5
In [11]: %timeit y=x**2
10000000 loops, best of 3: 73 ns per loop
In [12]: %timeit y=x*x
10000000 loops, best of 3: 58.3 ns per loop
In [15]: z = np.uint8([5])
In [17]: %timeit y=z*z
1000000 loops, best of 3: 1.25 us per loop
In [19]: %timeit y=np.square(z)
1000000 loops, best of 3: 1.16 us per loop
この結果を見ると, x = 5 ; y = x*x
の処理が最も速く,Numpyに比べて20倍ほど高速であることが分かる.配列の作成も処理速度に含めると100倍速い.すごくないだろうか? (Numpyの開発チームは,この計算に対して処理の高速化を行っている)
Note
Pythonのスカラー計算はNumpyのスカラー計算より高速なので,1個か2個の要素だけを含む処理であればPythonのスカラー計算を使うほうがNumpyの配列を使って計算するより高速に処理ができる.だから配列のサイズが大きくなった時にNumpyを使うと良いだろう.
もう一つ例を示す.この例では同じ画像に対して cv2.countNonZero() 関数と np.count_nonzero() 関数を適用した時の性能を比較する.
In [35]: %timeit z = cv2.countNonZero(img)
100000 loops, best of 3: 15.8 us per loop
In [36]: %timeit z = np.count_nonzero(img)
1000 loops, best of 3: 370 us per loop
結果が示すように,OpenCVの関数の方が25倍ほど高速である.
Note
一般的にOpenCVの関数はNumpyの関数より高速に実行されるため,同一の処理であればOpenCVを使った方が良いだろう.ただし例外がある.コピーではなくデータの値を見る時はNumpyの方が高速に処理ができる.
IPythonの更なるマジックコマンド¶
上で紹介した %timeit
以外にも,性能計測やプロファイリング,メモリ計測などに便利なマジックコマンドがある.これらのマジックコマンドについては公式ドキュメントによくまとめられているので,興味がある人は,ドキュメントに目を通してみよう.
パフォーマンスの最適化技術¶
PythonとNumpyの最大限の性能を引き出すための技術が幾つかある.ここでは関連性のある技術のみ紹介する.ここでの大事な点は,アルゴリズムを実装する時は,まず初めに単純な実装を試してみるということである.処理のボトルネックを見つけたりコードを最適化したりするのは,アルゴリズムが正しく動作することを確認してからにする.
- Pythonを使うのであれば,可能な限りループの使用を避けよう.特に二重/三重ループやそれ以上の多重ループは、処理速度が極端に遅くなってしまうため避けるべきである.
- アルゴリズムやコードはできる限りベクトル化しよう.なぜなら,NumpyとOpenCVはvectorの処理に対して性能を発揮するように最適化されているからである.
- キャッシュ・コヒーレンス(cache coherence)、つまりデータがメモリ/キャッシュ上でどのように配置されるか考えて処理をする.
- 不要な配列のコピーはせずに,データ参照を試せ.配列のコピーは重い処理の一つ.
上記の点を全て意識したとしても、あなたの書いたコードの処理速度が遅い,もしくは大きなループの使用が避けられないのであれば,ループ処理を高速に行うCythonのようなライブラリの使用を検討したほうが良いだろう.
補足資料¶
- Pythonの最適化技術(英語)
- Scipy講義資料(英語) - Advanced Numpy