|
昨日のカウンタ: 今日のカウンタ: |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
先に書いたようにリリースビルドでのデフォルトの最適化オプションは-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)をしてくれるというのは嬉しいところです。また浮動小数点演算でもうまく最適化にはまれば良いコードを出してくれるのも嬉しいところです。