GcraudNanoのソフト担当です。ハード担当に負けじと自分も活動報告記事を書いていきます。

今回は、Arduino高速化の研究を書いていきます。 開発環境はXcodeですが、コンパイラはArduino IDEと同じavr-gccです。

ロボットプログラミングではちょっとしたテクニックでの高速化が非常に重要になってきます。ある程度はコンパイラが最適化してくれますが、Arduino IDEのデフォルトコンパイラであるavr-gccはあまり信用できませんので、いくつかのテクニックを検証してみました。



その1 コンパイラを信用する

先ほどコンパイラは信用できないと書きましたが、信用できるときもある程度はあります。しかし、Arduino IDEのデフォルト設定では、その力を最大限引き出すことはできません。

最適化オプションを指定しよう

gccには、以下5つの最適化オプションが存在します。

  • -O0(大文字の"オー"と"ゼロ") 最適化をしない
  • -O, -O1 ちょっと最適化する
  • -O2 もっと最適化する
  • -O3 コードサイズを大きくしてでも高速にする
  • -Os コードサイズが小さくなるようにする

このうち、Arduino IDEが使っているのは"-Os"オプションです。つまり、"-O3"でコンパイルすれば、それだけで高速になる可能性があるということです。

最大の不幸

残念ながら、Arduino IDEにオプションを変更する設定は存在しません。 諦めて、早くもっと優秀な開発環境を使いましょう。WindowsならNetbeansが、OS XならXcodeがオススメです。

その2 除算とビットシフト

Arduino IDEの鬼畜すぎる仕様により、いよいよもってコンパイラに頼れなくなりました。ここでは、最も手軽に行える最適化テクニックを紹介します。

割り算をするな!

実は、普通に割り算をしようとすると、コンパイラは非常に効率の悪いコードを生成します。

int foo(int i)
{
return i / 4;
}

上のコードは悪い例です。生成されたアセンブリファイルを読むと、変数`i'が符号付きの変数であるために、とてもめんどくさいコードとなっています。正直、`i'が符号付きかなんてどうでもいいですし、気にするだけムダです。

では、ここで正解のコードをみてみましょう。

int foo(int i)
{
	return i >> 2;
	// return (unsigned int)i / 4;
}
上下どちらのコードでも正解です。コンパイラは変数`i'を右に2つシフトするコードを生成します。しかし、2の倍数でなければビットシフトは使えないですし、コードの読みやすさの点からしても下のコードがベターでしょう。 ちなみに、掛け算はきちんとビットシフトしてくれるようです。

その3 回数を指定したループ

さようならfor文

C言語の回数指定と言えば間違いなくfor文でしょう。ですが、場合によってはwhile文のほうが高速なこともあります。以下の比較表にfor文とwhile文のC言語ソースと生成されたアセンブリを示します。

  Cソース アセンブリ
for文
for (int i = 0; i < 100; i++) {
		digitalRead(i);
}
.LSM1:
ldi r17,lo8(0)
.L2:
.LBB2:
.LSM2:
mov r24,r17
call digitalRead
subi r17,lo8(-(1))
.LSM3:
cpi r17,lo8(100)
brne .L2
while文
{
		int i = 100;
		while (i--) {
			digitalRead(i);
		}
}
.LSM1:
ldi r17,lo8(99)
.L2:
.LBB2:
.LSM2:
mov r24,r17
call digitalRead
subi r17,1
brcc .L2

実は自分はアセンブリが読めないため、それぞれの命令が何を意味しているのかよく判らないのですが、while文のほうが命令が1つ減っていてみるからに速そうです。それにコンパイラが気を利かせて`i'の初期値を99にしてくれているみたいです。きっと速いのでしょう。

for文はいらない子なのか?

このままではfor文があまりに不憫なのでフォローをしておくと、まずfor文は可読性が非常に高いです。これは素晴らしいことでしょう。読みやすいコードを書くということは時に実行速度より大切です。(ただしこの記事のコードは汚くて読めたもんじゃない)小さい数から順番に使いたい場合などはfor文もwhile文も変わらいので、どんどんfor文を使ってあげてください。

