そもそもなんでそんな最適化が欲しいのか

さて、ここでちょっと断りを入れておきたいことがあります。それはどのレベルの最適化をしたいのかという点です。私が言うところの最適化というのは、だいたい10〜100倍の高速化を目指すようなときです。個人的には1割速くなるためだけに、既存のインターフェースを破壊してまで最適化したいとは思いません。ただし、1年かかる処理を1日で終わらせるようにしたい上、理論的にはできる、JavaJIT最適化がボトルネックを抱いていることがわかるときだけ、この手の最適化をすることをお勧めします。たとえば、理論的にMath.logが100万回呼ばれるのがわかっていて、それが全体の60%をしめるときに、残りの処理を最適にしても2倍速には絶対になりません。逆に、本質的な計算(たとえば浮動小数点演算やMath.logなど)が全体の1割にしかみたずに、ほとんどの時間が関数呼び出しのオーバーヘッドやメモリロードにかかっていることがわかっているときはこうした最適化の意義があります。しかし、こうした状況は機械学習自然言語処理の分野では頻繁に起こります。なので、いつもPythonで書けばいいやということにはならないのですよ!


簡単な例で言うと、疎行列と密行列の内積計算のインプリを考えてください。典型的な実装は以下のようになるでしょう。

int sum = 0;
for (int i = 0; i < sparse.indices.length; i++) {
  sum += sparse.value[i] * dense.data[sparse.indices[i]];
}
return sum;

疎行列を抽象化した場合、以下のようなインプリが考えられます。

int sum = 0;
for (int i = 0, last = sparse.entrySize(); i < last; i++) {
  sum += sparse.getValue(i) * dense.data(sparse.getIndex(i));
}
return sum;

実際のgetValueとgetIndexは単なる配列アクセスです。

public class SparseVector {
  public double getValue(int i) { return values[i]; }
  public int getIndex(int i) { return indices[i]; }
...
}

もし、バイナリ素性でしたら、もっと単純です。

public class BinarySparseVector {
  public double getValue(int i) { return 1.0; }
  public int getIndex(int i) { return indices[i]; }
...
}

関数の中身は非常に軽い処理なので、当然インライン展開されて欲しくなります。されないと、単なる配列アクセス(たぶん数クロックで終わる処理)が、関数呼び出しになって悲惨です。バイナリにいたっては定数です。このレベルで単純な処理でなければ性能にクリティカルな影響を与えることはないんですが、残念ながらこういうことをしたくなることはたくさんあるのです。

C++の場合は設計の煩雑さと実行時間のトレードオフを慎重に考えながら、どこをgenericsでどこを仮想関数で実装するか綿密に考えます。ただ、クリティカルな部分はふつうgenericsにして、おかげでコンパイルエラーが謎になります。こういう部分を統一的に解決してくれる可能性のある機能、それがJIT最適化なのだと思います。