| 
 | 昨日のカウンタ: 今日のカウンタ: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
せっかく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の計算を素に書いたコードとなります。各々を簡単に説明してみましょう。
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 ;
    }
}
  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では一番速いとなってますが、誤差の範囲ですし。
  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より明確に速くなってるのでちょっと嬉しいですね。
  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では最速です。うーん。こんな小手先の技が通用しちゃうんですね。ちょっと残念。
  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)のどれよりも遅くなっていますが、仕方ないところでしょうか。