Где искать сначала при оптимизации кода Scala?

В настоящее время мне нужно оптимизировать реализацию Scala алгоритма, который слишком медленный. Он реализован функционально, использует только значения (val) и неизменяемые структуры данных. Я нахожусь в точке, где я уже помнил важные функции (поэтому в моем коде есть несколько изменчивых карт), что сделало мой код вдвое быстрее, и мне интересно, что делать дальше.

Итак, я не ищу общий совет по оптимизации программного обеспечения (например, сначала оптимизируйте свой алгоритм, используйте профилировщик, делайте тесты...), а скорее для рекомендаций Scala -специфического или JVM-оптимизации.

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

В частности, я ищу совет по следующим пунктам:

  • Я читал, что конструкции for(...) медленны, потому что анонимный класс генерируется каждый раз, когда тело цикла выполняется. Это правда? Есть ли другие места, где генерируется анонимный класс? (например, при использовании map() с анонимной функцией)
  • Являются неизменяемыми коллекциями значительно медленнее, чем изменчивые коллекции в общем (особенно когда речь идет о структурах карт)?
  • Существуют ли существенные различия в производительности между Scala 2.8, 2.9 и 2.10?

Ответы

Ответ 1

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

  • Да, замена цикла for на while выполняется быстрее, даже с Scala 2.10. Подробнее об этом см. связанный разговор в комментариях. Кроме того, имейте в виду, что использование "для фильтрации" (условие, выполняемое после повторного набора) приведет к коробке/распаковке вашего состояния, что может иметь большое влияние на производительность (см. это сообщение для подробностей).

  • На вопрос об неизменяемом vs. mutable просто отвечает количество обновлений, которые вы должны выполнить, и мне сложно дать общий ответ.

  • До сих пор я не наблюдал значительных различий в производительности между 2.8, 2.9 и 2.10. Но ясно, что это зависит от проблемы. Например, если ваш алгоритм сильно использует Range.sum, вы увидите большие различия (потому что теперь это O (1) в 2.10).

  • Я заметил, что использование соответствующей Java-коллекции вместо версии Scala также может привести к значительным ускорениям (в качестве показателя шара можно сказать порядка 5-10%). Например, у меня были следующие результаты (отображаемые время выполнения) в микрообъекте для очень специфической проблемы (примечание: не обобщайте из этого, запустите свой собственный).

    ColtBitVector          min:      0.042    avg:      0.245    max:     40.120
    JavaBitSet             min:      0.043    avg:      0.165    max:      4.306
    JavaHashSet            min:      0.191    avg:      0.716    max:     12.624
    JavaTreeSet            min:      0.313    avg:      1.428    max:     64.504
    ScalaBitSetImmutable   min:      0.380    avg:      1.675    max:     13.838
    ScalaBitSetMutable     min:      0.423    avg:      3.693    max:    457.146
    ScalaSetImmutable      min:      0.458    avg:      2.305    max:      9.998
    ScalaSetMutable        min:      0.340    avg:      1.332    max:     10.974
    

    Проблема заключалась в том, чтобы вычислить простое пересечение целых множеств (с очень конкретным размером и количеством множеств). То, что я хочу продемонстрировать: выбор правильной/неправильной коллекции может оказать значительное влияние! Опять же, я думаю, что сложно дать общий совет, какой из этих типов данных выбрать, поскольку это говорит только о производительности в этой специальной проблеме пересечения (но я выбрал Java HashSet в нескольких случаях по сравнению с альтернативами). Также обратите внимание, что эта проблема пересечения не требует изменяемого типа данных. Тем не менее, могут быть различия в производительности даже в неизменяемой функциональности (и в то время как в отношении Set это была изменчивая коллекция, которая была значительно быстрее, она является неизменной для BitSet). Таким образом, в зависимости от ситуации вы можете захотеть выбрать изменяемую коллекцию по неизменному для максимальной производительности (используйте с осторожностью!).

  • Мне сказали, что объявление переменной private[this] var foo = ... предотвращает создание функций getter/setter и должно быть быстрее (отказ от ответственности: я никогда не подтверждал это в microbenchmark).

  • При работе с общими типами определение версии @specialized для определенных типов должно привести к ускорению.

  • Хотя я стараюсь избегать обобщений, я мог бы жить со следующим: Попробуйте использовать собственные массивы. Во многих моих тестах я просто использовал Массивы, что имеет смысл, учитывая их реализацию в JVM.

  • Небольшой момент, который приходит мне на ум: я наблюдал различия в построении коллекций либо origCollection.toSomeCollectionName над ручным построением и построением с использованием сопутствующего объекта (т.е. SomeCollectionName(origCollection :_*)). Во многих случаях последний был значительно быстрее.

Ответ 2

Создает ли ваш код экземпляр большого количества объектов при запуске? Scala case классы, например, или цепочки map/flatMap могут приводить к созданию огромного количества "ненужных" объектов. Это может замедлить работу кода и наложить больше работы на сборщик мусора.