| 
 
 昨日のカウンタ:  今日のカウンタ:  | 
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
珍しくこちらの日記でプログラミングのことを書いてみます。
iOSのプッシュ通知を自分のアプリで使おうと思った場合、特に個人開発者の場合だと、サーバを借りるのが負担だったりして躊躇してた例が多いのではないかなと思います。
プッシュ通知を行うには、AppleのAPNS(Apple Push Notification Service)を叩いてあげる必要がありますが、実は半年以上前からGAE(Google App Engine)からプッシュ通知用のAppleを叩けるようになって*1、その辺りの負担が軽くなっています。大量に使うとGAEも別に安くないですが、規模が小さければありだと思います。数人で実験する程度ならGAEの無料の範囲内に収まりますし。
こちらの本にもその辺りの手順やら解説やらを書きましたので読んでみて下さい。(ステマじゃない堂々とした宣伝)
iPhoneというかiOSデバイス側のサンプルはネット上のあちこちにあるのですが、APNSを叩く側のサンプルコードもちょっと書いてみました。GAEの標準もPythonなので(えっJavaとかGoとかは?)、Pythonで書いてみました。いや、ネット上にあるライブラリを呼び出してるだけですが。
from apns import APNs, Payload
 
apns = APNs(use_sandbox=True, cert_file='cert.pem', key_file='key.pem')
token_hex = 'b2df4272 aa(ここにデバイストークンを書く)128 2cca1b49'.replace(" ", "")
 
payload = Payload(alert="hi", content=0)
apns.gateway_server.send_notification(token_hex, payload)
print("sent\n")
例えばこれをsend_to_apns.pyという名前で保存します。
あとはライブラリとしてはPyAPNsをフォークしたもの*2を使います。とは言え、必要なのはapns.pyだけで、これをダウンロードしてsend_to_apns.pyと同じディレクトリに置けば良いです。
後はcert.pemとkey.pemを標準的な方法で生成してから、ターミナルから「% python send_to_apns.py」を実行すれば、あらかじめ動かしておいたiOS上のプッシュ通知用テストアプリに通知が飛ぶのを確認できるはずです。
えっ? cert.pemやkey.pemの作り方がわからない? プッシュ通知用iOSテストアプリのサンプルが欲しい?
● こっちの日記は半分ネタですが、本の方はちゃんと書いてます。m(_ _)m
日記に書き忘れましたが11/1早朝に注文して11/3朝に到着しました。emobileのデータ契約SIMは快適です。
日記に書き忘れましたが11/1にAppleストアに並んで買いました。セルラーモデルです。
色々と最適化かけて比較してきましたが、一つ忘れてました。つまり-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が一番速かったですね。
先に書いたようにリリースビルドでのデフォルトの最適化オプションは-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による最適化がどのように行われているか、ちらっと眺めてみましょう。ループの中の処理だけ抜き出し、邪魔になりそうなコメントは抜いて、あと私の方で最低限のコメントを入れてみました。 コンパイル元のコードについては前の記事を参照して下さい。
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が入っています。
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ピクセルの計算にまとめていました。しかも最適化後でもシンプル!
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
	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に負けてしまう理由なのでしょうか。
	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)をしてくれるというのは嬉しいところです。また浮動小数点演算でもうまく最適化にはまれば良いコードを出してくれるのも嬉しいところです。
せっかく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)のどれよりも遅くなっていますが、仕方ないところでしょうか。
Before...
◯ さきら [面白そうなネタですねー。ただ、このところ保留させてもらっている事柄が多くて、いつできるかというお話ができません。。。..]
◯ MattBdog [A church was created with the thinking that if Indiana’s b..]
◯ BrianWrora [For decades, Wunsiedel, a German town near the Czech borde..]