Итак, Scala должна быть такой же быстрой, как Java. Я возвращаюсь к некоторым проблемам Project Euler в Scala, которые изначально решал на Java. В частности, проблема 5: «Какое наименьшее положительное число делится без остатка на все числа от 1 до 20?»
Вот мое решение Java, выполнение которого на моем компьютере занимает 0,7 секунды:
public class P005_evenly_divisible implements Runnable{
final int t = 20;
public void run() {
int i = 10;
while(!isEvenlyDivisible(i, t)){
i += 2;
}
System.out.println(i);
}
boolean isEvenlyDivisible(int a, int b){
for (int i = 2; i <= b; i++) {
if (a % i != 0)
return false;
}
return true;
}
public static void main(String[] args) {
new P005_evenly_divisible().run();
}
}
Вот мой «прямой перевод» на Scala, который занимает 103 секунды (в 147 раз дольше!)
object P005_JavaStyle {
val t:Int = 20;
def run {
var i = 10
while(!isEvenlyDivisible(i,t))
i += 2
println(i)
}
def isEvenlyDivisible(a:Int, b:Int):Boolean = {
for (i <- 2 to b)
if (a % i != 0)
return false
return true
}
def main(args : Array[String]) {
run
}
}
Наконец, вот моя попытка функционального программирования, которая занимает 39 секунд (в 55 раз дольше).
object P005 extends App{
def isDivis(x:Int) = (1 to 20) forall {x % _ == 0}
def find(n:Int):Int = if (isDivis(n)) n else find (n+2)
println (find (2))
}
Использование Scala 2.9.0.1 в 64-битной Windows 7. Как мне улучшить производительность? Я делаю что-то неправильно? Или Java просто намного быстрее?
java
performance
scala
for-loop
while-loop
Луиджи Плинге
источник
источник
run
метод?Ответы:
Проблема в этом конкретном случае заключается в том, что вы возвращаетесь из выражения for. Это, в свою очередь, преобразуется в выброс NonLocalReturnException, который перехватывается включающим методом. Оптимизатор может исключить foreach, но еще не может устранить выброс / уловку. И бросить / поймать дорого. Но поскольку такие вложенные возвраты редко встречаются в программах Scala, оптимизатор еще не рассмотрел этот случай. Продолжается работа по улучшению оптимизатора, который, надеюсь, скоро решит эту проблему.
источник
Проблема, скорее всего, в использовании
for
понимания в методеisEvenlyDivisible
. Заменаfor
на эквивалентныйwhile
цикл должна устранить разницу в производительности с Java.В отличие от
for
циклов Java ,for
понимания Scala на самом деле являются синтаксическим сахаром для методов более высокого порядка; в этом случае вы вызываетеforeach
методRange
объекта. Scalafor
является очень общим, но иногда приводит к болезненной работе.Вы можете попробовать
-optimize
флаг в Scala версии 2.9. Наблюдаемая производительность может зависеть от конкретной используемой JVM и наличия у JIT-оптимизатора достаточного времени "прогрева" для выявления и оптимизации горячих точек.Недавние обсуждения в списке рассылки показывают, что команда Scala работает над улучшением
for
производительности в простых случаях:Проблема в трекере ошибок: https://issues.scala-lang.org/browse/SI-4633
Обновление 28.05 :
while
циклов.источник
for
тогда подходит?В качестве продолжения я попробовал флаг -optimize, и он сократил время работы с 103 до 76 секунд, но это все равно в 107 раз медленнее, чем у Java или цикла while.
Потом смотрел «функциональную» версию:
и пытаемся понять, как лаконично избавиться от "forall". Я с треском провалился и придумал
благодаря чему мое хитрое решение с 5 строками превратилось в 12 строк. Однако эта версия работает за 0,71 секунды , с той же скоростью, что и исходная версия Java, и в 56 раз быстрее, чем версия выше с использованием «forall» (40,2 с)! (см. ИЗМЕНИТЬ ниже, почему это быстрее, чем Java)
Очевидно, моим следующим шагом было перевести вышесказанное обратно в Java, но Java не может с этим справиться и выдает StackOverflowError с n около отметки 22000.
Затем я немного почесал в затылке и заменил "while" немного большей рекурсией хвоста, которая экономит пару строк, работает так же быстро, но давайте посмотрим правде в глаза, читать сложнее:
Итак, хвостовая рекурсия в Scala побеждает, но я удивлен, что что-то столь же простое, как цикл for (и метод forall) по существу не работает и должно быть заменено неэлегантным и многословным while или хвостовой рекурсией. , Во многом я пробую Scala из-за лаконичного синтаксиса, но это плохо, если мой код будет работать в 100 раз медленнее!
РЕДАКТИРОВАТЬ : (удалено)
РЕДАКТИРОВАНИЕ РЕДАКТИРОВАНИЯ : прежние расхождения между временем выполнения 2,5 с и 0,7 с полностью связаны с тем, использовались ли 32-разрядные или 64-разрядные JVM. Scala из командной строки использует все, что установлено JAVA_HOME, а Java использует 64-битную версию, если она доступна. В IDE есть свои настройки. Некоторые измерения здесь: время выполнения Scala в Eclipse
источник
def isDivis(x: Int, i: Int): Boolean = if (i > 20) true else if (x % i != 0) false else isDivis(x, i+1)
. Обратите внимание, что в Scala if-else - это выражение, которое всегда возвращает значение. Здесь нет необходимости в ключевом слове return.P005_V3
) можно сделать короче, декларативнее и, ИМХО, яснее, написав:def isDivis(x: Int, i: Int): Boolean = (i > 20) || (x % i == 0) && isDivis(x, i+1)
Ответ насчет понимания верен, но это еще не все. Обратите внимание, что использование
return
inisEvenlyDivisible
не является бесплатным. Использование return внутриfor
, заставляет компилятор scala генерировать нелокальный возврат (то есть возвращать вне его функции).Это делается с помощью исключения для выхода из цикла. То же самое происходит, если вы создаете свои собственные абстракции элементов управления, например:
Привет только один раз.
Обратите внимание, что
return
infoo
выходитfoo
(чего и следовало ожидать). Поскольку выражение в квадратных скобках является функциональным литералом, который вы можете видеть в сигнатуре,loop
это заставляет компилятор генерировать нелокальный возврат, то естьreturn
вынуждает вас выйтиfoo
, а не толькоbody
.В Java (т.е. JVM) единственный способ реализовать такое поведение - это создать исключение.
Возвращаясь к
isEvenlyDivisible
:Это
if (a % i != 0) return false
функциональный литерал, который имеет возврат, поэтому каждый раз, когда происходит возврат, среда выполнения должна генерировать и перехватывать исключение, что вызывает довольно много накладных расходов GC.источник
Некоторые способы ускорить
forall
обнаруженный мною метод:Оригинал: 41,3 с
Предварительное создание экземпляра диапазона, поэтому мы не создаем каждый раз новый диапазон: 9,0 с
Преобразование в список вместо диапазона: 4,8 с
Я пробовал несколько других коллекций, но List был самым быстрым (хотя все еще в 7 раз медленнее, чем если бы мы вообще избегали Range и функции высшего порядка).
Хотя я новичок в Scala, я предполагаю, что компилятор может легко реализовать быстрый и значительный прирост производительности, просто автоматически заменив литералы Range в методах (как указано выше) на константы Range во внешней области. Или лучше, интернируйте их, как литералы String в Java.
сноска : Массивы были примерно такими же, как и Range, но, что интересно, использование нового
forall
метода (показанного ниже) привело к ускорению выполнения на 24% в 64-битной версии и на 8% быстрее в 32-битной. Когда я уменьшил размер вычислений, уменьшив количество факторов с 20 до 15, разница исчезла, так что, возможно, это эффект сборки мусора. Какой бы ни была причина, она имеет значение при длительной работе с полной нагрузкой.Подобный сутенер для List также привел к повышению производительности примерно на 10%.
источник
Я просто хотел прокомментировать для людей, которые могут потерять веру в Scala из-за подобных проблем, что такого рода проблемы возникают при работе практически всех функциональных языков. Если вы оптимизируете свертку в Haskell, вам часто придется переписывать ее как рекурсивный цикл, оптимизированный для хвостового вызова, иначе у вас возникнут проблемы с производительностью и памятью, с которыми придется бороться.
Я знаю, что очень жаль, что FP еще не оптимизированы до такой степени, что нам не нужно думать о подобных вещах, но это вовсе не проблема Scala.
источник
Проблемы, характерные для Scala, уже обсуждались, но основная проблема заключается в том, что использование алгоритма грубой силы не очень круто. Учтите это (намного быстрее, чем исходный код Java):
источник
Попробуйте однострочник, указанный в решении Scala для Project Euler
Указанное время, по крайней мере, быстрее, чем у вас, хотя и далеко от цикла while .. :)
источник
def r(n:Int):Int = if ((1 to 20) forall {n % _ == 0}) n else r (n+2); r(2)
, что на 4 символа короче, чем у Павла. :) Однако я не претендую на то, чтобы мой код был хорош - когда я опубликовал этот вопрос, я закодировал в общей сложности около 30 строк Scala.