Как оптимизировать эту короткую факториальную функцию в scala? (Создание 50000 BigInts)
Я сравнил версию scala
(BigInt(1) to BigInt(50000)).reduce(_ * _)
для версии python
reduce(lambda x,y: x*y, range(1,50000))
и получается, что версия scala занимает примерно 10 раз дольше, чем версия python.
Я предполагаю, большая разница в том, что python может использовать свой родной длинный тип вместо создания новых объектов BigInt для каждого числа. Но есть ли обходной путь в scala?
Ответы
Ответ 1
Тот факт, что ваш код Scala создает 50 000 BigInt
объектов, вряд ли будет иметь большое значение здесь. Большей проблемой является алгоритм умножения - Python long
использует Karatsuba умножение и Java BigInteger
(который BigInt
просто обертывает).
Простейшим решением является, вероятно, переход на лучшую математическую библиотеку с произвольной точностью, например JScience:
import org.jscience.mathematics.number.LargeInteger
(1 to 50000).foldLeft(LargeInteger.ONE)(_ times _)
Это быстрее, чем решение Python на моей машине.
Обновление: я написал некоторый быстрый код бенчмаркинга, используя Caliper в ответ на ответ Luigi Plingi, который дает следующие результаты на моей (четырехъядерной) машине:
benchmark ms linear runtime
BigIntFoldLeft 4774 ==============================
BigIntFold 4739 =============================
BigIntReduce 4769 =============================
BigIntFoldLeftPar 4642 =============================
BigIntFoldPar 500 ===
BigIntReducePar 499 ===
LargeIntegerFoldLeft 3042 ===================
LargeIntegerFold 3003 ==================
LargeIntegerReduce 3018 ==================
LargeIntegerFoldLeftPar 3038 ===================
LargeIntegerFoldPar 246 =
LargeIntegerReducePar 260 =
Я не вижу разницы между reduce
и fold
, что он делает, но мораль ясна: если вы можете использовать Scala 2.9 параллельные коллекции, они дадут вам огромное улучшение, но переключение to LargeInteger
также помогает.
Ответ 2
Python на моей машине:
def func():
start= time.clock()
reduce(lambda x,y: x*y, range(1,50000))
end= time.clock()
t = (end-start) * 1000
print t
дает 1219 ms
Scala:
def timed[T](f: => T) = {
val t0 = System.currentTimeMillis
val r = f
val t1 = System.currentTimeMillis
println("Took: "+(t1 - t0)+" ms")
r
}
timed { (BigInt(1) to BigInt(50000)).reduce(_ * _) }
4251 ms
timed { (BigInt(1) to BigInt(50000)).fold(BigInt(1))(_ * _) }
4224 ms
timed { (BigInt(1) to BigInt(50000)).par.reduce(_ * _) }
2083 ms
timed { (BigInt(1) to BigInt(50000)).par.fold(BigInt(1))(_ * _) }
689 ms
// using org.jscience.mathematics.number.LargeInteger from Travis answer
timed { val a = (1 to 50000).foldLeft(LargeInteger.ONE)(_ times _) }
3327 ms
timed { val a = (1 to 50000).map(LargeInteger.valueOf(_)).par.fold(
LargeInteger.ONE)(_ times _) }
361 ms
Это 689 мс и 361 мс после нескольких разминок. Они оба начались примерно в 1000 мс, но, похоже, разогреваются разным количеством. Параллельные коллекции, по-видимому, прогреваются значительно больше, чем непараллельные: непараллельные операции существенно не уменьшались с их первых прогонов.
.par
(что означает использование параллельных коллекций), казалось бы, ускорило fold
больше, чем reduce
. У меня только 2 ядра, но большее количество ядер должно видеть большее увеличение производительности.
Итак, экспериментально, способ оптимизации этой функции -
a) Используйте fold
, а не reduce
b) Использование параллельных коллекций
обновление:
Вдохновленный замечанием о том, что разбивка расчёта на более мелкие куски ускоряет работу, мне удалось заставить его следовать за 215 ms
на моей машине, что на 40% больше по сравнению с стандартным параллелизованным алгоритмом. (Использование BigInt занимает 615 мс.) Кроме того, он не использует параллельные коллекции, но как-то использует 90% процессор (в отличие от BigInt).
import org.jscience.mathematics.number.LargeInteger
def fact(n: Int) = {
def loop(seq: Seq[LargeInteger]): LargeInteger = seq.length match {
case 0 => throw new IllegalArgumentException
case 1 => seq.head
case _ => loop {
val (a, b) = seq.splitAt(seq.length / 2)
a.zipAll(b, LargeInteger.ONE, LargeInteger.ONE).map(i => i._1 times i._2)
}
}
loop((1 to n).map(LargeInteger.valueOf(_)).toIndexedSeq)
}
Ответ 3
Другим трюком здесь может быть попытка как reduceLeft
, так и reduceRight
, чтобы узнать, что быстрее. На вашем примере я получаю гораздо более быстрое выполнение reduceRight
:
scala> timed { (BigInt(1) to BigInt(50000)).reduceLeft(_ * _) }
Took: 4605 ms
scala> timed { (BigInt(1) to BigInt(50000)).reduceRight(_ * _) }
Took: 2004 ms
Та же разница между foldLeft
и foldRight
. Угадайте, какова сторона, с которой вы начинаете сокращаться:)
Ответ 4
Наиболее эффективным способом вычисления факториала в Scala является использование стратегии разделения и завоевания:
def fact(n: Int): BigInt = rangeProduct(1, n)
private def rangeProduct(n1: Long, n2: Long): BigInt = n2 - n1 match {
case 0 => BigInt(n1)
case 1 => BigInt(n1 * n2)
case 2 => BigInt(n1 * (n1 + 1)) * n2
case 3 => BigInt(n1 * (n1 + 1)) * ((n2 - 1) * n2)
case _ =>
val nm = (n1 + n2) >> 1
rangeProduct(n1, nm) * rangeProduct(nm + 1, n2)
}
Также для получения большей скорости используйте последнюю версию JDK и следующие параметры JVM:
-server -XX:+TieredCompilation
Ниже приведены результаты для Intel (R) Core (TM) i7-2640M CPU @2.80GHz (max 3.50GHz), RAM 12Gb DDR3-1333, Windows 7 sp1, Oracle JDK 1.8.0_25-b18 64-бит:
(BigInt(1) to BigInt(100000)).product took: 3,806 ms with 26.4 % of CPU usage
(BigInt(1) to BigInt(100000)).reduce(_ * _) took: 3,728 ms with 25.4 % of CPU usage
(BigInt(1) to BigInt(100000)).reduceLeft(_ * _) took: 3,510 ms with 25.1 % of CPU usage
(BigInt(1) to BigInt(100000)).reduceRight(_ * _) took: 4,056 ms with 25.5 % of CPU usage
(BigInt(1) to BigInt(100000)).fold(BigInt(1))(_ * _) took: 3,697 ms with 25.5 % of CPU usage
(BigInt(1) to BigInt(100000)).par.product took: 406 ms with 66.3 % of CPU usage
(BigInt(1) to BigInt(100000)).par.reduce(_ * _) took: 296 ms with 71.1 % of CPU usage
(BigInt(1) to BigInt(100000)).par.reduceLeft(_ * _) took: 3,495 ms with 25.3 % of CPU usage
(BigInt(1) to BigInt(100000)).par.reduceRight(_ * _) took: 3,900 ms with 25.5 % of CPU usage
(BigInt(1) to BigInt(100000)).par.fold(BigInt(1))(_ * _) took: 327 ms with 56.1 % of CPU usage
fact(100000) took: 203 ms with 28.3 % of CPU usage
BTW для повышения эффективности факториального расчета для чисел, которые более 20000 используют после реализации алгоритма Шенхаге-Штрассена или дождаться его объединения к JDK 9 и Scala сможет использовать его