JavaからScalaにいくつかのコード(単純なベイズ推定アルゴリズムですが、それはそれほど重要ではありません)を再実装しています。可能な限り可変性を回避することでコードをクリーンで機能的に保ちながら、可能な限りパフォーマンスの高い方法で実装したいと思います。
Javaコードのスニペットは次のとおりです。
// initialize
double lP = Math.log(prior);
double lPC = Math.log(1-prior);
// accumulate probabilities from each annotation object into lP and lPC
for (Annotation annotation : annotations) {
float prob = annotation.getProbability();
if (isValidProbability(prob)) {
lP += logProb(prob);
lPC += logProb(1 - prob);
}
}
とても簡単ですよね?そこで、最初の試行でScalafoldLeftメソッドとmapメソッドを使用することにしました。累積する値が2つあるため、アキュムレータはタプルです。
val initial = (math.log(prior), math.log(1-prior))
val probs = annotations map (_.getProbability)
val (lP,lPC) = probs.foldLeft(initial) ((r,p) => {
if(isValidProbability(p)) (r._1 + logProb(p), r._2 + logProb(1-p)) else r
})
残念ながら、このコードのパフォーマンスはJavaの約5倍遅くなります(単純で不正確なメトリックを使用します。ループ内でコードを10000回呼び出すだけです)。1つの欠陥はかなり明らかです。リストを2回トラバースしています。1つはmapの呼び出しで、もう1つはfoldLeftでです。これが、リストを1回トラバースするバージョンです。
val (lP,lPC) = annotations.foldLeft(initial) ((r,annotation) => {
val p = annotation.getProbability
if(isValidProbability(p)) (r._1 + logProb(p), r._2 + logProb(1-p)) else r
})
これの方が良い!Javaコードの約3倍のパフォーマンスを発揮します。私の次の予感は、フォールドの各ステップですべての新しいタプルを作成するのにおそらくいくらかのコストがかかるということでした。そこで、タプルを作成せずに、リストを2回トラバースするバージョンを試すことにしました。
val lP = annotations.foldLeft(math.log(prior)) ((r,annotation) => {
val p = annotation.getProbability
if(isValidProbability(p)) r + logProb(p) else r
})
val lPC = annotations.foldLeft(math.log(1-prior)) ((r,annotation) => {
val p = annotation.getProbability
if(isValidProbability(p)) r + logProb(1-p) else r
})
これは、以前のバージョンとほぼ同じように機能します(Javaバージョンの3倍遅い)。それほど驚くことではありませんが、私は希望を持っていました。
だから私の質問は、Scalaコードをクリーンに保ち、不必要な可変性を避け、Scalaイディオムに従う一方で、このJavaスニペットをScalaに実装するより速い方法はありますか?最終的にはこのコードを並行環境で使用することを期待しているため、不変性を維持することの価値は、単一スレッドでのパフォーマンスの低下を上回る可能性があります。