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 が参考になると思います。