強火で進め

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

iPhoneでインラインアセンブラを使う(プログラム編[1])

iPhoneインラインアセンブラを使う「プログラム編」を始めます。先に記載した「資料編」で紹介した資料の入門部分をざっと読んでおいたり、一緒に見ながら進めると理解が進みやすいと思います。

準備

まず、プログラムに取りかかる前にいくつか気をつけることがあるので順に説明して行きます。

ARM命令、前提で解説

最近のARM CPUはARM命令とThumb命令という2つの命令が使用可能です。アセンブリで記述した場合、両方の命令を混ぜて使用できたりもするのですが今回はすべてARM命令で記述することとします。

なおそれぞれの命令には以下の特徴があります。

ARM命令 Thumb命令
1つの命令のサイズ 32bit 16bit
プログラムのサイズ 大きい 小さい
プログラムの起動 遅い 速い
プログラムの動作 速い 遅い

容量を気にせず、起動してからの動作速度を優先する場合は「ARM命令を選択」、それ以外の場合は「Thumb命令を選択」との判断になると思います。

プロジェクトのコンパイラの設定をARM命令に

新規作成したプロジェクトはデフォルトではThumb命令のアセンブリが使われる様にコンパイラの設定がされています。

そのためARM命令を使用する様に切り替えます。手順は以下の様になります。

1. プロジェクトを右クリックし、「情報を見る」を選択、「ビルド」タブを選択。
2. 「ビルド設定を検索」と記載してある入力欄に「Thumb」と記述。
3. 「Compile for Thumb」という項目が表示されるのでチェックを外す。

テストは実機(iPhone)で

テストは実機で行う必要があります。シミュレータはあくまでシミュレータであり、ARMアセンブリの再現までは行われません。どうしてもシミュレータで確認したい場合は「シミュレータで動作してるときはC言語のみの記述」、「実機で動作しているときはインラインアセンブラを使用する」とプログラムを分ける必要があります。なお、今回のサンプルは実機動作前提で「C言語のみの記述」は準備しません。

そんなに速くないよ(^_^;)

正直、アセンブリで書いてもそんなには速くないです。最近のC言語コンパイラは本当に優秀で下手に人間がアセンブリで書いたものよりよっぽど良いアセンブリに変換します。

それでも慣れて来たらコンパイラより速いアセンブリの記述は可能ですし、C言語をやってるときに「ここなんでこんなに遅いんだろ?」って思ったときにコンパイラが変換したアセンブリのコードを見て原因を突き止める事も、解決策を見つける事も出来る様になります。

そのような付加価値まで考えるとやってみる価値は十分に大きいのではないでしょうか?

まずは簡単なサンプル

まずは変数に値を代入するだけの簡単なサンプルから始めます。

内容は「最初、10が代入されている変数の値をインラインアセンブラで新たに3を代入し、値を変更するプログラム」です。

記述はこの様になります。

    int val1 = 10;
    __asm__ volatile (
                      "mov        %0, #3 \n\t"
                      : "=r" (val1) 
                      : 
                      );
    NSLog(@"%d\n", val1);

これをiPhoneプログラムのどこでも良いのですが取りあえず applicationDidFinishLaunching: に記述することにします。

実際のプログラムはこの様になります。

- (void)applicationDidFinishLaunching:(UIApplication *)application {    
    int val1 = 10;
    __asm__ volatile (
                      "mov        %0, #3 \n\t"
                      : "=r" (val1) 
                      : 
                      );
    NSLog(@"%d\n", val1);
    
    // Override point for customization after app launch    
    [window addSubview:viewController.view];
    [window makeKeyAndVisible];
}

順番に解説して行きます。

まず、このブロック。この部分がインラインアセンブラの記述になります。

    __asm__ volatile (
                      "mov        %0, #3 \n\t"
                      : "=r" (val1) 
                      : 
                      );

__asm__ がアセンブリの記述を行うことを示します。asm とだけの記述で解説してあるサイトも多いのですがこれではObjective-C(C言語もかな?)ではこの記述は使えない様です。

拡張子を .cpp や .mm にしてC++Objective-C++にすれば asm も使える様ですがそのような大げさな事をするよりは __asm__ を使う方が良いでしょう。

