Продуктивність Scala порівняно з Java


41

Перш за все, я хотів би дати зрозуміти, що це не питання "мова-X проти мови-Y", щоб визначити, що краще.

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

Моє запитання: як програмне забезпечення, написане на Scala, порівнюється з програмним забезпеченням, написаним на Java, за швидкістю виконання та витратою пам'яті? Звичайно, на це важко відповісти на загальне питання, але я би сподівався, що конструкції вищого рівня, такі як відповідність шаблонів, функції вищого порядку тощо, введуть певні накладні витрати.

Однак мій сучасний досвід роботи в Scala обмежений невеликими прикладами під 50 рядками коду, і я досі не використовував жодних орієнтирів. Отже, у мене немає реальних даних.

Якщо виявилося, що у Scala є якась накладна wrt 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 (використовуючи дальність, карту, лямбда) дійсно мінімальні, що не далеко від інформації, яку надає World Engineer.

Можливо, є інші конструкції Scala, які слід обережно використовувати, оскільки вони особливо важкі для виконання?

EDIT 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

Є одне, що ви можете зробити на Яві лаконічно та ефективно, чого не можна в Scala: перерахування. Для всього іншого, навіть для конструкцій, які повільно знаходяться в бібліотеці Scala, ви можете отримати ефективні версії, що працюють у Scala.

Отже, здебільшого вам не потрібно додавати Java у свій код. Навіть для коду, який використовує перерахування на Java, у Scala часто є рішення, яке є адекватним або хорошим - я виняток розміщую на перерахуваннях, які мають додаткові методи та чиї постійні значення використовуються.

Щодо того, на що слідкувати, ось деякі речі.

  • Якщо ви використовуєте шаблон збагачення моєї бібліотеки, завжди конвертуйте в клас. Наприклад:

    // 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 Anonymous class - це поняття Java, а не Scala. Це створює уточнення типу. Метод анонімного класу, який не замінює його базовий клас, не може бути доступний зовні. Те ж саме не стосується вдосконалення типу Scala, тому єдиний спосіб досягти цього методу - через рефлексію.
Даніель К. Собрал

Це звучить досить жахливо. Особливо: "Будьте обережні щодо методів збору - оскільки вони здебільшого поліморфні, JVM не оптимізує їх. Вам не потрібно уникати їх, але зверніть на це увагу на критичні ділянки".
мат

21

Відповідно до гри Benchmarks для одноядерної, 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
  • а) З моїх обмежених знань, я маю зазначити, що код у статичному основному методі не може бути оптимізований дуже добре. Ви повинні перемістити критичний код в інше місце.
  • б) З тривалих спостережень я б рекомендував не робити важких результатів на тесті на працездатність (за винятком саме того, що вам подобається оптимізувати, але хто коли-небудь повинен прочитати значення 2 мільйони?). Ви вимірюєте println, що не дуже цікаво. Заміна println на max:
(1 to 20000).toList.map (_ * 2).max

скорочує час з 800 мс до 20 в моїй системі.

  • в) Розуміння, як відомо, є дещо повільним (хоча ми маємо визнати, що воно постійно стає кращим). Використовуйте натомість функції функцій while або recursive. Не в цьому прикладі, де це зовнішня петля. Використовуйте анотацію @ tailrec-анотацію, щоб перевірити наявність тарекурсивності.
  • г) Порівнювати C / Assembler не вдається. Наприклад, ви не переписуєте код scala для різних архітектур. Інша важлива різниця в історичних ситуаціях
    • JIT-компілятор, що оптимізується на льоту, а може і динамічно, залежно від вхідних даних
    • Важливість кешу не вистачає
    • Зростаюче значення паралельної виклику. Сьогодні Scala має рішення для роботи без великих витрат паралельно. На Java це неможливо, хіба що ви робите набагато більше роботи.

2
Я вилучив println з циклу, і фактично код Scala швидше, ніж код Java.
Джорджіо

Порівняння з C та Assembler малося на увазі в наступному сенсі: мова вищого рівня має більш потужні абстракції, але, можливо, вам потрібно буде використовувати мову нижчого рівня для виконання. Чи має ця паралель розгляд Scala як вищого рівня, а Java як мови нижчого рівня? Можливо, ні, оскільки Scala, схоже, забезпечує продуктивність, схожу на Java.
Джорджіо

Я б не вважав, що це буде мати велике значення для Clojure або Scala, але коли я звик грати з jRuby та Jython, я б, напевно, написав критичніший код на Java. З тими двома я побачив значну невідповідність, але це було років тому назад ... могло бути і краще.
Ріг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.