«前月 最新 翌月»

ありし日の気分(改)

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でしか動作しないようにするか、エンディアンによって処理を分ける必要があります。


2013-10-09 (Wed)

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

先に書いたようにリリースビルドでのデフォルトの最適化オプションは-Osですが、一般的には-Ofastを指定すると更なる最適化を行ってくれます。*1 他の条件は一切変えずに計測したのが以下の結果です。

機種名 iPhone 5s (-Ofast) iPhone 5 (-Ofast) iPhone 4 (-Ofast)
uchar 319 656 2797
uint32 124 320 1335
uint64 249 858 5556
uint16x8 152 1209 3534
float 168 368 1618

ucharで-Osから-Ofastへの差異は少ないのですが、それ以外では最適化の効果が大きく出て高速化されているようです。NEONなどのSIMD処理を使ってループの中の2回〜4回の処理を一度に行っているようです。ではiPhone5sのARMv8の場合にて-Ofastによる最適化がどのように行われているか、ちらっと眺めてみましょう。ループの中の処理だけ抜き出し、邪魔になりそうなコメントは抜いて、あと私の方で最低限のコメントを入れてみました。 コンパイル元のコードについては前の記事を参照して下さい。

  • uchar:
    
    	add	x17, x11, x16  ; x17
    
    	ldrb	w0, [x17]              ; w0 に緑要素(green)を読込
    
    	madd	w0, w0, w13, wzr  ;  w0 := green×150
    
    	ldurb	w1, [x17, #-1]  ; w1に赤要素(red)を読み込み
    
    	madd	w0, w1, w12, w0   ; w0 := red×76 + w0 = red×76 + green×150
    
    	ldrb	w17, [x17, #1]  
    
    	madd	w17, w17, w14, w0 ; w17 := blue×30+w0 = blue×30 + red×76 + green×150
    
    	lsr	w17, w17, #8  ; w17 := w17 >> 8
    
    	add	x0, x10, x16
    
    	add	x16, x16, #4
    
    	strb	w15, [x0, #2]   ; 青要素に書込み
    
    	strb	w17, [x0, #1]   ; 緑要素に書込み
    
    	strb	w17, [x0]   ; 赤要素に書込み
    
    	sturb	w17, [x0, #-1]   ; αチャンネルに書込み
    
    
    びっくりするぐらい最適化してません。C言語ほとんどそのままですね。ちなみにw12, w13, w14, w15に各々即値で76, 150, 30, 0xffが入っています。
  • uint32:
    
    	ldr	q5, [x2], #16    ; 4ピクセル分(128ビット==16バイト)まとめてq5レジスタ(≒v5)に読み込み
    
    	ushr.4s	v4, v5, #16
    
    	and.16b	v4, v4, v0  ; 4バイト毎に16ビット右シフトして 0x000000ff とandしたのをv4
    
    	ushr.4s	v6, v5, #8
    
    	and.16b	v6, v6, v0  ; 4バイト毎に8ビット右シフトして 0x000000ff とandしたのをv6
    
    	mul.4s	v6, v6, v1  ; 4バイトづつ v6 := v6 × 150
    
    	and.16b	v5, v5, v0  ; シフトせず 4ビット毎に 0x000000ff とandしたのをv5に
    
    	mla.4s	v6, v5, v2  ; 4バイトづつ v6 := v6 + v5 × 76
    
    	mla.4s	v6, v4, v3  ; 4バイトづつ v6 := v6 + v4 × 30
    
    	ushr.4s	v4, v6, #8  ; 4バイトづつまとめて右へ8つシフト
    
    	shl.4s	v5, v4, #8
    
    	orr.16b	v5, v4, v5
    
    	shl.4s	v4, v4, #16
    
    	orr.16b	v4, v5, v4  ; 4ピクセル分のモノクロ計算後RGB要素をv4とする
    
    	orr.4s	v4, #255, lsl #24 ; 4ピクセル分のαチャンネルを255とする
    
    	str	q4, [x1], #16   ; 4ピクセル分の計算結果をまとめて格納
    
    	sub	x17, x17, #4
    
    	cbnz	x17, LBB6_5
    
    
    こんな感じで128ビットのNEONレジスタを使って、頼んでもいないのに4ピクセルの計算にまとめていました。しかも最適化後でもシンプル!
  • uint64:
    
    	ldr	q4, [x2], #16  ; 4ピクセル分読み込み
    
    	ushr.2d	v2, v4, #16
    
    	and.16b	v2, v2, v0   ; v2に赤要素(4ピクセル分)
    
    	ushr.2d	v3, v4, #8
    
    	and.16b	v3, v3, v0  ; v3に緑要素(4ピクセル分)
    
    	and.16b	v4, v4, v0  ; v4に青要素(4ピクセル分)
    
    	umov.d	x3, v4[1]
    
    	madd	x3, x3, x15, xzr  ; 青要素の1,2ピクセル目を計算
    
    	fmov	d5, x3
    
    	fmov	x3, d4
    
    	madd	x3, x3, x15, xzr  ; 青要素の3,4ピクセル目を計算
    
    	fmov	d4, x3
    
    	zip1.2d	v4, v4, v5  ; 計算結果を再びv4に格納(4ピクセル分)
    
    	umov.d	x3, v3[1]
    
    	madd	x3, x3, x14, xzr
    
    (長いので以下略)
    
    
    こっちも128ビットのNEONレジスタを使ってくれているのですが、計算が64ビット単位になっているせいで、あまりシンプルになっていません。C言語側で無理して64ビットに2ピクセル分の計算を収めたところが足を引っ張っている印象です。結局2ピクセル単位でしか計算できていませんし…*2
  • uint16×8:
    
    	ldr	d1, [x19, x13]  ; 2ピクセル分(64ビット)まとめて読み込み
    
    	ushll.8h	v1, v1, #0  ; 8個の8ビット数値を8個の16ビット数値に拡張
    
    	mul.8h	v1, v1, v0  ; 各色要素毎に{r,gb}_lum_iを乗算
    
    	ushr.8h	v1, v1, #8  ; 各色要素毎に右に8ビットシフト
    
    	xtn.8b	v1, v1  ; 8個の16ビット数値を8個の8ビット数値に戻す
    
    	fmov	x13, d1
    
    	add	x14, x13, x13, lsr #8
    
    	add	x13, x14, x13, lsr #16
    
    	and	x13, x13, #0xff000000ff  ; モノクロ化後の色要素をx13とする
    
    	madd	x13, x13, x10, xzr  ; 各色要素にモノクロ化した数値を格納
    
    	orr	x13, x13, #0xff000000ff000000  ; αチャンネルを付与
    
    	str	x13, [x0, x11]. ; メモリへ2ピクセル分のデータを格納
    
    	add	x11, x11, #8
    
    	sub	w12, w12, #1
    
    	cbnz	w12, LBB8_3
    
    

    Cで書いたソースそのままです。元が明示的にNEONのこの命令使えというソースからなので、最適化の余地がないのは仕方ないところでしょうか。これが、-Ofastの下ではuint32に負けてしまう理由なのでしょうか。

  • float:
    
    	ldr	q18, [x16], #16  ; 128ビットレジスタに4ピクセル分読み込み
    
    	and.16b	v17, v18, v3
    
    	ucvtf.4s	v17, v17  ; 4ピクセル分の赤要素を4つのfloatへ
    
    	and.16b	v19, v18, v4
    
    	ucvtf.4s	v19, v19  ; 4ピクセル分の緑要素を4つのfloatへ 
    
    	and.16b	v18, v18, v5
    
    	ucvtf.4s	v18, v18  ; 4ピクセル分の青要素を4つのfloatへ
    
    	fmul.4s	v19, v19, v6
    
    	fmla.4s	v19, v7, v17
    
    	fmla.4s	v19, v16, v18  ; 各色要素に{r,g,b}_lum_i を乗算し加算
    
    	fcvtzu.4s	v17, v19  ; 再び4つの整数(unsigned int)に変換
    
    	shl.4s	v18, v17, #8
    
    	orr.16b	v18, v17, v18
    
    	shl.4s	v17, v17, #16
    
    	orr.16b	v17, v18, v17  ; 4ピクセル分のモノクロ値をRGBに設定
    
    	orr.4s	v17, #255, lsl #24  ; 4ピクセル分のαチャンネルを設定
    
    	str	q17, [x15], #16
    
    	sub	x14, x14, #4
    
    	cbnz	x14, LBB9_5
    
    

    読みやすいです。そしてNEONを使った並列計算をうまく生かせた最適化になっています。これにより浮動小数点演算でありながら最速のuint32に迫る速度をiPhone5s/iPhone5/iPhone4のいずれでも出せています。

iPhone5sでの事前の予測としてはuint64かuint16x8が-Ofast付けても高速かなーと思っていたのですが、uint32が最速となりました。最適化後のアセンブリコードを眺めてみると、いずれもNEONをしっかり使ったものになっています。

同じNEONを使ってもuint16x8では一つの128ビットレジスタでの2ピクセル同時計算に留まらせた一方で、uint32では色要素毎の3つの128ビットレジスタを使って4ピクセル同時計算にまで最適化できているのが大きな違いでしょうか。

うまく最適化を生かせるCコードを書けば、素人の下手な最適化(uint16x8)に比べてより良い最適化(uint32)をしてくれるというのは嬉しいところです。また浮動小数点演算でもうまく最適化にはまれば良いコードを出してくれるのも嬉しいところです。

*1 ちなにみにAndroiod NDKでの最適化デフォルト値は-O2のようですが、先の実験では-Ofastを指定しています。

*2 もっとうまい最適化できるんじゃないかとか思ってしまったりもしますが(苦笑)


2013-10-14 (Mon)

モノクロ演算

うっかり忘れてましたが使用したプロジェクトをgithubに公開しました。

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

色々と最適化かけて比較してきましたが、一つ忘れてました。つまり-O0、つまり最適化なしです。デバッグビルドのデフォルトです。

機種名 iPhone 5s (-O0) iPhone 5 (-O0) iPhone 4 (-O0)
uchar 1491 2669 10238
uint32 1294 2311 8393
uint64 730 1020 3616
uint16x8 785 1872 7435
float 2018 4208 16766

あらら、uint16x8が一番速いのかと思いきや、uint64が一番速かったですね。



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

«前月 最新 翌月»


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|