次に volatile です。この記述はコンパイラの最適化作業による変更を抑止します。この記述をしておけばこちらが記述した通りのアセンブリが生成されます。

もっと良く知りたい方はこちらのサイトなど、GCCインラインアセンブラについて解説しているサイトを参照下さい。

GCC Inline Assembler
http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html

そしてついにメインの記述部分です。

                      "mov        %0, #3 \n\t"
                      : "=r" (val1) 
                      : 

この場合、 mov 命令で %0 に #3 を代入という記述になります。

C言語風に書くとこの様になります。

%0 = #3

ここでの %0 (複数使用する場合は%1、%2などと続きます)はC言語で定義した変数との橋渡しを担います。イメージとしては printf("%d", val1); の %d と val1 の関係となります。

今回は val1 と %0 とが関連付けられているため先ほど記載したこちらの記述は

%0 = #3

実際にはこの様なイメージとなります。

val1 = #3

先頭に # が付いた数字はイミディエイト(即値。この場合は数値の直接指定のこと)を表します。そのため完全にC言語と同様に記述するとこうなります。

val1 = 3

なお、16進数を使用したい場合は #0x10 などの様に # の後に 0x を記述します。

次に "" の後に記載してある : についての説明です。
こちらはこの様なデータの区切りとして記述します。

__asm__ volatile (
"(アセンブリの記述)"
:(出力オペランドに対応するデータ)
:(入力オペランドに対応するデータ)
);

アセンブリの記述が行われる "" のブロックが終了した直後の : の後から「出力オペランドに対応するデータ」の記述、次の : 以降は「入力オペランドに対応するデータ」の記述エリアになります。

今回は「入力オペランドに対応するデータ」は特にC言語の世界からインラインアセンブラの世界に送り込んでいないので空欄になっています。

「出力オペランドに対応するデータ」については橋渡しが必要であったので "=r" (val1) の様な記述をしています。

オペランドについて

オペランドとはC言語で言うところの関数の引数の様なものです(もちろん厳密にはかなり違い、実際には式の方が近いのですが大体のイメージとして)。

今回の様に記述されていた場合、 %0 が第一オペランド、 #3 が第二オペランドと呼ばれます(関数の第一引数、第二引数の様なものです)。

mov %0, #3

多くの命令が以下の様に右側に記述されたオペランドで四則演算などが行われ、結果が左側に記述されるオペランドに代入されます。
※あくまで多くの命令でであり、一部例外があります。

第一オペランド←第二オペランド,第三オペランド

第一オペランド,第二オペランド←第三オペランド,第四オペランド

出力オペランドに対応するデータの記述について

"=r" (val1)

この部分では "" の中でデータの特性を指定し () の中にはCで作成した変数名を指定します。

"=r" については以下のサイトに記載があり、 = が「書き込み(アウトプット)専用」、 r がレジスタを使用するとこを指定しています。

ARM GCC Inline Assembler Cookbook
http://www.ethernut.de/en/documents/arm-inline-asm.html

レジスタとは

レジスタとはC言語風にざっくり言うとCPU上に確保された超高速な変数の様なものです。基本的にデータはこの中に展開された後、四則演算やシフトなどさまざまな加工が行われます。

実際には今回使用していない3つ目の項目があり、以下の様な構成となります。3つ目の項目については実際に使用するサンプルを解説したとき説明します。

__asm__ volatile (
"(アセンブリの記述)"
:(出力オペランドに対応するCの式)
:(入力オペランドに対応するCの式)
:(破壊されるデータの情報)
);

それではプログラムを通して見てみましょう。

    int val1 = 10;
    __asm__ volatile (
                      "mov        %0, #3 \n\t"
                      : "=r" (val1) 
                      : 
                      );
    NSLog(@"%d\n", val1);

1. C言語上で val1 変数を作成。10を代入。
2. mov %0, #3 により val1 の値は 3 に変更される。
3. NSLog() で出力される値は 3 となる。

とりあえずごくごく最小限のインラインアセンブラの記述はこの様になります。

少々、長くなったので「プログラム編」はこれからいくつかに分けて書いて行く予定です。

iPhoneインラインアセンブラを使うの記事一覧はこちら