Производительность Scala по сравнению с Java

41

Прежде всего, я хотел бы прояснить, что это не вопрос «язык против языка», чтобы определить, что лучше.

Я давно использую Java и намерен продолжать его использовать. Параллельно с этим я в настоящее время изучаю Scala с большим интересом: кроме незначительных вещей, которые требуют некоторого привыкания, у меня сложилось впечатление, что я действительно могу очень хорошо работать на этом языке.

Мой вопрос: как программное обеспечение, написанное на Scala, сравнивается с программным обеспечением, написанным на Java, с точки зрения скорости выполнения и потребления памяти? Конечно, на этот вопрос в целом сложно ответить, но я ожидаю, что конструкции более высокого уровня, такие как сопоставление с образцом, функции высшего порядка и т. Д., Вносят некоторые накладные расходы.

Тем не менее, мой текущий опыт работы с Scala ограничен небольшими примерами под 50 строками кода, и я до сих пор не выполнил никаких тестов. Итак, у меня нет реальных данных.

Если оказалось, что в Scala есть некоторые накладные расходы по сравнению с Java, имеет ли смысл смешивать проекты Scala / Java, где можно кодировать более сложные части в Scala и критические по производительности части в Java? Это обычная практика?

РЕДАКТИРОВАТЬ 1

Я выполнил небольшой тест: построил список целых чисел, умножил каждое целое число на два и поместил его в новый список, распечатал полученный список. Я написал реализацию Java (Java 6) и реализацию Scala (Scala 2.9). Я запустил оба на Eclipse Indigo под Ubuntu 10.04.

Результаты сопоставимы: 480 мс для Java и 493 мс для Scala (в среднем за 100 итераций). Вот фрагменты, которые я использовал.

// Java
public static void main(String[] args)
{
    long total = 0;
    final int maxCount = 100;
    for (int count = 0; count < maxCount; count++)
    {
        final long t1 = System.currentTimeMillis();

        final int max = 20000;
        final List<Integer> list = new ArrayList<Integer>();
        for (int index = 1; index <= max; index++)
        {
            list.add(index);
        }

        final List<Integer> doub = new ArrayList<Integer>();
        for (Integer value : list)
        {
            doub.add(value * 2);
        }

        for (Integer value : doub)
        {
            System.out.println(value);
        }

        final long t2 = System.currentTimeMillis();

        System.out.println("Elapsed milliseconds: " + (t2 - t1));
        total += t2 - t1;
    }

    System.out.println("Average milliseconds: " + (total / maxCount));
}

// Scala
def main(args: Array[String])
{
    var total: Long = 0
    val maxCount    = 100
    for (i <- 1 to maxCount)
    {
        val t1   = System.currentTimeMillis()
        val list = (1 to 20000) toList
        val doub = list map { n: Int => 2 * n }

        doub foreach ( println )

        val t2 = System.currentTimeMillis()

        println("Elapsed milliseconds: " + (t2 - t1))
        total = total + (t2 - t1)
    }

    println("Average milliseconds: " + (total / maxCount))
}

Таким образом, в этом случае кажется, что издержки Scala (с использованием range, map, lambda) действительно минимальны, что недалеко от информации, предоставленной World Engineer.

Может быть, есть другие конструкции Scala, которые следует использовать с осторожностью, потому что они особенно тяжелы для выполнения?

РЕДАКТИРОВАТЬ 2

Некоторые из вас отметили, что println во внутренних циклах занимают большую часть времени выполнения. Я удалил их и установил размер списков 100000 вместо 20000. В результате среднее значение составило 88 мс для Java и 49 мс для Scala.

