Scala performanceпод капотом
Гребенников Романsociohub.ru@public_void_grv
2015, jpoint
Intro: Зачем это всё?
● В Scala много “необычного”:○ FP, pattern matching, лень, коллекции.○ Всё такое удобное и классное.
Intro: Зачем это всё?
● В Scala много “необычного”:○ FP, pattern matching, лень, коллекции.○ Всё такое удобное и классное.
Intro: горячее необычное
● ФП в горячих участках кода:○ улучшает читабельность;○ добавляет непредсказуемости.
Intro: горячее необычное
● ФП в горячих участках кода:○ улучшает читабельность;○ добавляет непредсказуемости.
● ФП-абстракции могут:○ неявно создавать новые объекты;○ делать дополнительные вычисления;○ портить жизнь JVM JIT.
О чем доклад?
● Основы:○ Измерять производительность сложно.○ Что делает HotSpot и scalac под капотом.○ JMH и как (не)правильно измерять.
● Scala в реальной жизни:○ pattern matching;○ рекурсия;○ коллекции и лямбды.
● Как с этим жить.
А что тут сложного?
А что тут сложного?
● Что тут не так?○ цикл может быть удалён оптимизирован целиком;○ doSomeStuff может быть скомпилирован позже
1000-й итерации;○ весь цикл может занять меньше 1мс;○ и еще миллион проблем.
А что тут сложного?
● Что тут не так?○ цикл может быть удалён оптимизирован целиком;○ doSomeStuff может быть скомпилирован позже
1000-й итерации;○ весь цикл может занять меньше 1мс;○ и еще миллион проблем.
● Причина: HotSpot умнее тебя
Внутри HotSpot
Foo.scala
Foo.java
scalac
javac
Внутри HotSpot
Foo.scala
Foo.java
scalac
javac
МАГИЯ
Внутри HotSpot
Foo.scala
Foo.java
scalac
javac
МАГИЯ
Внутри HotSpot
● С0 -> C1 -> C2:○ агрессивнее оптимизации ~ быстрее код;○ больше времени на разогрев.
● Множество опасностей на каждом шагу
Foo.scala
Foo.java
scalac
javacJVM/HotSpot
Bytecode
CPUС0: интерпретация
С1
С2маш.код
Почему не надо делать это руками
● Надо обойти много ловушек JVM, чтобы получить достоверный результат:○ корректно измерять время;○ бороться с dead-code-elimination;○ избегать constant-folding;○ победить loop-unrolling;○ и еще два десятка особенностей.
Почему не надо делать это руками
● Надо обойти много ловушек JVM, чтобы получить достоверный результат:○ корректно измерять время;○ бороться с dead-code-elimination;○ избегать constant-folding;○ победить loop-unrolling;○ и еще два десятка особенностей.
● Не надо изобретать велосипед
JMH[1]
Harness для написания и запуска (микро) бенчмарков:● набор аннотаций;● консольный интерфейс для запуска;
[1]: http://openjdk.java.net/projects/code-tools/jmh/
JMH и Scala
● плагин sbt-jmh [1]
● запускается из консоли sbt
[1]: https://github.com/ktoso/sbt-jmh
Если хочется подробностей
● Алексей Шипилёв:○ Performance Methodology How-To[1];○ Java Benchmarking, Timestamping Failures[2];○ Nanotrusting the Nanotime[3].
● JMH code samples[4]
[1]: http://shipilev.net/#performance-101[2]: http://shipilev.net/#benchmarking[3]: http://shipilev.net/blog/2014/nanotrusting-nanotime/
Pattern matching
● сопоставление с образцом: ○ switch-case на стероидах;○ экстракторы, regex, списки и т.п.○ гордость ФП-фанатов, повод унижать Java.
Pattern matching
● сопоставление с образцом: ○ switch-case на стероидах;○ экстракторы, regex, списки и т.п.○ гордость ФП-фанатов, повод унижать Java.
● условия для тестовой задачи:○ часто используется в реальной жизни;○ простота и минимум дополнительной логики.
Выбор из нескольких вариантов
● базовый трейт и несколько наследников
● задача:○ def select(value:Base)○ какой дочерний тип реализует Base?
Есть две реализации...
● с матчингом и цепочкой сравнений:
Результаты
● результаты выглядят одинаково● но одинаково ли то, что внутри?
Заглянем в машинный код
Это даст нам все ответы. Наверное.
Заглянем в машинный код
Это даст нам все ответы. Наверное.
Проблема:● я не соображаю в x86_64 ассемблере;● но я неплохо делаю вид, что соображаю[1].
[1]: Wikibooks: x86 Disassembly, http://en.wikibooks.org/wiki/X86_Disassembly
JMH perfasm profiler
● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами:○ много буков, тяжело читать.
JMH perfasm profiler
● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами:○ много буков, тяжело читать.
● perf, CPU performance counters[1]:○ интерфейс ядра Linux;○ надо следить за изменениями.
JMH perfasm profiler
● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами:○ много буков, тяжело читать.
● perf, CPU performance counters[1]:○ интерфейс ядра Linux;○ надо следить за изменениями.
● perfasm парсит выхлоп JVM+perf, а потом:○ выделяет горячие регионы кода;○ сводит их в единый человекочитаемый вид.
[1]: https://perf.wiki.kernel.org/index.php/Main_Page
def measurePatternMatch(v:Baz)
def measurePatternMatch(v:Baz)
def measurePatternMatch(v:Baz)
● указатель на структуру-описание класса● по смещению 8 лежит classword
def measurePatternMatch(v:Baz)
● cmp: Compare, сравнить два значения.● je: Jump-if-equals, перейти, если равны.
def measurePatternMatch(v:Baz)
● jne: Jump-if-Not-Equals● учтён профиль выполнения кода
def measurePatternMatch(v:Baz)
● готовим аргументы для вызова consume()● проглатываем результат $0x3
def measurePatternMatch(v:Baz)
def measureIf(v:Baz)
If-else vs match
Идентичный машинный код:
Option[T]
● типизированная замена null-check● Option[T]:
○ Some(x:T) - если что-то есть,○ None - если ничего нет.
● scalac не даст сконвертить Option[T] => T● явная обработка None
Option[T] pattern matching
Option[T]: Результаты
● Задача и результаты весьма близки● Но null-check чуть быстрее● Как же так?
* - Scala 2.11.6, JMH 1.8, Oracle JDK 1.8_40
Внутри measureIfNull
Загрузить nullableString в %r11d
Если в %r11d лежит 0, то прыгнуть куда-то вдаль
Вернуть и проглотить nullableString
Внутри measureIfNull
Загрузить nullableString в %r11d
Если в %r11d лежит 0, то прыгнуть куда-то вдаль
Вернуть и проглотить nullableString
● HotSpot учел профиль выполнения:○ nullableString почти никогда не бывает null;○ обработка null вынесена за пределы горячего кода.
Внутри measureMatchOption
Проверка someString.getClass == classOf[Some]
Загрузить структуру, описывающую Some в %r10
Вынуть из нее поле x в регистр %r11d
Проверка x.getClass == classOf[String]
Вернуть и проглотить результат
В чём разница?
● measureIfNull - 1 проверка типа● measureMatchOption - 2 проверки:
○ проклятие генериков и type erasure;
В чём разница?
● measureIfNull - 1 проверка типа● measureMatchOption - 2 проверки:
○ проклятие генериков и type erasure;○ JVM не может сразу определить тип объекта и
тип его содержимого;○ Option[String] == Option[Int] == Option[Object]○ приходится сравнивать дважды.
Выводы о pattern matching
● if-elseif-elseif-else ~= PM○ PM значительно нагляднее if-else
● матчинг по генерикам: особенности JVM○ одно лишнее сравнение - справедливая цена за
удобство;○ JVM неплохо оптимизирует hot-code-path для PM.
Оптимизация хвостовой рекурсии
● трюк компилятора, замена рекурсивного вызова функции на цикл
● работает не для любого рекурсивного вызова, а только для хвостового
Оптимизация хвостовой рекурсии
● трюк компилятора, замена рекурсивного вызова функции на цикл;
● работает не для любого рекурсивного вызова, а только для хвостового;
● есть в scalac, нет в javac;● предмет для гордости
у любителей ФП.
Тестовая задача с TCO
● вычисление N-го числа Фибоначчи:● 0, 1, 1, 2, 3, 5, 8, 13, 21,…
Тестовая задача с TCO
● вычисление N-го числа Фибоначчи:● 0, 1, 1, 2, 3, 5, 8, 13, 21,…● Scala + TCO:
Числа Фибоначчи и Java
● Обычная рекурсия:
● Цикл:
Фибоначчи, результаты
● 10, 100, 1000-е число:
Фибоначчи, результаты
● 10, 100, 1000-е число:
● scala.measureTCO ~= java.measureLoop● рекурсия без TCO ожидаемо медленнее● но почему именно так?
Заглянем в байткод
● JVM байткод довольно прост, правда!
● javap - утилита из OpenJDK для вивисекции class-файлов
● Есть встроенный “дизассемблер”:○ $ javap -c JavaFibonacci.class
○ подходит и для работы с Scala○ интегрирована в Scala REPL, :javap
Java recursion под капотом
Байткод для java-recursion:
Scala TCO под капотом
Байткод для @tailrec функции:
Scala TCO под капотом
Байткод для @tailrec функции:
● разница только в вызове invokespecial● почему же она такая большая?
Вызов метода в JVM
● Несколько способов вызова методов:○ invokevirtual - виртуальный метод;○ invokestatic - статический метод;○ invokespecial - private/instance метод.
Вызов метода в JVM
● Несколько способов вызова методов:○ invokevirtual - виртуальный метод;○ invokestatic - статический метод;○ invokespecial - private/instance метод.
● Вызов метода:○ передача контроля VM;○ …○ тыр-пыр, туда-сюда;○ ...○ передача контроля коду.
На дне
переход прямо в кишки JVM
Goto vs invokespecial
● x86 goto - переход по адресу● invokespecial - инструкция для вызова
приватных/инстанс методов класса[1]:○ выяснить, что требуемый метод - private;○ найти код нужного метода по имени;○ кинуть exception, если не нашлось ничего;○ создать новый фрейм;○ передать аргументы.
[1]: http://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.12
Goto vs invokespecial
● x86 goto - переход по адресу● invokespecial - инструкция для вызова
приватных/инстанс методов класса:○ выяснить, что требуемый метод - private;○ найти код нужного метода по имени;○ кинуть exception, если не нашлось ничего;○ создать новый фрейм;○ передать аргументы.
● JVM отлично оптимизирует вызов метода● Но goto все равно быстрее
Инлайнинг
● -XX:+PrintInlining
Инлайнинг
● -XX:+PrintInlining
● всё может быть ещё сложнее:○ JVM встроит рекурсивную функцию саму в себя;○ через несколько итераций ему надоест;○ и он начнет дергать invokespecial.
Подробнее о JVM и Scala рекурсии
● A.Shipilev, “Scala vs Java: divided we fail”[1]
● A.Shipilev, “The black magic of Java method dispatch”[2]
● Oracle HotSpotInternals wiki[3]
[1]: http://shipilev.net/blog/2014/java-scala-divided-we-fail, 2014[2]: http://shipilev.net/blog/2015/black-magic-method-dispatch, 2015[3]: https://wiki.openjdk.java.net/dashboard.action
Выводы о рекурсии
● TCO работает так же быстро, как и цикл.● Не бойтесь рекурсии, она классная.● Необходим навык, чтобы:
○ перестать мыслить циклами;○ перестать вычислять числа Фибоначчи;○ начать писать понятные рекурсивные алгоритмы.
Scala collections
● Простой способ делать сложные вещи:○ map, flatMap, fold, etc...○ в Java7 подобные вещи надо делать руками○ Java8 streams: шаг в сторону ФП
Scala collections
● Простой способ делать сложные вещи:○ map, flatMap, fold, etc...○ в Java7 подобные вещи надо делать руками○ Java8 streams: шаг в сторону ФП
● За всё хорошее приходится платить:○ есть ли накладные расходы?○ почему они именно такие?○ как с этим дальше жить.
Выбор задачи
● java.util.ArrayList[1] vs scala.Array*:○ алгоритмически схожие;○ одинаковые показатели алгоритмической
эффективности по памяти и объему вычислений.
[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/*: scala.ArrayOps
Выбор задачи
● java.util.ArrayList[1] vs scala.Array*:○ алгоритмически схожие;○ одинаковые показатели алгоритмической
эффективности по памяти и объему вычислений.
● Задача должна быть показательной:○ использовать коллекции;○ минимум накладных вычислений вне работы с
коллекциями.
[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/*: scala.ArrayOps
Сумма квадратов
● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
Сумма квадратов
● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]
Проблемы:● синтетический тест, далёк от жизни● расходы на boxing/unboxing
примитивов
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
Сумма квадратов
● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]
Проблемы:● синтетический тест, далёк от жизни● расходы на boxing/unboxing
примитивов
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
Scala way
● ФП и императивный вариант
Java way
● императивная классика:
Результаты
Результаты
1. тормоза на больших массивах2. ФП vs императив
2
1
Императив vs императив
JavaSquares.imp ScalaSquares.imp
1: Императив vs императив
JavaSquares.imp ScalaSquares.imp
● imul и add иногда независимы по данным● можно не исполнять их последовательно
Out-of-order execution
Так почему же тормозит?
● HotSpot - коллекция эвристиков.● Эвристики иногда ошибаются,
○ особенно при виде Scala-кода
Так почему же тормозит?
● HotSpot - коллекция эвристиков.● Эвристики иногда ошибаются,
○ особенно при виде Scala-кода
● немного разный байткод;● разная размерность массива;● разный профиль выполнения;● разные решения по JIT-компиляции.
2: ФП vs императив
● подождите кидаться смотреть (байт)код!● scalac делает много оптимизаций:
○ инлайнинг лямбд и замыканий;○ dead code elimination;○ упрощение box+unbox;○ вот это всё.
2: ФП vs императив
● подождите кидаться смотреть (байт)код!● scalac делает много оптимизаций:
○ инлайнинг лямбд и замыканий;○ dead code elimination;○ упрощение box+unbox;○ вот это всё.
● scalac -print выведет непосредственно всё то, что перегниёт в байткод:○ всё неявное становится явным;○ последствия оптимизаций уровня AST.
squaresFold без сахара
squaresFold без сахара
squaresFold без сахара
● явное создание лямбды;● box/unbox;● кровь, кишки, специализация.
squaresFold hottest methods
squaresFold hottest methods
● java.lang.Object::<init> ест 17% CPU● похоже, мы активно плодим объекты:
JMH GC profiler[1]
● обрабатывает GC Notifications[2] через JMX● почти не влияет на производительность
[1] http://mail.openjdk.java.net/pipermail/jmh-dev/2015-April/001785.html
[2] https://docs.oracle.com/javase/8/docs/jre/api/management/extension/com/sun/management/GarbageCollectionNotificationInfo.html
JMH GC profiler[1]
● обрабатывает GC Notifications[2] через JMX● почти не влияет на производительность● результат:
[1] http://mail.openjdk.java.net/pipermail/jmh-dev/2015-April/001785.html
[2] https://docs.oracle.com/javase/8/docs/jre/api/management/extension/com/sun/management/GarbageCollectionNotificationInfo.html
JMH GC profiler[1]
● обрабатывает GC Notifications[2] через JMX● почти не влияет на производительность● результат:
полтора гига мусора в секунду?!
[1] http://mail.openjdk.java.net/pipermail/jmh-dev/2015-April/001785.html
[2] https://docs.oracle.com/javase/8/docs/jre/api/management/extension/com/sun/management/GarbageCollectionNotificationInfo.html
Сравнили теплое с мягким
Тесты не идентичны:
примитив
объект
Specialization: тернистый путь
@specialized - оптимизация scalac для избежания боксинга
Specialization: тернистый путь
@specialized - оптимизация scalac для избежания боксинга:● для требуемого примитивного типа
создается своя копия метода:
● хотели как лучше, а вышло как всегда.
Мама, я генерик
● scala.collection.*:○ не умеют в специализацию;○ являются генериками, List[T] = List[Object]○ все методы - тоже генерики,
list.fold(value:T)(...) == list.fold(value:Object)(...)
Мама, я генерик
● scala.collection.*:○ не умеют в специализацию;○ являются генериками, List[T] = List[Object]○ все методы - тоже генерики,
list.fold(value:T)(...) == list.fold(value:Object)(...)
Вывод: без боксинга никак нельзя(но в java N+2, возможно, будет можно[1])
[1]: Project Valhalla: http://cr.openjdk.java.net/~briangoetz/valhalla/specialization.html
Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.
Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.
● Коллекции в скале пока дженерики:○ даже scala.Array этим иногда страдает[1];○ боксинг и тормоза во все поля;○ в далеких планах минибоксинг @miniboxed[2].
[1]: http://www.scala-lang.org/api/current/index.html#scala.Array[2]: http://scala-miniboxing.org
Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.
● Коллекции в скале пока дженерики:○ даже scala.Array этим иногда страдает[1];○ боксинг и тормоза во все поля;○ в далеких планах минибоксинг @miniboxed[2].
● Сторонние коллекции:○ Debox: @specialized Buffer, Map, Set[3].
[1]: http://www.scala-lang.org/api/current/index.html#scala.Array[2]: http://scala-miniboxing.org[3]: https://github.com/non/debox
О коллекциях
● Если очень хочется, то жить можно● Есть коллекции для примитивов: debox● Можно написать свою: @specialized
О коллекциях
● Если очень хочется, то жить можно● Есть коллекции для примитивов: debox● Можно написать свою: @specialized
В итоге
● Scala медленная: ○ легко написать тормозной, но красивый код;○ коллекции не дружат с примитивами;○ scalac может нагенерить сумрачный байткод.
В итоге
● Scala медленная: ○ легко написать тормозной, но красивый код;○ коллекции не дружат с примитивами;○ scalac может нагенерить сумрачный байткод.
● Scala быстрая:○ при понимании внутренностей, красивый код
может работать со скоростью Java;○ коллекции можно подружить с примитивами при
помощи синей изоленты (а в Java - нельзя);○ JVM из сумрачного байткода может сделать
эффективный машинный код.
Личный опыт
● Пишем на скале два года и ничё.● Баланс быстроты кода и разработки:
○ пишешь быстро - работает медленно; ○ пишешь медленно - работает быстро.
● Все бенчмарки доступны на гитхабе:○ https://github.com/shuttie/scala-perf-talk
Вопросы?
Коллекции и объекты
● найти сумму длин всех строк● java.util.ArrayList vs scala.Array
Scala collections
Страх и ненависть в scala.Array
● scala.Array + ФП-примочки:○ Array + ArrayOps○ WrappedArray○ ArraySeq○ ArrayBuffer
Как жить с примитивными типами
Debox сильно упрощает жизнь:● хитрый, канонический и наивный пример
Примитивы, результат тестов
Примитивы, результат тестов
● debox ~= while-цикл, yay!● baseline ~3 нс*● обработка 1 элемента - ~0.225 baseline.
* - 2.7GHz Core i5-3337U
Немного x86_64 assembly
Немного x86_64 assembly
● почему все инструкции повторяются?● loop-unrolling и out-of-order execution!