プログラミングと最適化
C MAGAZINE11月号の「最適化 徹底研究」を読んで思ったこと。
volatile属性
最近のコンパイラの最適化はかなり優秀なので、プログラマ自身がロジックを最適化する必要はあまりなくなってきた。
最新のVisual Studioでコンパイルされたアセンブラコードを見ると、もはやアセンブラで書く意味もあまりないと思えるぐらい効率のよいコードが生成されている。
ループ内で配列演算をせずポインタを使うとか、よく使う変数にregister属性をつけるとか、四則演算よりもなるべく単純な論理演算を行うとか、そういうことはもはやプログラマがやることではないようだ。
もちろんループ回数を減らすとかI/Oを少なくするとか、プログラマがするべき最適化の余地も多く残されてはいるけれど。
だが、あまりコンパイラの最適化がしにくい部分というのが存在する。
マルチスレッドなどの並列処理や、メモリマップドI/Oなど特殊なアクセスを必要とする空間などである。
ぼくが知る限りコンパイラがこれらをサポートするために用意している機能はvolatile属性だけだ。
他の部分はプログラミングテクニックであるとか、OSのサポートルーチンなどで対応するしかない。
最近遭遇した問題
先日ファイラーをデバッグしている時、こんなことがあった。
ファイラーはテキストビュワーのしおりをSRAM領域に保存している。
GBAのSRAM領域は8ビットバスであり、メモリアクセスはバイト単位に行わないと、正しい結果が得られない。
そこでしおり領域のヘッダも当然バイト単位で構造体を定義していた。
以下はしおり管理エリアのヘッダである。
プログラムは最初にこのヘッダをチェックし、正しい情報でなければヘッダとしおり情報を再作成するようになっている。
typedef unsigned char u8; typedef struct _BMHEAD { u8 sig[2]; u8 ver; u8 headsize; u8 nitems; u8 curindex; u8 reserved[10]; } BMHEAD, *PBMHEAD; #define BMH_SIG "BM" #define BMH_VER 0x01
先日プログラムが最適化されていなかったことに気づいたぼくは、全てのソースをコンパイルしなおした。
だが、なぜかしおり機能が動かなくなった。
どうもヘッダ部分が常に不正と判断され、毎回再作成されているようだ。
ヘッダ部分はこんなふうにチェックしていた。
static BOOL IsValidBMHead(PBMHEAD pbh) { if (memcmp(pbh->sig, BMH_SIG, sizeof(pbh->sig)) || pbh->ver != BMH_VER || pbh->headsize != sizeof(*pbh)) return FALSE; return TRUE; }
値も特におかしいわけではないのに、なぜFALSEが返るのだろう。
最適化しておかしくなったのだから、ひょっとして変なコードが出ているのか?
そう思ってアセンブラリストを取ってみると。。。
ldrh r2, [r4, #2] mov r3, #4096 add r3, r3, #1 cmp r2, r3 moveq r0, #1 beq .L23
そう、verとheadsizeがまとめて評価されていたのだ。
常にバイト単位でアクセスしなくてはならないSRAM領域に2バイトアクセスしていたのだから、常にFALSEが返るわけだ。
そんなわけでヘッダやしおり部分の構造体のメンバーにvolatileをつけることで問題は解決した。
たしかにこれはバイトアクセスを要求するメモリ空間を普通に扱おうとした、ぼくのミスである。
通常のアクセスができないのだから、一種のI/O空間とも言えるかもしれない。
だが、構造体のメンバーも全てバイトで、アクセスもバイトなのだから、問題ないと思ってしまったのだ。
まさかコンパイラがこんな最適化をするとは思わなかったよ。
いい勉強になりました。
こんな機能が欲しい
さて、このような特殊な空間、読み出すたびに値が変わるI/O空間、あるいは別スレッドから変更される可能性がある共有メモリ空間。
これらに対してコンパイラに指示できるのはvolatile属性だけだ。
これを付けることで、コンパイラはメモリ空間が変更されている可能性を知ることができる。
だが、当然これはコンパイラの最適化を抑止することになる。
実際にはvolatileが付けられていても、常に値が変わっているとは限らない。
スレッド共有空間であっても、クリティカルセクション内なら値は変わらない。
I/O空間だって、常に読み書きするばかりではなく、普通に最適化してほしい場合もあろう。
例えばファイラーの件では、実際にはバイト単位にアクセスしたいわけで、volatileなわけではないのだ。
さらにこういった空間の属性がvolatileしかないので、それ以外の部分はプログラマのスキルに頼るしかない。
マルチスレッドやI/Oの制御などはそれぞれ独自のノウハウが必要である。
しかもそのノウハウを知らない人がソースを見ても、なかなかそのへんのノウハウが読みとりにくい。
「なんでここでこんなことをする必要があるんだろう」などと悩むことも多いのだ。(ぼくだけ?)
そんなわけで、言語の処理系にはvolatile以上にこういった属性を指定できる機能を追加してほしい。
例えば#pragmaなどで、この空間は常にバイトアクセスを要求するとか、この空間はスレッド共有空間であるとか。
さらに進めて、コンパイラがプログラムのロジックをチェックできたりするといいな。
「Warning: このメモリ空間はスレッド共有空間ですが、クリティカルセクションの外で参照されています」とかね。
最適化の可能性が増し、なおかつプログラムの品質も向上するので、なかなかいいアイデアだと自分では思っているのだが。
各社の処理系を作ってくださっている担当者様。
処理系にこのような機能を入れていただけないでしょうか。
通常のアプリケーション開発者にはあまり意味がないかもしれないですが、OSや組み込み系の技術者たちは喜ぶと思います。
今やARMなどの組み込みプロセッサですらマルチプロセッサになる時代。
今後マルチスレッドなどはさらに普及していくと思います。
ぜひぜひご検討くださいませ。(_O_)