■
明日書くと言った(以下略
というわけで、コンパイラには最適化できないって話だったんだけど、あんまりコンパイラの限界の話ばっかりしててもアレなので、コンパイラは賢いよ、っていう話も書いておく。なんかやる気が出ないので短めで。
int loop( int *a, int *b, int ai, int bi, unsigned int n ) { unsigned int i; for ( i=0; i<n; i++ ) { a[i*ai] = b[i*bi]; } }
こういうコードがあったとしよう。さて、これは、どんなふうに最適化できるだろうか。
.L5: movl (%ecx), %eax addl %edi, %ecx movl %eax, (%ebx) addl %esi, %ebx decl %edx jne .L5
GCCの場合で、ループの本体はたったこれだけのコードになる。いやー、素晴らしいですね。人間がわざわざ考える必要なんかない。
上のloop関数を最適化してくれ、と言われたら、「あー、ループの中に乗算があるから外に追い出す?ていうか、配列からのオフセットをいちいち計算するのはよろしくないので、ポインタを増やしていく?ダウンカウンタ?」とか、考えてしまいがちなんだけど、全くそんな必要はない、そのぐらいならコンパイラが勝手にやってくれるのである。
関数呼び出しさえなければ、どの変数がどの部分で何を指してるか、とかは確実に判断できて、人間がぱっと思い付くくらいの最適化はしてくれるのだ。
「関数呼び出しの無いループでは、コンパイラを信じて、一番読みやすくなるように書くこと」、というのは頭に叩き込んでおくべきだろう。ループはちょっと弄れば最適化の効果が大きいような気がするだけに、変に色々やりたくなってしまう。けど、そういう時こそ、コンパイラを信じなくてはいけないのだ。
あとループだけでなく、関数を越えない範囲内ではコンパイラは色々最適化してくれる、というのも覚えておけば幸せになれるかもしれない。たとえば、一回目の話の、ポインタの先がどうなってるかわからない、という話の場合でも、
int alias( int *argptr ) { int a = 4; int *ptr1 = &a; int *ptr2 = argptr; int argval = *argptr; *ptr1 = 40; return *ptr2 + argval; }
ポインタの指す先がローカル変数だった場合、「ptr1はローカル変数しか指さない」、ぐらいは判断して、
alias: movl 4(%esp), %eax movl (%eax), %eax addl %eax, %eax ret
きれいに正しく最適化してくれる。コンパイラは別にいつも保守的なコードばっかり出してるわけじゃないんだよ。
と、いうわけで、まとめ。
最近のコンパイラは賢い、というのは、間違いではない。「人間が最適化したものよりも…」と、いうのはちょっと誇張してると思うけど、「人間がぱっと思い付く程度のものよりも速い」、ぐらいの能力はあるだろう。
けど、コンパイラが賢いといっても、その最適化の邪魔をするのは難しいことではない。ちょろっとポインタをいじくって関数を読んでやるだけで、簡単に最適化不可能なコードにしてしまうことができる。そして、それは、最適化技術の進歩ではどうにもならない場合が多い。
で、ここらへんを、何も考えずに、「今のコンパイラは賢いから、人間は最適化なんて考えなくていい」と、言ってもいいものだろうか。「コンパイラよりも職人がゴリゴリやるほうが最適化できる」、と決めつけてしまっていいものだろうか、という話だ。
いや、僕は、別にそれでも構わないと思ってるんだけど。人間、なにをするにしても、自分が知ってる知識の範囲内でできることを一番楽しいと思う方法で確実にやるのが大事だと思うのですよ。