強火で進め

このブログではプログラム関連の記事を中心に書いてます。

iPhoneでインラインアセンブラを使う(プログラム編[6] 画像のグレースケール化)


iPhoneでのインラインアセンブラの使い方を簡単に説明してきましたが最後となる第6回では実用的なサンプルとして「画像のグレースケール化」の解説をします。

サンプルのインラインアセンブラの部分はこの様になります。

static __inline__ void asmGrayscale(unsigned char *bitmap, int bitmapSize){
    int div3 = 21845; // 16:16の固定少数での 1/3 の値
    __asm__ volatile (
"1: \n\t"
                      // メモリからロード
                      "ldrb       r0, [%[src_bitmap]], #1 \n\t" // r0 = R値
                      "ldrb       r1, [%[src_bitmap]], #1 \n\t" // r1 = G値
                      "ldrb       r2, [%[src_bitmap]], #2 \n\t" // r2 = B値(アルファチャンネルをスキップするため #2 となる)
                      
                      "add        r0, r0, r1 \n\t"
                      "add        r0, r0, r2 \n\t"
                      "mov        r0, r0, lsl #16 \n\t"         // RGBの合計値も16:16の固定少数点にするため16シフトする
                      "smull      r1, r2, r0, %[div3] \n\t"     // 16:16の固定少数点として乗算

                      // メモリへストア
                      "strb       r2, [%[dst_bitmap]], #1 \n\t"
                      "strb       r2, [%[dst_bitmap]], #1 \n\t"
                      "strb       r2, [%[dst_bitmap]], #2 \n\t" // アルファチャンネルをスキップするため #2 となる
                      "subs       %[bitmapSize], %[bitmapSize], #1 \n\t"
                      "bne        1b \n\t"
                      : [src_bitmap] "+r" (bitmap), [dst_bitmap] "+r" (bitmap), [div3] "+r" (div3),
                      [bitmapSize] "+r" (bitmapSize)
                      : 
                      : "r0", "r1", "r2",
                      "cc", "memory");
}

こちらにファイルを置いていますので今回解説するインラインアセンブラの部分以外についてはこちらを参照下さい。

大体は今まで解説したので理解できるかと思いますが主な部分やちょっと変わった手法を使っているところを解説します。

関数の引数

まず、この関数の引数について解説しておきます。 bitmap が画像データの先頭アドレスを指しています。 bitmapSize がピクセルの数(画像の幅x高さ)です。

処理内容

次に処理内容を説明します。今回は画像のグレースケール化なのでRGBの各値を加算した後、3で割る以下の様な処理をします。

gray = (r + g + b) / 3

※厳密にはグレースケール化するときは以下の割合いで行う方が綺麗に結果となります。今回はプログラムの簡略化ために省略しました。

gray = r*0.299 + g*0.587 + b*0.114;

今回の処理を素直に実装すれば除算(割り算)を使うことになるのですが除算はとにかく遅いので速度が必要なプログラムではさけるのがセオリーになっています。

と言っても自分も始めはガンガン使っていてそれでもCオンリーで記述したものよりずいぶん速かったので良いかなぁ。と取りあえず置いておいたのですがふと、「そういえばリリースビルドしてないなぁ」と思い立ちリリースビルドをしたところかなりの大差をつけられてCオンリーのものに負けてしまいました。GCCさんまだ本気出してなかったのねorz

ということでGCC先生のリリースビルド版のアセンブリコードを参考に除算を使わない様に修正したものが先ほどのものとなります。

ちなみにそこまでがんばっても以下の様な処理速度となりました。
※画像データグレースケール化の部分のみの処理速度

インラインアセンブラ使用 約0.010sec
Cのみ 約0.011sec

サンプルでは「ASM」、「C」のそれぞれのボタンで「インラインアセンブラ使用」「Cのみ使用」のそれぞれの処理速度がコンソールに出力される様に作成しているんですが何回か実行したところ処理のタイミングにはよっては多少変動があるのでかろうじて勝ってますけど、実際はほとんど誤差みたいなものですね。

実際のプログラムではこのくらいの改善ではあまり積極的に使うべきではないですね。悪い例です、みなさんはもっと有効なところで使いましょうw

さてアセンブリの内容ですがさすがGCC先生、きちんと除算を回避してました。ちょっとこの辺りは分かりづらいと思うので詳しく解説しておきます。

除算は乗算に置き換える

まず、 /3 で除算が発生するのをさけるためにここを乗算に置き換えます。 /3 = *(1/3) という式が成り立つので事前に 1/3 の値を計算しておいてそれを乗算することで対応します。 1/3 なので結果は 0.3333333333333333 となります。

固定小数点を使う

自分だと想像できたのはここまでで後はそのまま浮動小数点の乗算で実行する程度までしか発想が行かないのですがさすがGCC先生、優秀なプログラマーの英知の結晶なだけあります。ここを浮動小数点ではなく固定小数点で処理して、整数演算(浮動小数点の演算より速い)だけで済ましてます。

