ARM NEON命令を使って画像の回転
What is the fastest way to copy memory on a Cortex-A8?
という記事によると、単純にワードごとにメモリをコピーした場合に比べ、ARM NEON命令を使うと約50%ほどコピーが早くなるそうです。
画像の回転も、基本的には順番を入れ替えながらのメモリコピーですので、NEON命令を使うとどのくらい早くなるのか、調べてみました。
単純にC言語で実装した場合
まずは、こんなコードでFull HD(1920x1080サイズ)画像を180度回転させて、どのくらいかかるか測定します。bits/pixelはRGBxを想定して32bit決め打ちです。画像を180度回転させる処理は、コピー元の開始アドレスから1ピクセル(32bit)ずつ読み出して、コピー先の最終アドレスから逆順にコピーしていくだけの処理となります。
/** * @param src 入力画像アドレス * @param dst 出力画像アドレス * @param nr_pix 画像のピクセル数 */ void rotate180(void *src, void *dst, size_t nr_pix) { unsigned int i; uint32_t *s = (uint32_t *)src; uint32_t *d = (uint32_t *)dst + nr_pix - 1; for (i = nr_pix; i ; i--) { *d = *s; s++; d--; } }
Cortex-A9 シングルコア、DDR3 SDRAM 512MB搭載のCPUボードで動作させてみたところ、約110msecかかりました。
gcc -Sで出力したアセンブリコードは下記のようになります。
.align 2 .global rotate180 .thumb .thumb_func .type rotate180, %function rotate180: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 @ link register save eliminated. cbz r2, .L1 subs r3, r2, #1 add r1, r1, r3, lsl #2 .L3: ldr r3, [r0], #4 subs r2, r2, #1 str r3, [r1], #-4 bne .L3 .L1: bx lr
コピーを半分にする
入力と出力で2面も画像用のメモリを持つのは勿体ないので、元の画像用メモリを上書きするようにしてみました。
/** * @param p 画像アドレス * @param nr_pix 画像のピクセル数 */ void rotate180_2(void *p, size_t nr_pix) { unsigned int i; uint32_t *s = (uint32_t *)p; uint32_t *d = (uint32_t *)p + nr_pix - 1; uint32_t tmp; for (i = nr_pix/2; i ; i--) { tmp = *d; *d = *s; *s = tmp; s++; d--; } }
ループが半分になったので、処理時間は約70msecとなりました。
アセンブリコードは下記のようになります。
.align 2 .global rotate180_2 .thumb .thumb_func .type rotate180_2, %function rotate180_2: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 @ link register save eliminated. lsrs r3, r1, #1 push {r4} beq .L6 subs r1, r1, #1 mov r2, r0 add r1, r0, r1, lsl #2 .L8: ldr r0, [r1, #0] subs r3, r3, #1 ldr r4, [r2, #0] str r4, [r1], #-4 str r0, [r2], #4 bne .L8 .L6: pop {r4} bx lr
NEON命令を使う
インラインアセンブラでNEON命令を使って書いてみます。vrev64.32 は、64bit長のレジスタ(d0やd1)を32bitずつ入れ替える命令です。
/** * @param p 画像アドレス * @param nr_pix 画像のピクセル数(8の倍数) */ void rotate180_simd(void *p, size_t nr_pix) { unsigned int i=nr_pix/8/2; uint32_t *s = (uint32_t *)p; uint32_t *d = (uint32_t *)p + nr_pix - 1 - 8; asm volatile("\n" ".p2align 2\n" ".loop:\n" "pld [%0, #64]\n" "pld [%1, #64]\n" "vld1.64 {d0-d3}, [%0]\n" "vld1.64 {d4-d7}, [%1]\n" "vrev64.32 d11, d0\n" "vrev64.32 d10, d1\n" "vrev64.32 d9, d2\n" "vrev64.32 d8, d3\n" "vrev64.32 d15, d4\n" "vrev64.32 d14, d5\n" "vrev64.32 d13, d6\n" "vrev64.32 d12, d7\n" "vst1.64 {d8-d11}, [%1]!\n" "vst1.64 {d12-d15}, [%0]!\n" "subs %1, %1, #64\n" "subs %2, %2, #1\n" "bne .loop\n" : : "r"(s), "r"(d), "r"(i) : "q0","q1","q2","q3","q4","q5","q6","q7"); }
このコードで、約40msまで短縮することができました。
アセンブリコードは下記のようになりました。
.align 2 .global rotate180_simd .thumb .thumb_func .type rotate180_simd, %function rotate180_simd: @ args = 0, pretend = 0, frame = 0 @ frame_needed = 0, uses_anonymous_args = 0 @ link register save eliminated. sub r3, r1, #9 fstmfdd sp!, {d8, d9, d10, d11, d12, d13, d14, d15} lsrs r1, r1, #4 add r3, r0, r3, lsl #2 #APP @ 139 "rotate.c" 1 .p2align 2 .loop: pld [r0, #16] pld [r3, #16] vld1.64 {d0-d3}, [r0] vld1.64 {d4-d7}, [r3] vrev64.32 d11, d0 vrev64.32 d10, d1 vrev64.32 d9, d2 vrev64.32 d8, d3 vrev64.32 d15, d4 vrev64.32 d14, d5 vrev64.32 d13, d6 vrev64.32 d12, d7 vst1.64 {d8-d11}, [r3]! vst1.64 {d12-d15}, [r0]! subs r3, r3, #64 subs r1, r1, #1 bne .loop @ 0 "" 2 .thumb fldmfdd sp!, {d8, d9, d10, d11, d12, d13, d14, d15} bx lr
まとめ
画像を180度回転させるコードを、NEONを使って書いてみました。倍速とまではいきませんでしたが、それに近い高速化(70msから40ms)ができました。
今回は単純な180度回転のみを扱いましたが、90度回転をしたい場合は、 Rotating a color image 90°using ARM "NEON" Advanced SIMD assembly code が参考になると思います。