CPUの動作を理解するとちょっとだけ早いコードが書けるかもしれない
昨日処理の高速化について考えてたのでその関連の小ネタを一つ。
例えばn回ループする処理を書くとき、C言語の入門では
1) for(i = 0; i < n; i++){(ループ内処理)}
という書き方が行われていることが多い。しかしシステムによってはループの速度を上げるためには
2) for(i = n; i; i--){(ループ内処理)}
としたほうがいい場合がある。なぜか。
簡単のために具体的な例としてAVRのCPUの動作について考える。
CPUの処理の中で、条件分岐はどのように行われているかというと、直前の計算結果によって変化する「ステータスレジスタ」というものを見て分岐するか否かを判定している。
AVRのステータスレジスタは以下の8bitからなる
- C Carry Flag
- Z Zero Flag
- N Negative Flag
- V Two’s complement overflow indicator
- S N ⊕ V, for signed tests
- H Half Carry Flag
- T Transfer bit used by BLD and BST instructions
- I Global Interrupt Enable/Disable Flag
それぞれの説明は面倒なので省くが、はっきりわかるのは、この中に「iがnより小さい」ということを示すフラグがないことと「計算結果がゼロになった」ということを示すフラグであるZero Flagがあることである。
じゃあ「iがnより小さい」ということを調べるにはどうすればいいかというと「iとnの大小を比較する」命令(CP)を使う。この命令は「iからnを引く処理を行ってステータスレジスタを変化させるが、引き算の結果そのものは保存しない」という動作をする。引き算の結果が負になったとすればそれはiがnより小さいということで、引き算の結果が負になったかどうかはNegative Flagを見ることでわかる。
1)の書き方の場合処理は以下のようになる。
- iを0にする
- (ループ内処理)
- iに1を足す
- iとnを比較する
- Negative Flagが1ならばループ内処理の先頭に戻る
2)の書き方の場合処理は以下のようになる。
- iにnを代入する
- (ループ内処理)
- iから1引く
- Zero Flagが1ならループ内処理の先頭に戻る
1)のほうがiとnを比較する必要があるため1命令多く実行することになる。
ちなみにもっと早くしたければ(ループ内処理)をn回並べればいい。分岐がなくなるので速くなるが当然コードのサイズは膨れ上がる。
複数の命令が実行できたりキャッシュがあったりする最新鋭のCPUでは上記の限りではないかもしれない。試したことも考察したこともないので知らない。
以前は電子工作でマイコンを使う場合はアセンブリ必須だったようだが、私が電子工作を始めた時期にはavr-gccがあり、その後もCコンパイラのあるマイコンしか使ったことがないのでアセンブリをまともに触ったことがない。現代では命令を理解しCなどで記述した処理を高速に実行できるアセンブリに変換するのは基本的にコンパイラの仕事だが、CPUの動作というものを知っておけばちょっとだけ早いコードを書くことができる、と思う。