固定小数点について簡単に解説すると変数の上位ビットを整数部、下位ビットを浮動小数部として割当て整数型の変数で浮動小数点を表現するという手法です。

例えばここで int a; が有り、この a に固定少数点で2.5を代入する場合を考えます。
ここでは「int=(iPhoneでは)32bit」の上位16ビットに整数部、下位16ビットに浮動小数部と使う事にします。

以下の赤い部分を整数部、青い部分を浮動小数点部として使用します。

00000000:00000000:00000000:00000000

※8ビット区切りで左端が最上位ビットの31ビット目として記述しています。

整数部はそのまま使用すれば良いので全ビットで表すと2を表す場合、以下の様になります。

00000000:00000010:00000000:00000000

※ビットでの代入はC言語では出来ないので実際に使用する値としては 0x02000000

さてくせ者な「浮動小数部」です。こちらは下位16ビットの最上位ビットがセット、つまり以下の様な状態の時に 0.5 を表します。

00000000:00000000:10000000:00000000

※実際に使用する値としては 0x00008000

以下の場合は 0.25 を表します。

00000000:00000000:01000000:00000000

※実際に使用する値としては 0x00004000

つまり1つ下位に移動する毎に1/2されるルールです。

整数部と浮動小数点部の境界付近の値は以下の様になっています。上の行がどのビットかを示し、下の段がそのビットがセットされていたときの表す数値を記述しています。

18ビット目 17ビット目 16ビット目 15ビット目 14ビット目 13ビット目
4 2 1 1/2 1/4 1/8

そのため今回、例にとした 2.5 を表す場合、ビットは以下の様になります。

00000000:00000010:10000000:00000000

※実際に使用する値としては 0x00028000

ちなみにプログラム上で「浮動小数点→固定小数点」の計算する場合は浮動小数点部のビット数だけシフトした値を乗算することになります。 2.5 を変換する場合、C言語では以下の様な記述となります。

fix = 2.5 * (1<<16)

さて、それをふまえて今回使用する 0.3333333333333333 を固定小数点に変換します。

(1.0/3) * (1<<16)

このような計算はLL言語でささっと計算するのが効率的でしょう。例えばPythonでは以下のような記述で求められます。
(10進数で取得するとき)

"%d" % ((1.0/3)*(1<<16))

(16進数で取得するとき)

"%X" % ((1.0/3)*(1<<16))

※ターミナルで python とタイプするとPythonが実行できるようになります。Pythonから抜けるときは ctrl+D となります。

そして、計算した結果は 21845 なのでこの様になります。

    int div3 = 21845; // 16:16の固定少数での 1/3 の値

RGBAの順番で並んでいる画素情報からRGBのみレジスタにロードし、

ldrb       r0, [%[src_bitmap]], #1

そのRGBの値を合計します。

add        r0, r0, r1

そしてその値は通常の整数値なので div3 に格納されている整数部は上位16ビット、浮動小数点部は下位16ビットの形式に合わせるためにビットシフトを行います。ARMの場合は mov 命令などいくつかの命令で最後のパラメータとしてシフトを指定することが可能です。そのためARMでのシフト処理これを利用し、以下の様に記述します。

mov        r0, r0, lsl #16

次に smull です。これは乗算処理をします。しかし、通常の mul とは異なり、結果は64ビットで返すという特徴があります。しかし、ARMで汎用レジスタのサイズは32ビットであるため上位32ビットと下位32ビットに分けて返します。

smull      r1, r2, r0, %[div3]

ここが「さすがGCC先生、ハンパないなぁ」と思ったんですけど

結果(64ビット) =  (第一オペランド) * (第二オペランド)

って実際は

上位32ビット =  (第一オペランドの上位16ビット) * (第二オペランドの上位16ビット)
下位32ビット =  (第一オペランドの下位16ビット) * (第二オペランドの下位16ビット)

となっているです(「ARMアーキテクチャリファレンスマニュアル」を参照)。

これってよく見ると結果の上位32ビットって固定小数点の整数部のみの乗算結果がそのまま格納されてるんですよね。
そのため計算結果の上位32ビットが格納されている r2 をそのまま書き戻せばOKとなります。

strb       r2, [%[dst_bitmap]], #1

うーん、すばらしい!!
GCC先生が一番参考になるアセンブラコードの様な気がします。いつもそばにいてくれますしねw

後は画素の数だけループするため bitmapSize を1づつ減算してます。

subs       %[bitmapSize], %[bitmapSize], #1
bne        1b

とりあえず今回で「iPhoneでのインラインアセンブラの使い方」は一区切りつけます。

そのうちVFPなどのプログラムを作ったときやなにか面白いテクニックを見つけたときなどにはまたアップします。それでは(^_^)/

iPhoneインラインアセンブラを使うのエントリー一覧はこちら