ロゴ メインコンテンツへ
RSSフィード
「ソフトウェア開発」に関連する記事一覧

C 画像処理系のコードをチューニングして高速化

2011/11/02
(この記事の文字数: 2952)
OpenMPで並列化によりずいぶんと処理の重い画像処理系のC言語で書いたプログラムの速度を高めることができたのですが、逆にループ回数が多くても一回の処理が軽いときはOpenMPを使うと逆に遅くなってしまうような場合もありました。
 
OpenMPを使っていてなんとなく分かってきたのが、OpenMPによる並列化は出来るだけループの外側に持ってくるのが良くて、さらに高速化をするためには内側にある細かいループを出来る限り自分でチューニングする必要があるということです。
 
ということで、今使っている処理速度をほとんど考えていないコードに簡単なチューニングをしてみたところ結構速くなったので、どんなチューニングを行ったのかを記しておこうと思います。
 
まずはコードのクリティカルな部分を探さなければならないのですが、画像処理でよく出てくる処理は何かと探すと、そのひとつに画像データのコピーがありました。なので、画像のコピーを例にチューニング例を書きたいと思います。
 
具体的にチューニングする処理内容なのですが、画像を読み込むときにpng画像をunsigned charバイト列で読み込み、それを少数演算用にfloatのデータ配列にコピーするという処理が結構あったので、そのコピーの部分に着目します。
 
まず元となる何のチューニングもないコード例は次のようなコードとします。dst_dataがコピー先float型1次元配列、src_dataがコピー元unsigned char型1次元配列で、widthが横解像度、heightが縦解像度です。

 
int x, y, i, j;
int dst_index, src_index;
int num_channels_src = 4; //コピー元のチャンネル数
int num_channels_dst = 3; //コピー先のチャンネル数
for(y=0; y<height; y++){ //heightは縦画素数
    for(x=0; x<width; y++){ //widthは横画素数
        i = y*width + x;
        src_index = i * num_channels_src;
        dst_index = i * num_channels_dst;
        for(j=0; j<num_channels; j++){
            dst_data[dst_index+j] = (float)src_data[src_index+j];
        }
    }
}


画像処理では縦横の解像度の他にRGBのようなチャンネル数を考えなければならないので、処理速度に関して何も考えていない場合は3次元的なループを書く人もいると思います。ちなみにコード中のnum_channels_srcはコピー元データのチャンネル数で、コピー元unsigned char型配列のデータsrc_dataはアルファチャンネルを含んだRGBA構造なので4チャンネル、そしてnum_channels_dstはコピー先のチャンネル数で、コピー先のfloat型配列dst_dataはアルファチャンネルを省いたRGBの3チャンネルのみを格納するという場合を考えています。
 
ループ回数が多いほど少しの無駄の省きで速度が上がるので、結果がわかりやすいように10000x10000の画像を実験に使いました。30回処理速度を計測して平均を取りました。チューニング前の平均速度は下記のようになりました。
 
30回の平均速度: 3.454667 sec
 
ということでまず最初のチューニングを行います。まずはかなり初歩的なところですが、画像の縦横の2重ループを1重ループに直します。
 
int i, j;
int dst_index, src_index;
int num_channels_src = 4;
int num_channels_dst = 3;
int size = width*height;
for(i=0; i<size; i++){ //sizeは画素数
    src_index = i * num_channels_src;
    dst_index = i * num_channels_dst;
    for(j=0; j<num_channels; j++){
        dst_data[dst_index+j] = (float)src_data[src_index+j];
    }
}

30回の平均速度: 3.269134 sec
 
若干処理速度が改善されました。
さらに、このコードではsrc_index, dst_indexの演算も無駄です。続いてそれも省きます。
 
int i, j;
int num_channels_src = 4;
int num_channels_dst = 3;
int nc_diff = num_channels_src - num_channels_dst;
int size = width*height;
float *dst_ptr = dst_data;
uchar *src_ptr = src_data;
for(i=0; i<size; i++){
    for(j=0; j<num_channels_dst; j++){
        *dst_ptr = (float)(*src_ptr);
        dst_ptr++;
        src_ptr++;
    }
    src_ptr+=nc_diff;
}


30回の平均速度: 2.635134 sec
 
これだけでずいぶん処理速度が向上しました。次にポインタをカウンタ代わりに使えばカウンタiが不要になるので、カウンタiを省きます。
 
int num_channels_src = 4;
int num_channels_dst = 3;
int nc_diff = num_channels_src - num_channels_dst;
int size = width*height;
float *dst_ptr = dst_data;
uchar *src_ptr = src_data;
float *end_ptr = dst_data + size*num_channels_dst;
int cnt=0;
for(; dst_ptr!=end_ptr; dst_ptr++, src_ptr++){
    *dst_ptr = (float)(*src_ptr);
    cnt++;
    if(cnt==o_num_channels){
        src_ptr+=nc_diff;
        cnt=0;
    }
}

 
30回の平均速度: 2.558067 sec
 
これだけでも目に見えて処理時間が縮まりました。私の知識ではチューニングはここまでが限界ですので、画像データコピーのチューニングは以上になります。
最終的なコードはずいぶんみづらくなってしまいましたが、ちゃんとわかりやすいコメントを入れておけば問題はないと思います。
今回は10000x10000の画像で0.9秒高速になりましたが、これをパーセンテージで言えば処理時間は74パーセントほどに縮まったことになります。これはかなり大きいことだと思います。
 
しかし、何でもかんでもチューニングすれば良いという話ではなく、コードの可読性と効率の良さのバランスを考えることがとても大切だといろいろなプログラミングの参考書に書いてあったのを覚えています。この両者のバランスというのは多くの人と一緒に開発するような経験がないと判断できないものだと思うので、私にはまだよくわかりませんが、いつかは最適なコードが書けるようになりたいです。

  このエントリーをはてなブックマークに追加  

<<「ソフトウェア開発」の記事一覧に戻る

<<「ソフトウェア開発」の次の記事
「ソフトウェア開発」の前の記事 >>

コメント(0 件)



コンテンツロード: 0.0081 sec
Copyright(C)2006-2024 puarts All Rights Reserved