それから、重要な事として、実はループの回数が少なければコンパイラはループの中身を展開してしまいます。以下に証拠を示します。

Cソース
for (int i = 0; i < 5; i++) {
    digitalRead(1);
}
アセンブリ .LBB2:
.LSM1:
ldi r24,lo8(1)
call digitalRead
ldi r24,lo8(1)
call digitalRead
ldi r24,lo8(1)
call digitalRead
ldi r24,lo8(1)
call digitalRead
ldi r24,lo8(1)
call digitalRead

これはwhile文でも同じ結果でした。どの程度の回数まで展開してくれるのかは不明ですが、このくらいの回数ならfor文を使ってあげましょう。

その4 配列の参照

for文は 良くも悪くも 読みやすい

配列を順番に参照する方法といえばまたしてもfor文ですが、残念ながらここでもwhile文が速度で勝ります。ただし、while文での配列参照は複雑怪奇でC言語が苦手な人にとってはトラウマものになってしまいます。悪いことは言いません。配列とポインタの関係性が判らない人は以下のコードを読む時は考えるのをやめ、感覚で使えそうになければ大人しくfor文の友達になってあげてください。

C言語のあるべき姿

これから使うテクニックは、"ポインタ演算"と呼ばれています。読みやすさを生贄に捧げて、C言語特有の圧倒的な速さを召喚できます。

以下のテストコードを実行し、関数`foo'とコンパイラの最適化オプションを変えながら実行時間を計測しました。使用したマイコンはArduino Microです。

int main(int argc, const char * argv[])
{
    init();

#if defined(USBCON)
    USBDevice.attach();
#endif

// -------------------ここからがsetup()に相当-------------------
    int arg1[] = {1, 2, 3, 4, 5};
    int arg2[] = {42};
    int arg3[] = {1, 2, 4, 8, 16, 32, 64, 128};

    Serial.begin(9600);
    while (! Serial) ;

    unsigned long beg = micros();

    int ret = 0;

    {
        int i = 100;
        while (i--) {
            ret += foo(arg1, 5);
            ret += foo(arg2, 1);
            ret += foo(arg3, 8);
        }
    }

    unsigned long fini = micros();

    Serial.println(ret);
    Serial.print(fini - beg); Serial.println(" micros.");
// ---------------------------------ここまで---------------------------------

// この何もしない無限ループがloop()に相当
    while (1) ;

    return 0;
}

setup()とloop()は廃止したので存在しません。

  Cソース 実行時間(-Os) 実行時間(-O3)
for文
int foo(int array[], int count)
{
int ret = 0;
for (int i = 0; i < count; i++){
ret += array[i];
}
return ret;
}
  1. 956
  2. 956
  3. 956
  1. 956
  2. 956
  3. 956
while文
int foo(int array[], int count)
{
int ret = 0;
int *last = array + count;
do {
ret += *array;
} while (++array != last);
return ret;
}
  1. 808
  2. 804
  3. 804
  1. 804
  2. 804
  3. 800

関数`foo'は与えられた配列の各要素を足しあわせた結果を返します。for文とwhile文では最大156マイクロ秒もの差が出ました。

さいごに

ここで紹介したテクニックを使っても、稼げる時間は精々数百マイクロ秒程度でしょう。ですが、リアルタイムでの判断が勝負を分けるロボットの世界ではこの差は圧倒的な強さとなりえます。動けばいいと思わず、積極的な最適化を心がけましょう。

以上C++をこよなく嫌うNanoソフト担当でした。

 

補足

コメントでご指摘を頂いたのでコードを見なおしたところ、改善点がみつかりましたので、以下に示します。

int foo(int array[], int count)
{
int ret = 0;
do {
ret += *array++;
} while (--count);
return ret;
}
対応するのは一番最後のコードです。変数lastを宣言しないで済む分高速化が期待できます。

次回:Arduino高速化テクニック②