Джорджио
источник
5
Я предполагаю, что, поскольку Scala компилируется в байтовый код JVM, то теоретически производительность может быть эквивалентна Java, работающему под той же JVM, при прочих равных условиях. Разница, на мой взгляд, заключается в том, как компилятор Scala создает байт-код и делает ли он это эффективно.
maple_shaft
2
@maple_shaft: А может, во время компиляции в Scala есть издержки?
FrustratedWithFormsDesigner
1
@Giorgio Не существует различий во время выполнения между объектами Scala и объектами Java, все они являются объектами JVM, которые определены и ведут себя в соответствии с байтовым кодом. Например, Scala как язык имеет концепцию замыканий, но когда они компилируются, они компилируются в ряд классов с байтовым кодом. Теоретически, я мог бы физически написать код Java, который мог бы компилироваться с точно таким же байтовым кодом, и поведение во время выполнения было бы точно таким же.
maple_shaft
2
@maple_shaft: Это именно то, к чему я стремлюсь: я нахожу вышеупомянутый код Scala намного более кратким и читаемым, чем соответствующий код Java. Мне просто интересно, имеет ли смысл писать части проекта Scala на Java по соображениям производительности и какими будут эти части.
Джорджио
2
Время выполнения будет в основном занято вызовами println. Вам нужен более интенсивный компьютерный тест.
Кевин Клайн

Ответы:

39

Есть одна вещь, которую вы можете сделать кратко и эффективно в Java, чего нельзя делать в Scala: перечисления. Для всего остального, даже для медленных конструкций в библиотеке Scala, вы можете получить эффективные версии, работающие в Scala.

Таким образом, по большей части вам не нужно добавлять Java в ваш код. Даже для кода, использующего перечисления в Java, в Scala часто есть решение, которое является адекватным или хорошим - я помещаю исключение в перечисления, которые имеют дополнительные методы и чьи значения констант int используются.

Что касается того, что нужно остерегаться, вот некоторые вещи.

  • Если вы используете шаблон обогащения моей библиотеки, всегда конвертируйте в класс. Например:

    // WRONG -- the implementation uses reflection when calling "isWord"
    implicit def toIsWord(s: String) = new { def isWord = s matches "[A-Za-z]+" }
    
    // RIGHT
    class IsWord(s: String) { def isWord = s matches "[A-Za-z]+" }
    implicit def toIsWord(s: String): IsWord = new IsWord(s)
  • Остерегайтесь методов сбора данных - поскольку они большей частью полиморфны, JVM не оптимизирует их. Вам не нужно избегать их, но обратите внимание на это на критических разделах. Имейте в виду, что forв Scala реализованы вызовы методов и анонимные классы.

  • Если вы используете класс Java, например String, Arrayили AnyValклассы, соответствующие примитивам Java, предпочтите методы, предоставляемые Java, когда существуют альтернативы. Например, используйте lengthна Stringи Arrayвместо size.

  • Избегайте небрежного использования неявных преобразований, поскольку вы можете использовать преобразования по ошибке, а не по замыслу.

  • Расширьте классы вместо черт. Например, если вы расширяете Function1, расширьте AbstractFunction1вместо этого.

  • Использование -optimiseи специализация, чтобы получить большую часть Scala.

  • Поймите, что происходит: javapваш друг, и так же куча флагов Scala, которые показывают, что происходит.

  • Идиомы Scala предназначены для улучшения правильности и делают код более лаконичным и понятным. Они не предназначены для скорости, поэтому, если вам нужно использовать nullвместо Optionкритического пути, сделайте это! Есть причина, по которой Scala является мультипарадигмой.

  • Помните, что истинной мерой производительности является выполнение кода. Посмотрите на этот вопрос пример того, что может произойти, если вы игнорируете это правило.

Даниэль С. Собрал
источник
1
+1: много полезной информации, даже по темам, которые мне еще предстоит изучить, но полезно прочитать некоторые подсказки, прежде чем я смогу на них взглянуть.
Джорджио
Почему первый подход использует отражение? В любом случае он генерирует анонимный класс, так почему бы не использовать его вместо отражения?
Александр
@ Oleksandr.Bezhan Анонимный класс - это концепция Java, а не Scala. Он генерирует уточнение типа. К анонимному методу класса, который не переопределяет свой базовый класс, нельзя получить доступ извне. То же самое не относится к уточнениям типов в Scala, поэтому единственный способ получить этот метод - это отражение.
Даниэль С. Собрал,
Это звучит довольно ужасно. В частности: «Остерегайтесь методов сбора - поскольку они большей частью полиморфны, JVM не оптимизирует их. Вам не нужно избегать их, но обращайте внимание на это в критических разделах».
Мэтт
21

