PyGTK2からPyGI GTK3への移行(5)

今回は「最適化の努力をするよりも正しい方法を探す方が良い」という良く知られたお話についての私の経験です.

現状PyGIではGdkPixbufから各ピクセルのデータを読んだり操作する方法がありません. そこでninix-aya(gtk3)ではGdkPixbufからcairo.ImageSurfaceに画像を書き込んで, その上でデータを操作しています.

ではpnrの処理「座標(0, 0)のピクセルと同じ色のピクセルを全て透明にする」を何通りか実装して実行時間を測定してみましょう. 非常に遅い実装から始めて改良していきます.

まずは画像を読み込んでcairo.ImageSurfaceを作る共通部分です.

from gi.repository import Gdk
from gi.repository import GdkPixbuf
import cairo

path = 'test.png'
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)

def create_surface():
    surface = cairo.ImageSurface(0, pixbuf.get_width(),
    pixbuf.get_height())

    cr = cairo.Context(surface)
    Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
    cr.paint()
    del cr
    return surface

実際にpnrの処理を実行する部分は後に載せることにして, 先に最後の部分です. 複数回関数を実行して処理にかかった時間を測定します.(以下は一部のみの抜粋です.)

if __name__ == '__main__':
    N = 10
    for x in range(N):
        surface = create_surface()
        surface = process_pnr_0(surface)
(以下同様)

プロファイリングは

$ script
$ python -m cProfile pnr_test.py
$ exit (もしくはCtrl+Dを押してscritpを終了する.)

の様に実行します.
ある画像(具体的にはゴースト「安子さん」のギコのサーフェスの1枚)についての結果を先に出しておきます.

      ncalls  tottime  percall  cumtime  percall
(0)       10    0.474    0.047    0.674    0.067
(I)       10    0.142    0.014    0.144    0.014
(II)      10    0.107    0.011    0.109    0.011
(III)     10    0.072    0.007    0.075    0.008

段々と速くなっているのが分かると思います.
それぞれの実装を見ていきましょう.

(0) 何の工夫もなく実装してみました.
def process_pnr_0(surface):
    buf = surface.get_data()
    width = surface.get_width()
    height = surface.get_height()
    stride = surface.get_stride()
    size = stride / width
    rgba = buf[0:4]
    for i in range(width):
        for j in range(height):
            index = i * size + j * stride
            if buf[index:index+4] == rgba:
                buf[index + 0] = chr(0)
                buf[index + 1] = chr(0)
                buf[index + 2] = chr(0)
                buf[index + 3] = chr(0)
   
return surface

(I) (0)との違いはbufへの書き込みを4byte一度に行なっている点です. これだけで相当差が出ます.
def process_pnr_1(surface):
    buf = surface.get_data()
    width = surface.get_width()
    height = surface.get_height()
    stride = surface.get_stride()
    size = stride / width
    rgba = buf[0:4]
    clear = chr(0) * 4
    for i in range(width):
        for j in range(height):
            index = i * size + j * stride
            if buf[index:index+4] == rgba:
                buf[index:index+4] = clear
    return surface

(II) bufに格納されたデータは1次元ですので2重のループをやめます.
def process_pnr_2(surface):
    buf = surface.get_data()
    rgba = buf[0:4]
    clear = chr(0) * 4
    for index in range(0, len(buf), 4):
        if buf[index:index+4] == rgba:
            buf[index:index+4] = clear
    return surface

(III) (I)の結果から推測して, データの書き込みを出来るだけ大きな単位で行なうようにします.
def process_pnr_3(surface):
    buf = surface.get_data()
    rgba = buf[0:4]
    clear = chr(0) * 4
    pos = 0
    end = len(buf)
    for index in range(0, end, 4):
        if buf[index:index + 4] != rgba:
            if pos < index:
                buf[pos:index] = clear * ((index - pos) // 4)
            pos = index + 4
    else:
        if pos != end:
            buf[pos:end] = clear * ((end - pos) // 4)
    return surface

以上, 少しでも速くという無駄な努力をお見せしました.

今回私が作ったコードの中で最速だったのは次です.

    ncalls  tottime  percall  cumtime  percall
(IV)    10    0.000    0.000    0.007    0.001

(IV) 大切なことは正しい方法(この場合は文字列型のreplaceメソッドが十分高速な処理であること)を見付けることです.(このコードには明らかなバグがありますが, ほとんどのゴーストで動作します. これはベストケースに対してのみの実装と考えて下さい. この点については次に書く予定です.)
def process_pnr_4(surface):
    buf = surface.get_data()
    buf[:] = buf[:].replace(buf[0:4], chr(0) * 4)
    return surface

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です