Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Scala performance под капотом

2,350 views

Published on

Слайды к докладу на Javapoint 2015.

Published in: Technology

Scala performance под капотом

  1. 1. Scala performance под капотом Гребенников Роман sociohub.ru @public_void_grv 2015, jpoint
  2. 2. Intro: Зачем это всё? ● В Scala много “необычного”: ○ FP, pattern matching, лень, коллекции. ○ Всё такое удобное и классное.
  3. 3. Intro: Зачем это всё? ● В Scala много “необычного”: ○ FP, pattern matching, лень, коллекции. ○ Всё такое удобное и классное.
  4. 4. Intro: горячее необычное ● ФП в горячих участках кода: ○ улучшает читабельность; ○ добавляет непредсказуемости.
  5. 5. Intro: горячее необычное ● ФП в горячих участках кода: ○ улучшает читабельность; ○ добавляет непредсказуемости. ● ФП-абстракции могут: ○ неявно создавать новые объекты; ○ делать дополнительные вычисления; ○ портить жизнь JVM JIT.
  6. 6. О чем доклад? ● Основы: ○ Измерять производительность сложно. ○ Что делает HotSpot и scalac под капотом. ○ JMH и как (не)правильно измерять. ● Scala в реальной жизни: ○ pattern matching; ○ рекурсия; ○ коллекции и лямбды. ● Как с этим жить.
  7. 7. А что тут сложного?
  8. 8. А что тут сложного? ● Что тут не так? ○ цикл может быть удалён оптимизирован целиком; ○ doSomeStuff может быть скомпилирован позже 1000-й итерации; ○ весь цикл может занять меньше 1мс; ○ и еще миллион проблем.
  9. 9. А что тут сложного? ● Что тут не так? ○ цикл может быть удалён оптимизирован целиком; ○ doSomeStuff может быть скомпилирован позже 1000-й итерации; ○ весь цикл может занять меньше 1мс; ○ и еще миллион проблем. ● Причина: HotSpot умнее тебя
  10. 10. Внутри HotSpot Foo.scala Foo.java scalac javac
  11. 11. Внутри HotSpot Foo.scala Foo.java scalac javac МАГИЯ
  12. 12. Внутри HotSpot Foo.scala Foo.java scalac javac МАГИЯ
  13. 13. Внутри HotSpot ● С0 -> C1 -> C2: ○ агрессивнее оптимизации ~ быстрее код; ○ больше времени на разогрев. ● Множество опасностей на каждом шагу Foo.scala Foo.java scalac javac JVM/HotSpot Bytecode CPU С0: интерпретация С1 С2 маш. код
  14. 14. Почему не надо делать это руками ● Надо обойти много ловушек JVM, чтобы получить достоверный результат: ○ корректно измерять время; ○ бороться с dead-code-elimination; ○ избегать constant-folding; ○ победить loop-unrolling; ○ и еще два десятка особенностей.
  15. 15. Почему не надо делать это руками ● Надо обойти много ловушек JVM, чтобы получить достоверный результат: ○ корректно измерять время; ○ бороться с dead-code-elimination; ○ избегать constant-folding; ○ победить loop-unrolling; ○ и еще два десятка особенностей. ● Не надо изобретать велосипед
  16. 16. JMH[1] Harness для написания и запуска (микро) бенчмарков: ● набор аннотаций; ● консольный интерфейс для запуска; [1]: http://openjdk.java.net/projects/code-tools/jmh/
  17. 17. JMH и Scala ● плагин sbt-jmh [1] ● запускается из консоли sbt [1]: https://github.com/ktoso/sbt-jmh
  18. 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. 19. Pattern matching ● сопоставление с образцом: ○ switch-case на стероидах; ○ экстракторы, regex, списки и т.п. ○ гордость ФП-фанатов, повод унижать Java.
  20. 20. Pattern matching ● сопоставление с образцом: ○ switch-case на стероидах; ○ экстракторы, regex, списки и т.п. ○ гордость ФП-фанатов, повод унижать Java. ● условия для тестовой задачи: ○ часто используется в реальной жизни; ○ простота и минимум дополнительной логики.
  21. 21. Выбор из нескольких вариантов ● базовый трейт и несколько наследников ● задача: ○ def select(value:Base) ○ какой дочерний тип реализует Base?
  22. 22. Есть две реализации... ● с матчингом и цепочкой сравнений:
  23. 23. Результаты ● результаты выглядят одинаково ● но одинаково ли то, что внутри?
  24. 24. Заглянем в машинный код Это даст нам все ответы. Наверное.
  25. 25. Заглянем в машинный код Это даст нам все ответы. Наверное. Проблема: ● я не соображаю в x86_64 ассемблере; ● но я неплохо делаю вид, что соображаю[1]. [1]: Wikibooks: x86 Disassembly, http://en.wikibooks.org/wiki/X86_Disassembly
  26. 26. JMH perfasm profiler ● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами: ○ много буков, тяжело читать.
  27. 27. JMH perfasm profiler ● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами: ○ много буков, тяжело читать. ● perf, CPU performance counters[1]: ○ интерфейс ядра Linux; ○ надо следить за изменениями.
  28. 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
  29. 29. def measurePatternMatch(v:Baz)
  30. 30. def measurePatternMatch(v:Baz)
  31. 31. def measurePatternMatch(v:Baz) ● указатель на структуру-описание класса ● по смещению 8 лежит classword
  32. 32. def measurePatternMatch(v:Baz) ● cmp: Compare, сравнить два значения. ● je: Jump-if-equals, перейти, если равны.
  33. 33. def measurePatternMatch(v:Baz) ● jne: Jump-if-Not-Equals ● учтён профиль выполнения кода
  34. 34. def measurePatternMatch(v:Baz) ● готовим аргументы для вызова consume() ● проглатываем результат $0x3
  35. 35. def measurePatternMatch(v:Baz)
  36. 36. def measureIf(v:Baz)
  37. 37. If-else vs match Идентичный машинный код:
  38. 38. Option[T] ● типизированная замена null-check ● Option[T]: ○ Some(x:T) - если что-то есть, ○ None - если ничего нет. ● scalac не даст сконвертить Option[T] => T ● явная обработка None
  39. 39. Option[T] pattern matching
  40. 40. Option[T]: Результаты ● Задача и результаты весьма близки ● Но null-check чуть быстрее ● Как же так? * - Scala 2.11.6, JMH 1.8, Oracle JDK 1.8_40
  41. 41. Внутри measureIfNull Загрузить nullableString в %r11d Если в %r11d лежит 0, то прыгнуть куда-то вдаль Вернуть и проглотить nullableString
  42. 42. Внутри measureIfNull Загрузить nullableString в %r11d Если в %r11d лежит 0, то прыгнуть куда-то вдаль Вернуть и проглотить nullableString ● HotSpot учел профиль выполнения: ○ nullableString почти никогда не бывает null; ○ обработка null вынесена за пределы горячего кода.
  43. 43. Внутри measureMatchOption Проверка someString.getClass == classOf[Some] Загрузить структуру, описывающую Some в %r10 Вынуть из нее поле x в регистр %r11d Проверка x.getClass == classOf[String] Вернуть и проглотить результат
  44. 44. В чём разница? ● measureIfNull - 1 проверка типа ● measureMatchOption - 2 проверки: ○ проклятие генериков и type erasure;
  45. 45. В чём разница? ● measureIfNull - 1 проверка типа ● measureMatchOption - 2 проверки: ○ проклятие генериков и type erasure; ○ JVM не может сразу определить тип объекта и тип его содержимого; ○ Option[String] == Option[Int] == Option[Object] ○ приходится сравнивать дважды.
  46. 46. Выводы о pattern matching ● if-elseif-elseif-else ~= PM ○ PM значительно нагляднее if-else ● матчинг по генерикам: особенности JVM ○ одно лишнее сравнение - справедливая цена за удобство; ○ JVM неплохо оптимизирует hot-code-path для PM.
  47. 47. Оптимизация хвостовой рекурсии ● трюк компилятора, замена рекурсивного вызова функции на цикл ● работает не для любого рекурсивного вызова, а только для хвостового
  48. 48. Оптимизация хвостовой рекурсии ● трюк компилятора, замена рекурсивного вызова функции на цикл; ● работает не для любого рекурсивного вызова, а только для хвостового; ● есть в scalac, нет в javac; ● предмет для гордости у любителей ФП.
  49. 49. Тестовая задача с TCO ● вычисление N-го числа Фибоначчи: ● 0, 1, 1, 2, 3, 5, 8, 13, 21,…
  50. 50. Тестовая задача с TCO ● вычисление N-го числа Фибоначчи: ● 0, 1, 1, 2, 3, 5, 8, 13, 21,… ● Scala + TCO:
  51. 51. Числа Фибоначчи и Java ● Обычная рекурсия: ● Цикл:
  52. 52. Фибоначчи, результаты ● 10, 100, 1000-е число:
  53. 53. Фибоначчи, результаты ● 10, 100, 1000-е число: ● scala.measureTCO ~= java.measureLoop ● рекурсия без TCO ожидаемо медленнее ● но почему именно так?
  54. 54. Заглянем в байткод ● JVM байткод довольно прост, правда! ● javap - утилита из OpenJDK для вивисекции class-файлов ● Есть встроенный “дизассемблер”: ○ $ javap -c JavaFibonacci.class ○ подходит и для работы с Scala ○ интегрирована в Scala REPL, :javap
  55. 55. Java recursion под капотом Байткод для java-recursion:
  56. 56. Scala TCO под капотом Байткод для @tailrec функции:
  57. 57. Scala TCO под капотом Байткод для @tailrec функции: ● разница только в вызове invokespecial ● почему же она такая большая?
  58. 58. Вызов метода в JVM ● Несколько способов вызова методов: ○ invokevirtual - виртуальный метод; ○ invokestatic - статический метод; ○ invokespecial - private/instance метод.
  59. 59. Вызов метода в JVM ● Несколько способов вызова методов: ○ invokevirtual - виртуальный метод; ○ invokestatic - статический метод; ○ invokespecial - private/instance метод. ● Вызов метода: ○ передача контроля VM; ○ … ○ тыр-пыр, туда-сюда; ○ ... ○ передача контроля коду.
  60. 60. На дне переход прямо в кишки JVM
  61. 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. 62. Goto vs invokespecial ● x86 goto - переход по адресу ● invokespecial - инструкция для вызова приватных/инстанс методов класса: ○ выяснить, что требуемый метод - private; ○ найти код нужного метода по имени; ○ кинуть exception, если не нашлось ничего; ○ создать новый фрейм; ○ передать аргументы. ● JVM отлично оптимизирует вызов метода ● Но goto все равно быстрее
  63. 63. Инлайнинг ● -XX:+PrintInlining
  64. 64. Инлайнинг ● -XX:+PrintInlining ● всё может быть ещё сложнее: ○ JVM встроит рекурсивную функцию саму в себя; ○ через несколько итераций ему надоест; ○ и он начнет дергать invokespecial.
  65. 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. 66. Выводы о рекурсии ● TCO работает так же быстро, как и цикл. ● Не бойтесь рекурсии, она классная. ● Необходим навык, чтобы: ○ перестать мыслить циклами; ○ перестать вычислять числа Фибоначчи; ○ начать писать понятные рекурсивные алгоритмы.
  67. 67. Scala collections ● Простой способ делать сложные вещи: ○ map, flatMap, fold, etc... ○ в Java7 подобные вещи надо делать руками ○ Java8 streams: шаг в сторону ФП
  68. 68. Scala collections ● Простой способ делать сложные вещи: ○ map, flatMap, fold, etc... ○ в Java7 подобные вещи надо делать руками ○ Java8 streams: шаг в сторону ФП ● За всё хорошее приходится платить: ○ есть ли накладные расходы? ○ почему они именно такие? ○ как с этим дальше жить.
  69. 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. 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. 71. Сумма квадратов ● Дано: коллекция целых чисел ● Рассчитать сумму квадратов[1] [1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
  72. 72. Сумма квадратов ● Дано: коллекция целых чисел ● Рассчитать сумму квадратов[1] Проблемы: ● синтетический тест, далёк от жизни ● расходы на boxing/unboxing примитивов [1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
  73. 73. Сумма квадратов ● Дано: коллекция целых чисел ● Рассчитать сумму квадратов[1] Проблемы: ● синтетический тест, далёк от жизни ● расходы на boxing/unboxing примитивов [1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/
  74. 74. Scala way ● ФП и императивный вариант
  75. 75. Java way ● императивная классика:
  76. 76. Результаты
  77. 77. Результаты 1. тормоза на больших массивах 2. ФП vs императив 2 1
  78. 78. Императив vs императив JavaSquares.imp ScalaSquares.imp
  79. 79. 1: Императив vs императив JavaSquares.imp ScalaSquares.imp ● imul и add иногда независимы по данным ● можно не исполнять их последовательно
  80. 80. Out-of-order execution
  81. 81. Так почему же тормозит? ● HotSpot - коллекция эвристиков. ● Эвристики иногда ошибаются, ○ особенно при виде Scala-кода
  82. 82. Так почему же тормозит? ● HotSpot - коллекция эвристиков. ● Эвристики иногда ошибаются, ○ особенно при виде Scala-кода ● немного разный байткод; ● разная размерность массива; ● разный профиль выполнения; ● разные решения по JIT-компиляции.
  83. 83. 2: ФП vs императив ● подождите кидаться смотреть (байт)код! ● scalac делает много оптимизаций: ○ инлайнинг лямбд и замыканий; ○ dead code elimination; ○ упрощение box+unbox; ○ вот это всё.
  84. 84. 2: ФП vs императив ● подождите кидаться смотреть (байт)код! ● scalac делает много оптимизаций: ○ инлайнинг лямбд и замыканий; ○ dead code elimination; ○ упрощение box+unbox; ○ вот это всё. ● scalac -print выведет непосредственно всё то, что перегниёт в байткод: ○ всё неявное становится явным; ○ последствия оптимизаций уровня AST.
  85. 85. squaresFold без сахара
  86. 86. squaresFold без сахара
  87. 87. squaresFold без сахара ● явное создание лямбды; ● box/unbox; ● кровь, кишки, специализация.
  88. 88. squaresFold hottest methods
  89. 89. squaresFold hottest methods ● java.lang.Object::<init> ест 17% CPU ● похоже, мы активно плодим объекты:
  90. 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. 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. 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
  93. 93. Сравнили теплое с мягким Тесты не идентичны: примитив объект
  94. 94. Specialization: тернистый путь @specialized - оптимизация scalac для избежания боксинга
  95. 95. Specialization: тернистый путь @specialized - оптимизация scalac для избежания боксинга: ● для требуемого примитивного типа создается своя копия метода: ● хотели как лучше, а вышло как всегда.
  96. 96. Мама, я генерик ● scala.collection.*: ○ не умеют в специализацию; ○ являются генериками, List[T] = List[Object] ○ все методы - тоже генерики, list.fold(value:T)(...) == list.fold(value:Object)(...)
  97. 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. 98. Коллекции и примитивные типы ● хочешь быстрой работы с примитивами? ○ опасайся боксинга; ○ или перепиши все на С/С++.
  99. 99. Коллекции и примитивные типы ● хочешь быстрой работы с примитивами? ○ опасайся боксинга; ○ или перепиши все на С/С++. ● Коллекции в скале пока дженерики: ○ даже scala.Array этим иногда страдает[1]; ○ боксинг и тормоза во все поля; ○ в далеких планах минибоксинг @miniboxed[2]. [1]: http://www.scala-lang.org/api/current/index.html#scala.Array [2]: http://scala-miniboxing.org
  100. 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. 101. О коллекциях ● Если очень хочется, то жить можно ● Есть коллекции для примитивов: debox ● Можно написать свою: @specialized
  102. 102. О коллекциях ● Если очень хочется, то жить можно ● Есть коллекции для примитивов: debox ● Можно написать свою: @specialized
  103. 103. В итоге ● Scala медленная: ○ легко написать тормозной, но красивый код; ○ коллекции не дружат с примитивами; ○ scalac может нагенерить сумрачный байткод.
  104. 104. В итоге ● Scala медленная: ○ легко написать тормозной, но красивый код; ○ коллекции не дружат с примитивами; ○ scalac может нагенерить сумрачный байткод. ● Scala быстрая: ○ при понимании внутренностей, красивый код может работать со скоростью Java; ○ коллекции можно подружить с примитивами при помощи синей изоленты (а в Java - нельзя); ○ JVM из сумрачного байткода может сделать эффективный машинный код.
  105. 105. Личный опыт ● Пишем на скале два года и ничё. ● Баланс быстроты кода и разработки: ○ пишешь быстро - работает медленно; ○ пишешь медленно - работает быстро. ● Все бенчмарки доступны на гитхабе: ○ https://github.com/shuttie/scala-perf-talk
  106. 106. Вопросы?
  107. 107. Коллекции и объекты ● найти сумму длин всех строк ● java.util.ArrayList vs scala.Array
  108. 108. Scala collections
  109. 109. Страх и ненависть в scala.Array ● scala.Array + ФП-примочки: ○ Array + ArrayOps ○ WrappedArray ○ ArraySeq ○ ArrayBuffer
  110. 110. Как жить с примитивными типами Debox сильно упрощает жизнь: ● хитрый, канонический и наивный пример
  111. 111. Примитивы, результат тестов
  112. 112. Примитивы, результат тестов ● debox ~= while-цикл, yay! ● baseline ~3 нс* ● обработка 1 элемента - ~0.225 baseline. * - 2.7GHz Core i5-3337U
  113. 113. Немного x86_64 assembly
  114. 114. Немного x86_64 assembly ● почему все инструкции повторяются? ● loop-unrolling и out-of-order execution!

×