高速化とコードの読みやすさ

高速化するとコード読み辛くなる、という説があるけど、個人的には、決してそんなことは無い、と、思う。
依存関係のわかりやすいコードは、読みやすいし、最適化しやすいし、実際性能もいい。


↑のベンチマークが、何やってるか、という解説は…またそのうち書こうと思うが、こういうことを考えてる時、プログラムを依存グラフの形として考えていて、命令レベルの最適化というのは、この依存グラフの形をCPUにとって見えやすいに変換しているだけで、思考内容としては、コードをきれいに書こうという努力と、かなり同じ思考をしているのだよな。


上の nosched と nosched hazard が良い例だと思うが、

static void
__attribute__((noinline,noclone))
func_nosched(data_type *out, data_type *in, int n)
{
    int i;
    for (i=0; i<n; i+=4) {
        /* load, mul, store */
        out[i+0] = f(in[i+0]);
        out[i+1] = f(in[i+1]);
        out[i+2] = f(in[i+2]);
        out[i+3] = f(in[i+3]);
    }
}

/* ... */
    // nosched
    for (i=0; i<nloop; i++) {
        func_nosched(out, in, ndata);
    }

    // nosched hazard
    for (i=0; i<nloop; i++) {
        func_nosched(in+1, in, ndata);
    }

これで、どっちのほうが速いでしょう?というと、

 * // E350
 * | sched 3100.598877[clk/loop], 3.027929[cyc/data]
 * | nosched-hazard 14360.824951[clk/loop], 14.024243[cyc/data]

sched hazard のほうが、かなり遅い。(実際には依存があるから公平な比較ではない)


nosched-hazard は何やってるかというと、実際には、

static void
__attribute__((noinline,noclone))
func_nosched(data_type *out, data_type *in, int n)
{
    int i;
    data_type acc = in[0];
    for (i=0; i<n; i++) {
        /* load, mul, store */
        in[i+1] = acc;
        acc = f(acc);
    }
}

こんな感じのことをやっていて、つまり、ループのイテレーションごとに依存がある。

static void
__attribute__((noinline,noclone))
func_nosched(data_type *out, data_type *in, int n)
{
    int i;
    for (i=0; i<n; i+=4) {
        /* load, mul, store */
        out[i+0] = f(in[i+0]);
        out[i+1] = f(in[i+1]);
        out[i+2] = f(in[i+2]);
        out[i+3] = f(in[i+3]);
    }
}

が、それを、このコードから読み取るのは、かなり難しい。

in と、 out が同じ領域を指している、というのは、なんかデバッグしてて、よくわからん挙動をするなー、と思って、バックトレース追いかけて良く見たら同じポインタじゃねーかf**k。みたいな事件は起こりそうである。


多分、↑の説明は、よく読まないとよくわからないと思うが、つまり、そういうことで、人間によくわからないコードと、CPUによくわからないコードは、かなり、似ているということである。



最適化するしないに関わらず、何が、どういう風に依存しているのか、わかりやすいコードを書くべきである。

現代のプログラムは、リファクタリングする時/機能追加する時/並列化する時/コンパイラが自動でスケジュールする時/人間が最適化する時/CPUが命令を実行する時、あらゆる箇所で生産性、性能両方の点でボトルネックになるのは、依存性である。