Я повторно реализую некоторый код (простой алгоритм байесовского вывода, но это не очень важно) с 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);
}
}
Довольно просто, правда? Поэтому я решил использовать методы Scala foldLeft и map для моей первой попытки. Поскольку у меня есть два значения, которые я накапливаю, аккумулятор представляет собой кортеж:
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
})
К сожалению, этот код работает примерно в 5 раз медленнее, чем Java (с использованием простой и неточной метрики; просто вызвал код 10000 раз в цикле). Один недостаток довольно очевиден; мы просматриваем списки дважды: один раз при вызове map, а другой - в foldLeft. Итак, вот версия, которая проходит по списку один раз.
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
})
Так лучше! Он работает примерно в 3 раза хуже, чем код Java.Моя следующая догадка заключалась в том, что создание всех новых кортежей на каждом этапе свертки, вероятно, требует определенных затрат. Поэтому я решил попробовать версию, которая дважды просматривает список, но без создания кортежей.
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
})
Это работает примерно так же, как и предыдущая версия (в 3 раза медленнее, чем версия для Java). На самом деле это не удивительно, но я был полон надежд.
У меня вопрос: есть ли более быстрый способ реализовать этот фрагмент Java в Scala, сохранив при этом чистый код Scala, избегая ненужной изменчивости и следуя идиомам Scala? Я действительно ожидаю использовать этот код в конечном итоге в параллельной среде, поэтому ценность сохранения неизменяемости может перевесить более низкую производительность в одном потоке.