Согласно игре Benchmarks Game для одноядерной 32-битной системы, Scala в среднем на 80% быстрее, чем Java. Производительность примерно одинакова для компьютера Quad Core x64. Даже использование памяти и плотность кода в большинстве случаев очень похожи. Я бы сказал, основываясь на этом (довольно ненаучном) анализе, что вы правы, утверждая, что Scala добавляет некоторые издержки Java. Похоже, что это не добавляет тонны накладных расходов, поэтому я подозреваю, что диагноз предметов более высокого порядка, занимающих больше места / времени, является наиболее правильным.

Мировой инженер
источник
2
Для этого ответа , пожалуйста , просто использовать прямое сравнение , как страница Помощь предполагает ( shootout.alioth.debian.org/help.php#comparetwo )
igouy
18
  • Производительность Scala очень приличная, если вы просто пишете Java / C-подобный код в Scala. Компилятор будет использовать JVM примитивы для Int, Charи т.д. , когда это возможно. В то время как циклы так же эффективны в Scala.
  • Имейте в виду, что лямбда-выражения компилируются в экземпляры анонимных подклассов Functionклассов. Если вы передаете лямбда map-выражение, необходимо создать экземпляр анонимного класса (и, возможно, придется передать некоторые локальные объекты), а затем каждая итерация будет иметь дополнительные издержки вызова функции (с передачей некоторых параметров) от applyвызовов.
  • Многие классы вроде scala.util.Randomявляются просто обертками вокруг эквивалентных классов JRE. Дополнительный вызов функции является немного расточительным.
  • Следите за последствиями в критичном для производительности коде. java.lang.Math.signum(x)гораздо более прямой, чем x.signum(), который преобразует в RichIntи обратно.
  • Основным преимуществом Scala по сравнению с Java является специализация. Имейте в виду, что специализация редко используется в библиотечном коде.
Даниэль Любаров
источник
5
  • a) Из моих ограниченных знаний я должен отметить, что код в методе static main нельзя оптимизировать очень хорошо. Вы должны переместить критический код в другое место.
  • б) Из долгих наблюдений я бы порекомендовал не делать значительных результатов при тестировании производительности (за исключением того, что это именно то, что вы хотели бы оптимизировать, но кто должен когда-либо читать значения 2 миллиона?). Вы измеряете println, что не очень интересно. Замена println с max:
(1 to 20000).toList.map (_ * 2).max

сокращает время с 800 мс до 20 в моей системе.

  • в) Известно, что понимание - немного медленное (хотя мы должны признать, что оно все время улучшается). Вместо этого используйте while или tailrecursive функции. Не в этом примере, где это внешний цикл. Используйте @ tailrec-annotation, чтобы проверить наличие тайркурсивности.
  • г) Сравнивать с С / Ассемблером не получается. Например, вы не переписываете scala-код для разных архитектур. Другое важное отличие от исторических ситуаций
    • JIT-компилятор, оптимизирующий на лету, а может и динамически, в зависимости от входных данных
    • Важность промахов кеша
    • Растущая важность параллельного вызова. Сегодня Scala предлагает решения для параллельной работы без особых накладных расходов. Это не возможно в Java, за исключением того, что вы делаете намного больше работы.
неизвестный пользователь
источник
2
Я удалил println из цикла, и на самом деле код Scala работает быстрее, чем код Java.
Джорджио
Сравнение с C и Ассемблером подразумевалось в следующем смысле: язык более высокого уровня имеет более мощные абстракции, но вам может потребоваться использовать язык более низкого уровня для производительности. Сохраняется ли эта параллель, рассматривая Scala как язык более высокого уровня и Java как язык более низкого уровня? Возможно, нет, так как кажется, что Scala обеспечивает производительность, аналогичную Java.
Джорджио
Я бы не подумал, что это будет иметь большое значение для Clojure или Scala, но когда я привык играть с jRuby и Jython, я, вероятно, написал бы более критичный для производительности код на Java. С этими двумя я видел значительное несоответствие, но это было много лет назад ... могло бы быть и лучше.
Rig