C++テンプレートメタプログラミングの検証

処理速度について

テンプレートメタプログラミングの非常に魅力的なところは、処理速度が速いという事にあります。テンプレートを使うだけでも処理は速くなりますが、実行時に行う処理をビルド時に行うため、さらに処理速度は速くなります。
ここでは、テンプレートも含めて、従来の手法とテンプレートを駆使した方法で、どれくらいの処理速度の差がでるか検証してみたいと思います。

Imageライブラリにおいてのパフォーマンス向上策(その1)

RGB24bitや、8bitグレイスケールなど、様々な画像形式のイメージバッファを扱うクラスを考えてみます。アクセスするAPIは、以下の2つだけ考慮します。

ColorRGBA getPixel(int x, int y);
void setPixel(int x, int y, ColorRGBA col);

ColorRGBAは、以下のような実装になっています。

struct ColorRGBA {
  union {
    uint32_t rgba;
    struct {
      uint32_t r:8;
      uint32_t g:8;
      uint32_t b:8;
      uint32_t a:8;
    };
  };
};

つまり、ColorRGBAは、32bitの整数を8bit毎のR,G,B,Aでアクセス可能にしたものです。 RGB24bitの場合は、R,G,Bのみを、グレイスケールの場合はRのみを使用することにします。
さて、何も考えずにとりあえずクラスを作ってみましょう。

struct ImageBuffer {
  virtual ColorRGBA getPixel(int x, int y) const = 0;
  virtual void setPixel(int x, int y, ColorRGBA col) = 0;
  int width_;
  int height_;
};
struct ImageBufferRGBA : ImageBuffer {
  ColorRGBA getPixel(int x, int y) const {
    return buffer32[x + y * width_]; 
  }
  void setPixel(int x, int y, ColorRGBA col) {
    buffer32[x + y * width_] = col.rgba; 
  }
  uint32_t* buffer32;
};
struct ImageBufferRGB : ImageBuffer {
  ColorRGBA getPixel(int x, int y) const {
    ColorRGBA col;
    col.r = buffer8[(x + y * width_)*3]; 
    col.g = buffer8[(x + y * width_)*3+1]; 
    col.b = buffer8[(x + y * width_)*3+2];
    return col; 
  }
  void setPixel(int x, int y, ColorRGBA col) { 
    buffer8[(x + y * width_)*3] = col.r; 
    buffer8[(x + y * width_)*3+1] = col.g; 
    buffer8[(x + y * width_)*3+2] = col.b; 
  }
  uint8_t* buffer8;
};

ご覧のとおり、R,G,B,Aの32bitを扱うイメージは、uint32_t型のバッファを使って効率よくアクセスできますが、R,G,Bの24bitの場合は、uint8_t型のバッファを使っています。
さて、ここで任意のImageBufferから任意のImageBufferへ、ピクセルをコピーするファンクションを作ってみましょう。ImageBufferは各イメージの既定クラスですから、

void copy(ImageBuffer* dest, const ImageBuffer* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}

こんな感じでしょうか。(バッファのメモリ確保は省略しています) ここまで、テンプレートを全く使わずにうまく実装できました。
さて、ここで素朴が疑問がひとつ。ImageBufferのsetPixel/getPixelは、virtualファンクションです。もちろん、ちゃんと動作しますが、setPixelやgetPixelを行う毎に、vtableという仮想ファンクションテーブルを参照して間接的に関数コールが起こります。
関数コールのアドレスをルックアップする手順が増えるわけですが、昨今の高速なCPUならそれほどコスト増になりません。それよりも問題なのは、派生したクラスを直接呼べば、setPixel/getPixelはインライン展開されて、関数コールすら発生しません。setPixelやgetPixelは、単純なメモリアクセスですから、インライン展開されると効率の良いコードが実行されることが予想できます。
テンプレートを使わないでも解決策はあります。C++は引数の型でオーバーライドできますから、

void copy(ImageBufferRGBA* dest, const ImageBufferRGBA* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}
void copy(ImageBufferRGB* dest, const ImageBufferRGB* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}
void copy(ImageBufferRGBA* dest, const ImageBufferRGB* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}
void copy(ImageBufferRGB* dest, const ImageBufferRGBA* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}

これでOK。コピペすれば簡単ですし、#defineでマクロにするのもよいでしょう。 しかし、これには問題があります。そう、ImageBufferXXXXXのタイプを拡張した場合、関数のオーバーライドがどんどん増えてしまいます。いくら#defineでマクロにしても、みっともないソースコードになってしまいますね。そこで、テンプレートの登場です。

template <class DEST, class SRC>
void copy(DEST* dest, const SRC* src) {
  for (int y= 0; y < src->height_; ++y) {
    for (int x = 0; x < src->width_; ++x) {
      dest->setPixel(x, y, src->getPixel(x,y));
    }
  }
}

これならコピペも#define不要で、virtualコールも起こらずに高速です。実際に、どれぐらい高速になるのでしょう? 4000x3000ピクセル程度のループで試したところ、基本クラスを使ってvirtualコールが発生する場合と、テンプレートを使用した場合では、30%~60%程度の差がありました。もちろん、テンプレートを使ったほうが遥かに高速です。

つづく


[ 戻る ]

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2010-02-02 (火) 07:25:01 (3005d)