«前の日記(2013-09-30 (Mon)) 最新 次の日記(2013-10-09 (Wed))»

ありし日の気分(改)

2002|05|06|07|08|09|10|11|12|
2003|01|02|03|04|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|09|10|11|12|
2011|01|02|03|04|05|06|07|10|11|12|
2012|02|03|04|07|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|12|
2015|05|06|
2018|02|03|08|09|12|
2019|10|
2013年
10月
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
昨日のカウンタ:
今日のカウンタ:

[RDF]

最近のトラックバック

2013-10-08 (Tue)

モノクロ演算 (iOSデバイス編) (-Os)

せっかくAndroidで画像のモノクロ化の速度比較をしたのだから、ついでのiOSでもやっておこうか。それぐらいの気持で速度を測り始めたのですが、小手先の技は通用するかな、最適化意識したらどうなるのかな、という方向で調べてたら、ちょっと時間がかかってしまいました。

iOS側では画像とそれをバイト列としてラスターデータの間の変換処理は時間に含めていません。インスタンス変数のsourceDataにNSDataとして画像のバイト列をあらかじめ格納しておきます。そしてsourceDataからNSData* targetDataにモノクロ化後のデータを入れるまでの処理を計測の対象とします。

説明の前に先に計測した時間を示します。Androidの時と同じく1024×768の画像を100回モノクロ化しています。NDKの時と全く同じアルゴリズムのものが表の一番上の「uchar」の行です。最適化オプションは配布ビルド時のデフォルトである-Osとしています。*1

機種名 iPhone 5s (-Os) iPhone 5 (-Os) iPhone 4 (-Os)
uchar 307 621 2529
uint32 341 657 2422
uint64 231 767 2539
uint16x8 165 1191 3347
float 447 1151 6159

今回はどういう風に書いたら最適化が効きやすいのかな、CPUアーキテクチャを意識してコード書いたら最適化はどうなるのかなといった視点でいくつかパターンを用意してみました。uchar, uint32, uint64, uint16×8は全て整数演算で、floatだけは参考情報として浮動小数点演算で計算させてみました。

