5. Intro: горячее необычное
● ФП в горячих участках кода:
○ улучшает читабельность;
○ добавляет непредсказуемости.
● ФП-абстракции могут:
○ неявно создавать новые объекты;
○ делать дополнительные вычисления;
○ портить жизнь JVM JIT.
6. О чем доклад?
● Основы:
○ Измерять производительность сложно.
○ Что делает HotSpot и scalac под капотом.
○ JMH и как (не)правильно измерять.
● Scala в реальной жизни:
○ pattern matching;
○ рекурсия;
○ коллекции и лямбды.
● Как с этим жить.
8. А что тут сложного?
● Что тут не так?
○ цикл может быть удалён оптимизирован целиком;
○ doSomeStuff может быть скомпилирован позже
1000-й итерации;
○ весь цикл может занять меньше 1мс;
○ и еще миллион проблем.
9. А что тут сложного?
● Что тут не так?
○ цикл может быть удалён оптимизирован целиком;
○ doSomeStuff может быть скомпилирован позже
1000-й итерации;
○ весь цикл может занять меньше 1мс;
○ и еще миллион проблем.
● Причина: HotSpot умнее тебя
13. Внутри HotSpot
● С0 -> C1 -> C2:
○ агрессивнее оптимизации ~ быстрее код;
○ больше времени на разогрев.
● Множество опасностей на каждом шагу
Foo.scala
Foo.java
scalac
javac
JVM/HotSpot
Bytecode
CPU
С0: интерпретация
С1
С2
маш.
код
14. Почему не надо делать это руками
● Надо обойти много ловушек JVM, чтобы
получить достоверный результат:
○ корректно измерять время;
○ бороться с dead-code-elimination;
○ избегать constant-folding;
○ победить loop-unrolling;
○ и еще два десятка особенностей.
15. Почему не надо делать это руками
● Надо обойти много ловушек JVM, чтобы
получить достоверный результат:
○ корректно измерять время;
○ бороться с dead-code-elimination;
○ избегать constant-folding;
○ победить loop-unrolling;
○ и еще два десятка особенностей.
● Не надо изобретать велосипед
16. JMH[1]
Harness для написания и запуска (микро)
бенчмарков:
● набор аннотаций;
● консольный интерфейс для запуска;
[1]: http://openjdk.java.net/projects/code-tools/jmh/
17. JMH и Scala
● плагин sbt-jmh [1]
● запускается из консоли sbt
[1]: https://github.com/ktoso/sbt-jmh
18. Если хочется подробностей
● Алексей Шипилёв:
○ 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/
19. Pattern matching
● сопоставление с образцом:
○ switch-case на стероидах;
○ экстракторы, regex, списки и т.п.
○ гордость ФП-фанатов, повод унижать Java.
20. Pattern matching
● сопоставление с образцом:
○ switch-case на стероидах;
○ экстракторы, regex, списки и т.п.
○ гордость ФП-фанатов, повод унижать Java.
● условия для тестовой задачи:
○ часто используется в реальной жизни;
○ простота и минимум дополнительной логики.
21. Выбор из нескольких вариантов
● базовый трейт и несколько наследников
● задача:
○ def select(value:Base)
○ какой дочерний тип реализует Base?
25. Заглянем в машинный код
Это даст нам все ответы. Наверное.
Проблема:
● я не соображаю в x86_64 ассемблере;
● но я неплохо делаю вид, что соображаю[1].
[1]: Wikibooks: x86 Disassembly, http://en.wikibooks.org/wiki/X86_Disassembly
26. JMH perfasm profiler
● -XX:+PrintAssembly - заставить JVM
плеваться ассемблерными листингами:
○ много буков, тяжело читать.
27. JMH perfasm profiler
● -XX:+PrintAssembly - заставить JVM
плеваться ассемблерными листингами:
○ много буков, тяжело читать.
● perf, CPU performance counters[1]:
○ интерфейс ядра Linux;
○ надо следить за изменениями.
28. 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
38. Option[T]
● типизированная замена null-check
● Option[T]:
○ Some(x:T) - если что-то есть,
○ None - если ничего нет.
● scalac не даст сконвертить Option[T] => T
● явная обработка None
42. Внутри measureIfNull
Загрузить nullableString в %r11d
Если в %r11d лежит 0, то прыгнуть куда-то вдаль
Вернуть и проглотить nullableString
● HotSpot учел профиль выполнения:
○ nullableString почти никогда не бывает null;
○ обработка null вынесена за пределы горячего кода.
43. Внутри measureMatchOption
Проверка someString.getClass == classOf[Some]
Загрузить структуру, описывающую Some в %r10
Вынуть из нее поле x в регистр %r11d
Проверка x.getClass == classOf[String]
Вернуть и проглотить результат
44. В чём разница?
● measureIfNull - 1 проверка типа
● measureMatchOption - 2 проверки:
○ проклятие генериков и type erasure;
45. В чём разница?
● measureIfNull - 1 проверка типа
● measureMatchOption - 2 проверки:
○ проклятие генериков и type erasure;
○ JVM не может сразу определить тип объекта и
тип его содержимого;
○ Option[String] == Option[Int] == Option[Object]
○ приходится сравнивать дважды.
46. Выводы о pattern matching
● if-elseif-elseif-else ~= PM
○ PM значительно нагляднее if-else
● матчинг по генерикам: особенности JVM
○ одно лишнее сравнение - справедливая цена за
удобство;
○ JVM неплохо оптимизирует hot-code-path для PM.
47. Оптимизация хвостовой рекурсии
● трюк компилятора, замена рекурсивного
вызова функции на цикл
● работает не для любого рекурсивного
вызова, а только для хвостового
48. Оптимизация хвостовой рекурсии
● трюк компилятора, замена рекурсивного
вызова функции на цикл;
● работает не для любого рекурсивного
вызова, а только для хвостового;
● есть в scalac, нет в javac;
● предмет для гордости
у любителей ФП.
49. Тестовая задача с TCO
● вычисление N-го числа Фибоначчи:
● 0, 1, 1, 2, 3, 5, 8, 13, 21,…
50. Тестовая задача с TCO
● вычисление N-го числа Фибоначчи:
● 0, 1, 1, 2, 3, 5, 8, 13, 21,…
● Scala + TCO:
53. Фибоначчи, результаты
● 10, 100, 1000-е число:
● scala.measureTCO ~= java.measureLoop
● рекурсия без TCO ожидаемо медленнее
● но почему именно так?
54. Заглянем в байткод
● JVM байткод довольно прост, правда!
● javap - утилита из OpenJDK для
вивисекции class-файлов
● Есть встроенный “дизассемблер”:
○ $ javap -c JavaFibonacci.class
○ подходит и для работы с Scala
○ интегрирована в Scala REPL, :javap
61. 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
62. Goto vs invokespecial
● x86 goto - переход по адресу
● invokespecial - инструкция для вызова
приватных/инстанс методов класса:
○ выяснить, что требуемый метод - private;
○ найти код нужного метода по имени;
○ кинуть exception, если не нашлось ничего;
○ создать новый фрейм;
○ передать аргументы.
● JVM отлично оптимизирует вызов метода
● Но goto все равно быстрее
64. Инлайнинг
● -XX:+PrintInlining
● всё может быть ещё сложнее:
○ JVM встроит рекурсивную функцию саму в себя;
○ через несколько итераций ему надоест;
○ и он начнет дергать invokespecial.
65. Подробнее о 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
66. Выводы о рекурсии
● TCO работает так же быстро, как и цикл.
● Не бойтесь рекурсии, она классная.
● Необходим навык, чтобы:
○ перестать мыслить циклами;
○ перестать вычислять числа Фибоначчи;
○ начать писать понятные рекурсивные алгоритмы.
67. Scala collections
● Простой способ делать сложные вещи:
○ map, flatMap, fold, etc...
○ в Java7 подобные вещи надо делать руками
○ Java8 streams: шаг в сторону ФП
68. Scala collections
● Простой способ делать сложные вещи:
○ map, flatMap, fold, etc...
○ в Java7 подобные вещи надо делать руками
○ Java8 streams: шаг в сторону ФП
● За всё хорошее приходится платить:
○ есть ли накладные расходы?
○ почему они именно такие?
○ как с этим дальше жить.
69. Выбор задачи
● java.util.ArrayList[1] vs scala.Array*:
○ алгоритмически схожие;
○ одинаковые показатели алгоритмической
эффективности по памяти и объему вычислений.
[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/
*: scala.ArrayOps
70. Выбор задачи
● java.util.ArrayList[1] vs scala.Array*:
○ алгоритмически схожие;
○ одинаковые показатели алгоритмической
эффективности по памяти и объему вычислений.
● Задача должна быть показательной:
○ использовать коллекции;
○ минимум накладных вычислений вне работы с
коллекциями.
[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/
*: scala.ArrayOps
71. Сумма квадратов
● Дано: коллекция целых чисел
● Рассчитать сумму квадратов[1]
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
72. Сумма квадратов
● Дано: коллекция целых чисел
● Рассчитать сумму квадратов[1]
Проблемы:
● синтетический тест, далёк от жизни
● расходы на boxing/unboxing
примитивов
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
73. Сумма квадратов
● Дано: коллекция целых чисел
● Рассчитать сумму квадратов[1]
Проблемы:
● синтетический тест, далёк от жизни
● расходы на boxing/unboxing
примитивов
[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
81. Так почему же тормозит?
● HotSpot - коллекция эвристиков.
● Эвристики иногда ошибаются,
○ особенно при виде Scala-кода
82. Так почему же тормозит?
● HotSpot - коллекция эвристиков.
● Эвристики иногда ошибаются,
○ особенно при виде Scala-кода
● немного разный байткод;
● разная размерность массива;
● разный профиль выполнения;
● разные решения по JIT-компиляции.
83. 2: ФП vs императив
● подождите кидаться смотреть (байт)код!
● scalac делает много оптимизаций:
○ инлайнинг лямбд и замыканий;
○ dead code elimination;
○ упрощение box+unbox;
○ вот это всё.
84. 2: ФП vs императив
● подождите кидаться смотреть (байт)код!
● scalac делает много оптимизаций:
○ инлайнинг лямбд и замыканий;
○ dead code elimination;
○ упрощение box+unbox;
○ вот это всё.
● scalac -print выведет непосредственно
всё то, что перегниёт в байткод:
○ всё неявное становится явным;
○ последствия оптимизаций уровня AST.
90. 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
91. 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
92. 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
95. Specialization: тернистый путь
@specialized - оптимизация scalac для
избежания боксинга:
● для требуемого примитивного типа
создается своя копия метода:
● хотели как лучше, а вышло как всегда.
96. Мама, я генерик
● scala.collection.*:
○ не умеют в специализацию;
○ являются генериками, List[T] = List[Object]
○ все методы - тоже генерики,
list.fold(value:T)(...) == list.fold(value:Object)(...)
97. Мама, я генерик
● 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
98. Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?
○ опасайся боксинга;
○ или перепиши все на С/С++.
99. Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?
○ опасайся боксинга;
○ или перепиши все на С/С++.
● Коллекции в скале пока дженерики:
○ даже scala.Array этим иногда страдает[1];
○ боксинг и тормоза во все поля;
○ в далеких планах минибоксинг @miniboxed[2].
[1]: http://www.scala-lang.org/api/current/index.html#scala.Array
[2]: http://scala-miniboxing.org
100. Коллекции и примитивные типы
● хочешь быстрой работы с примитивами?
○ опасайся боксинга;
○ или перепиши все на С/С++.
● Коллекции в скале пока дженерики:
○ даже 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
101. О коллекциях
● Если очень хочется, то жить можно
● Есть коллекции для примитивов: debox
● Можно написать свою: @specialized
102. О коллекциях
● Если очень хочется, то жить можно
● Есть коллекции для примитивов: debox
● Можно написать свою: @specialized
103. В итоге
● Scala медленная:
○ легко написать тормозной, но красивый код;
○ коллекции не дружат с примитивами;
○ scalac может нагенерить сумрачный байткод.
104. В итоге
● Scala медленная:
○ легко написать тормозной, но красивый код;
○ коллекции не дружат с примитивами;
○ scalac может нагенерить сумрачный байткод.
● Scala быстрая:
○ при понимании внутренностей, красивый код
может работать со скоростью Java;
○ коллекции можно подружить с примитивами при
помощи синей изоленты (а в Java - нельзя);
○ JVM из сумрачного байткода может сделать
эффективный машинный код.
105. Личный опыт
● Пишем на скале два года и ничё.
● Баланс быстроты кода и разработки:
○ пишешь быстро - работает медленно;
○ пишешь медленно - работает быстро.
● Все бенчмарки доступны на гитхабе:
○ https://github.com/shuttie/scala-perf-talk