整数演算の中ではucharが一番無難なコード、uint32がARMv7までの32ビットで速くならないかなと意識したコード、uint64が64ビットCPUで速くならないかなと意識したコード、uint16x8が16ビット×8個(128ビット)のNEONの計算を素に書いたコードとなります。各々を簡単に説明してみましょう。

  • uchar:
    無難なモノクロ化コードです。NDKのときとほとんど同じです。sourceDataには1024×768のARGB888のデータがそのまま入っており、その各ピクセルへの変換をtargetDataに格納するというのを100回繰り返します。
    
    static const unsigned int LoopCount = 100;
    
    static const unsigned int r_lum_i = 76;
    
    static const unsigned int g_lum_i = 150;
    
    static const unsigned int b_lum_i = 30;
    
    // (中略)
    
    - (void)mono_uchar {
    
      unsigned char const *inp = (unsigned char*)sourceData.bytes;
    
      unsigned char *outp = (unsigned char*)targetData.mutableBytes;
    
      int length4 = bitmapHeight * bitmapWidth * 4;
    
      int m;
    
      int r, g, b;
    
      for (int j = 0; j < LoopCount; j ++)
    
        for (int i = 0; i < length4; i += 4) {
    
          r = inp[i + 0];
    
          g = inp[i + 1];
    
          b = inp[i + 2];
    
          m = (r * r_lum_i + g * g_lum_i + b * b_lum_i) >> 8;
    
          outp[i + 3] = 0xff;
    
          outp[i + 0] = outp[i + 1] = outp[i + 2] = m ;
    
        }
    
    }
    
    
  • uint32:
    コンパイラによって最適化しない(-O0)場合は、ucharの方式だとメモリアクセスの無駄があまりにも多いので、その回数を少しでも減らそうという意図で書いてみたのが以下のコードです。モノクロ計算する部分の工夫は特にありません。最初の変数初期化の部分は省略します。
    
      int length = bitmapHeight * bitmapWidth;
    
      uint32_t s;
    
      uint m;
    
      for (int j = 0; j < LoopCount; j ++)
    
        for (int i = 0; i < length; i ++) {
    
          s = inp[i];
    
          m = (((s >> 16) & 0xff) * b_lum_i +
    
               ((s >>  8) & 0xff) * g_lum_i +
    
               ((s >>  0) & 0xff) * r_lum_i) >> 8;
    
          outp[i] = 0xff000000 | m | (m << 8) | (m << 16);
    
        }
    
    
    上の最適化-Osの場合では、そんなに速くなってないですね。iPhone4では一番速いとなってますが、誤差の範囲ですし。
  • uint64:
    iPhone5sでせっかく64ビットARMを搭載したのでその実力を見てみたいと、64ビット(2ピクセル)単位でメモリアクセスするようにしてみました。また、2ピクセル分のRGB各要素ごとにまとめて計算しています。
    
      const int length_2 = bitmapWidth * bitmapHeight >> 1;
    
      uint64_t s, m;
    
      for (uint j = 0; j < LoopCount; j ++)
    
        for (uint i = 0; i < length_2; i ++) {
    
          s = inp[i];
    
          m = ((((s >> 16) & 0xff000000ff) * b_lum_i) +
    
               (((s >>  8) & 0xff000000ff) * g_lum_i) +
    
               (((s >>  0) & 0xff000000ff) * r_lum_i)) & 0xff000000ff00;
    
          outp[i] = 0xff000000ff000000 | (m << 8) | m | (m >> 8);
    
        }
    
    
    iPhone5やiPhone4ではCPUとして64ビットレジスタがありませんし、遅くなるのは仕方ありません。(むしろ思ったほど遅くなってませんでした。) iPhone5sではucharやuint32より明確に速くなってるのでちょっと嬉しいですね。
  • uint16x8:
    せっかく128ビットのNEONレジスタと演算命令を持ってるんだからと思って、その辺りをもうちょっとガリガリ書いてあげようという感じです。64ビット(2ピクセル)分をベクタレジスタ(8ビットのを8個)に読み込んで(vld1_u8)、その8個の数値を各々16ビットに拡張して(vmovl_u8)、8個の数値を乗算し(vmulq_u16)、256で割り(vshrq_n_u16)しています。その結果を64ビットレジスタに戻してからの処理はuint64のときと大体同じです。なんというか、そのままNEONのアセンブリ言語書いてるのに近いイメージなのかも。
    
      const uint length_2 = bitmapWidth * bitmapHeight >> 1;
    
      const uint16x8_t lum_i =
    
        vmovl_u8(vcreate_u8((0x0001000000010000 * b_lum_i) |
    
                            (0x0000010000000100 * g_lum_i) |
    
                            (0x0000000100000001 * r_lum_i)));
    
      
    
      uint16x8_t sv, mv;
    
      uint64_t m;
    
      for (uint j = 0; j < LoopCount; j ++)
    
        for (uint i = 0; i < length_2; i ++) {
    
          sv = vmovl_u8(vld1_u8(inp + i * 8));
    
          mv = vmulq_u16(sv, lum_i);
    
          mv = vshrq_n_u16(mv, 8);
    
          m = (uint64_t)vmovn_u16(mv);
    
          m = ((m >> 16) + (m >> 8) + m) & 0xff000000ff;
    
          outp[i] = 0xff000000ff000000 | (m * 0x010101);
    
        }
    
    
    で、結果はというと、iPhone5sでの-Osでは最速です。うーん。こんな小手先の技が通用しちゃうんですね。ちょっと残念。
  • float:
    参考までにと浮動小数点でのモノクロ演算です。ただ、これはNDKのもの比較してはいけません。なぜなら、こちらのバージョンではエンディアンを意識することでRGB要素をまとめて読み書きするように変更しているためです。*2
    
      uint32_t s;
    
      float r, g, b;
    
      uint m;
    
      for (int j = 0; j < LoopCount; j ++)
    
        for (int i = 0; i < length; i ++) {
    
          s = inp[i];
    
          r = (float)(s & 0x0000ff);
    
          g = (float)(s & 0x00ff00);
    
          b = (float)(s & 0xff0000);
    
          m = (uint)(r * (r_lum_f / 0x0000ff * 255) +
    
                     g * (g_lum_f / 0x00ff00 * 255) +
    
                     b * (b_lum_f / 0xff0000 * 255));
    
          outp[i] = 0xff000000L | (m << 16) | (m << 8) | m;
    
        }
    
    
    計算結果を見るとどの機種でも、整数演算(uchar, uint32, uint64, uint16x8)のどれよりも遅くなっていますが、仕方ないところでしょうか。

*1 ちゃんと計測してませんが、いくつかの数値を見るとデバッグビルド時のデフォルトである-O0ではここから各々5倍は遅くなりそうです。

*2 Android NDKでもこういう工夫はできるのですが、CPUによるエンディアンの差異があるため、特定のCPUでしか動作しないようにするか、エンディアンによって処理を分ける必要があります。



C++でつくるUnreal Engineアプリ開発 for Windows & macOS  UE4でC++を使う方法を書いた本です。

«前の日記(2013-09-30 (Mon)) 最新 次の日記(2013-10-09 (Wed))»


2002|05|06|07|08|09|10|11|12|
2003|01|02|03|04|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|08|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|12|
2007|01|02|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|09|10|11|12|
2011|01|02|03|04|05|06|07|10|11|12|
2012|02|03|04|07|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|12|
2015|05|06|
2018|02|03|08|09|12|
2019|10|