116
Scala performance под капотом Гребенников Роман sociohub.ru @public_void_grv 2015, jpoint

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

Embed Size (px)

Citation preview

Page 1: Scala performance под капотом

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

Гребенников Романsociohub.ru@public_void_grv

2015, jpoint

Page 2: Scala performance под капотом

Intro: Зачем это всё?

● В Scala много “необычного”:○ FP, pattern matching, лень, коллекции.○ Всё такое удобное и классное.

Page 3: Scala performance под капотом

Intro: Зачем это всё?

● В Scala много “необычного”:○ FP, pattern matching, лень, коллекции.○ Всё такое удобное и классное.

Page 4: Scala performance под капотом

Intro: горячее необычное

● ФП в горячих участках кода:○ улучшает читабельность;○ добавляет непредсказуемости.

Page 5: Scala performance под капотом

Intro: горячее необычное

● ФП в горячих участках кода:○ улучшает читабельность;○ добавляет непредсказуемости.

● ФП-абстракции могут:○ неявно создавать новые объекты;○ делать дополнительные вычисления;○ портить жизнь JVM JIT.

Page 6: Scala performance под капотом

О чем доклад?

● Основы:○ Измерять производительность сложно.○ Что делает HotSpot и scalac под капотом.○ JMH и как (не)правильно измерять.

● Scala в реальной жизни:○ pattern matching;○ рекурсия;○ коллекции и лямбды.

● Как с этим жить.

Page 7: Scala performance под капотом

А что тут сложного?

Page 8: Scala performance под капотом

А что тут сложного?

● Что тут не так?○ цикл может быть удалён оптимизирован целиком;○ doSomeStuff может быть скомпилирован позже

1000-й итерации;○ весь цикл может занять меньше 1мс;○ и еще миллион проблем.

Page 9: Scala performance под капотом

А что тут сложного?

● Что тут не так?○ цикл может быть удалён оптимизирован целиком;○ doSomeStuff может быть скомпилирован позже

1000-й итерации;○ весь цикл может занять меньше 1мс;○ и еще миллион проблем.

● Причина: HotSpot умнее тебя

Page 10: Scala performance под капотом

Внутри HotSpot

Foo.scala

Foo.java

scalac

javac

Page 11: Scala performance под капотом

Внутри HotSpot

Foo.scala

Foo.java

scalac

javac

МАГИЯ

Page 12: Scala performance под капотом

Внутри HotSpot

Foo.scala

Foo.java

scalac

javac

МАГИЯ

Page 13: Scala performance под капотом

Внутри HotSpot

● С0 -> C1 -> C2:○ агрессивнее оптимизации ~ быстрее код;○ больше времени на разогрев.

● Множество опасностей на каждом шагу

Foo.scala

Foo.java

scalac

javacJVM/HotSpot

Bytecode

CPUС0: интерпретация

С1

С2маш.код

Page 14: Scala performance под капотом

Почему не надо делать это руками

● Надо обойти много ловушек JVM, чтобы получить достоверный результат:○ корректно измерять время;○ бороться с dead-code-elimination;○ избегать constant-folding;○ победить loop-unrolling;○ и еще два десятка особенностей.

Page 15: Scala performance под капотом

Почему не надо делать это руками

● Надо обойти много ловушек JVM, чтобы получить достоверный результат:○ корректно измерять время;○ бороться с dead-code-elimination;○ избегать constant-folding;○ победить loop-unrolling;○ и еще два десятка особенностей.

● Не надо изобретать велосипед

Page 16: Scala performance под капотом

JMH[1]

Harness для написания и запуска (микро) бенчмарков:● набор аннотаций;● консольный интерфейс для запуска;

[1]: http://openjdk.java.net/projects/code-tools/jmh/

Page 17: Scala performance под капотом

JMH и Scala

● плагин sbt-jmh [1]

● запускается из консоли sbt

[1]: https://github.com/ktoso/sbt-jmh

Page 18: Scala performance под капотом

Если хочется подробностей

● Алексей Шипилёв:○ 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/

Page 19: Scala performance под капотом

Pattern matching

● сопоставление с образцом: ○ switch-case на стероидах;○ экстракторы, regex, списки и т.п.○ гордость ФП-фанатов, повод унижать Java.

Page 20: Scala performance под капотом

Pattern matching

● сопоставление с образцом: ○ switch-case на стероидах;○ экстракторы, regex, списки и т.п.○ гордость ФП-фанатов, повод унижать Java.

● условия для тестовой задачи:○ часто используется в реальной жизни;○ простота и минимум дополнительной логики.

Page 21: Scala performance под капотом

Выбор из нескольких вариантов

● базовый трейт и несколько наследников

● задача:○ def select(value:Base)○ какой дочерний тип реализует Base?

Page 22: Scala performance под капотом

Есть две реализации...

● с матчингом и цепочкой сравнений:

Page 23: Scala performance под капотом

Результаты

● результаты выглядят одинаково● но одинаково ли то, что внутри?

Page 24: Scala performance под капотом

Заглянем в машинный код

Это даст нам все ответы. Наверное.

Page 25: Scala performance под капотом

Заглянем в машинный код

Это даст нам все ответы. Наверное.

Проблема:● я не соображаю в x86_64 ассемблере;● но я неплохо делаю вид, что соображаю[1].

[1]: Wikibooks: x86 Disassembly, http://en.wikibooks.org/wiki/X86_Disassembly

Page 26: Scala performance под капотом

JMH perfasm profiler

● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами:○ много буков, тяжело читать.

Page 27: Scala performance под капотом

JMH perfasm profiler

● -XX:+PrintAssembly - заставить JVM плеваться ассемблерными листингами:○ много буков, тяжело читать.

● perf, CPU performance counters[1]:○ интерфейс ядра Linux;○ надо следить за изменениями.

Page 28: Scala performance под капотом

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

Page 29: Scala performance под капотом

def measurePatternMatch(v:Baz)

Page 30: Scala performance под капотом

def measurePatternMatch(v:Baz)

Page 31: Scala performance под капотом

def measurePatternMatch(v:Baz)

● указатель на структуру-описание класса● по смещению 8 лежит classword

Page 32: Scala performance под капотом

def measurePatternMatch(v:Baz)

● cmp: Compare, сравнить два значения.● je: Jump-if-equals, перейти, если равны.

Page 33: Scala performance под капотом

def measurePatternMatch(v:Baz)

● jne: Jump-if-Not-Equals● учтён профиль выполнения кода

Page 34: Scala performance под капотом

def measurePatternMatch(v:Baz)

● готовим аргументы для вызова consume()● проглатываем результат $0x3

Page 35: Scala performance под капотом

def measurePatternMatch(v:Baz)

Page 36: Scala performance под капотом

def measureIf(v:Baz)

Page 37: Scala performance под капотом

If-else vs match

Идентичный машинный код:

Page 38: Scala performance под капотом

Option[T]

● типизированная замена null-check● Option[T]:

○ Some(x:T) - если что-то есть,○ None - если ничего нет.

● scalac не даст сконвертить Option[T] => T● явная обработка None

Page 39: Scala performance под капотом

Option[T] pattern matching

Page 40: Scala performance под капотом

Option[T]: Результаты

● Задача и результаты весьма близки● Но null-check чуть быстрее● Как же так?

* - Scala 2.11.6, JMH 1.8, Oracle JDK 1.8_40

Page 41: Scala performance под капотом

Внутри measureIfNull

Загрузить nullableString в %r11d

Если в %r11d лежит 0, то прыгнуть куда-то вдаль

Вернуть и проглотить nullableString

Page 42: Scala performance под капотом

Внутри measureIfNull

Загрузить nullableString в %r11d

Если в %r11d лежит 0, то прыгнуть куда-то вдаль

Вернуть и проглотить nullableString

● HotSpot учел профиль выполнения:○ nullableString почти никогда не бывает null;○ обработка null вынесена за пределы горячего кода.

Page 43: Scala performance под капотом

Внутри measureMatchOption

Проверка someString.getClass == classOf[Some]

Загрузить структуру, описывающую Some в %r10

Вынуть из нее поле x в регистр %r11d

Проверка x.getClass == classOf[String]

Вернуть и проглотить результат

Page 44: Scala performance под капотом

В чём разница?

● measureIfNull - 1 проверка типа● measureMatchOption - 2 проверки:

○ проклятие генериков и type erasure;

Page 45: Scala performance под капотом

В чём разница?

● measureIfNull - 1 проверка типа● measureMatchOption - 2 проверки:

○ проклятие генериков и type erasure;○ JVM не может сразу определить тип объекта и

тип его содержимого;○ Option[String] == Option[Int] == Option[Object]○ приходится сравнивать дважды.

Page 46: Scala performance под капотом

Выводы о pattern matching

● if-elseif-elseif-else ~= PM○ PM значительно нагляднее if-else

● матчинг по генерикам: особенности JVM○ одно лишнее сравнение - справедливая цена за

удобство;○ JVM неплохо оптимизирует hot-code-path для PM.

Page 47: Scala performance под капотом

Оптимизация хвостовой рекурсии

● трюк компилятора, замена рекурсивного вызова функции на цикл

● работает не для любого рекурсивного вызова, а только для хвостового

Page 48: Scala performance под капотом

Оптимизация хвостовой рекурсии

● трюк компилятора, замена рекурсивного вызова функции на цикл;

● работает не для любого рекурсивного вызова, а только для хвостового;

● есть в scalac, нет в javac;● предмет для гордости

у любителей ФП.

Page 49: Scala performance под капотом

Тестовая задача с TCO

● вычисление N-го числа Фибоначчи:● 0, 1, 1, 2, 3, 5, 8, 13, 21,…

Page 50: Scala performance под капотом

Тестовая задача с TCO

● вычисление N-го числа Фибоначчи:● 0, 1, 1, 2, 3, 5, 8, 13, 21,…● Scala + TCO:

Page 51: Scala performance под капотом

Числа Фибоначчи и Java

● Обычная рекурсия:

● Цикл:

Page 52: Scala performance под капотом

Фибоначчи, результаты

● 10, 100, 1000-е число:

Page 53: Scala performance под капотом

Фибоначчи, результаты

● 10, 100, 1000-е число:

● scala.measureTCO ~= java.measureLoop● рекурсия без TCO ожидаемо медленнее● но почему именно так?

Page 54: Scala performance под капотом

Заглянем в байткод

● JVM байткод довольно прост, правда!

● javap - утилита из OpenJDK для вивисекции class-файлов

● Есть встроенный “дизассемблер”:○ $ javap -c JavaFibonacci.class

○ подходит и для работы с Scala○ интегрирована в Scala REPL, :javap

Page 55: Scala performance под капотом

Java recursion под капотом

Байткод для java-recursion:

Page 56: Scala performance под капотом

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

Байткод для @tailrec функции:

Page 57: Scala performance под капотом

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

Байткод для @tailrec функции:

● разница только в вызове invokespecial● почему же она такая большая?

Page 58: Scala performance под капотом

Вызов метода в JVM

● Несколько способов вызова методов:○ invokevirtual - виртуальный метод;○ invokestatic - статический метод;○ invokespecial - private/instance метод.

Page 59: Scala performance под капотом

Вызов метода в JVM

● Несколько способов вызова методов:○ invokevirtual - виртуальный метод;○ invokestatic - статический метод;○ invokespecial - private/instance метод.

● Вызов метода:○ передача контроля VM;○ …○ тыр-пыр, туда-сюда;○ ...○ передача контроля коду.

Page 60: Scala performance под капотом

На дне

переход прямо в кишки JVM

Page 61: Scala performance под капотом

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

Page 62: Scala performance под капотом

Goto vs invokespecial

● x86 goto - переход по адресу● invokespecial - инструкция для вызова

приватных/инстанс методов класса:○ выяснить, что требуемый метод - private;○ найти код нужного метода по имени;○ кинуть exception, если не нашлось ничего;○ создать новый фрейм;○ передать аргументы.

● JVM отлично оптимизирует вызов метода● Но goto все равно быстрее

Page 63: Scala performance под капотом

Инлайнинг

● -XX:+PrintInlining

Page 64: Scala performance под капотом

Инлайнинг

● -XX:+PrintInlining

● всё может быть ещё сложнее:○ JVM встроит рекурсивную функцию саму в себя;○ через несколько итераций ему надоест;○ и он начнет дергать invokespecial.

Page 65: Scala performance под капотом

Подробнее о 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

Page 66: Scala performance под капотом

Выводы о рекурсии

● TCO работает так же быстро, как и цикл.● Не бойтесь рекурсии, она классная.● Необходим навык, чтобы:

○ перестать мыслить циклами;○ перестать вычислять числа Фибоначчи;○ начать писать понятные рекурсивные алгоритмы.

Page 67: Scala performance под капотом

Scala collections

● Простой способ делать сложные вещи:○ map, flatMap, fold, etc...○ в Java7 подобные вещи надо делать руками○ Java8 streams: шаг в сторону ФП

Page 68: Scala performance под капотом

Scala collections

● Простой способ делать сложные вещи:○ map, flatMap, fold, etc...○ в Java7 подобные вещи надо делать руками○ Java8 streams: шаг в сторону ФП

● За всё хорошее приходится платить:○ есть ли накладные расходы?○ почему они именно такие?○ как с этим дальше жить.

Page 69: Scala performance под капотом

Выбор задачи

● java.util.ArrayList[1] vs scala.Array*:○ алгоритмически схожие;○ одинаковые показатели алгоритмической

эффективности по памяти и объему вычислений.

[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/*: scala.ArrayOps

Page 70: Scala performance под капотом

Выбор задачи

● java.util.ArrayList[1] vs scala.Array*:○ алгоритмически схожие;○ одинаковые показатели алгоритмической

эффективности по памяти и объему вычислений.

● Задача должна быть показательной:○ использовать коллекции;○ минимум накладных вычислений вне работы с

коллекциями.

[1]: http://www.programcreek.com/2014/09/top-100-classes-used-in-java-projects/*: scala.ArrayOps

Page 71: Scala performance под капотом

Сумма квадратов

● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]

[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/

Page 72: Scala performance под капотом

Сумма квадратов

● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]

Проблемы:● синтетический тест, далёк от жизни● расходы на boxing/unboxing

примитивов

[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/

Page 73: Scala performance под капотом

Сумма квадратов

● Дано: коллекция целых чисел● Рассчитать сумму квадратов[1]

Проблемы:● синтетический тест, далёк от жизни● расходы на boxing/unboxing

примитивов

[1]: Clash of the lambdas, 2014: http://biboudis.github.io/clashofthelambdas/

Page 74: Scala performance под капотом

Scala way

● ФП и императивный вариант

Page 75: Scala performance под капотом

Java way

● императивная классика:

Page 76: Scala performance под капотом

Результаты

Page 77: Scala performance под капотом

Результаты

1. тормоза на больших массивах2. ФП vs императив

2

1

Page 78: Scala performance под капотом

Императив vs императив

JavaSquares.imp ScalaSquares.imp

Page 79: Scala performance под капотом

1: Императив vs императив

JavaSquares.imp ScalaSquares.imp

● imul и add иногда независимы по данным● можно не исполнять их последовательно

Page 80: Scala performance под капотом

Out-of-order execution

Page 81: Scala performance под капотом

Так почему же тормозит?

● HotSpot - коллекция эвристиков.● Эвристики иногда ошибаются,

○ особенно при виде Scala-кода

Page 82: Scala performance под капотом

Так почему же тормозит?

● HotSpot - коллекция эвристиков.● Эвристики иногда ошибаются,

○ особенно при виде Scala-кода

● немного разный байткод;● разная размерность массива;● разный профиль выполнения;● разные решения по JIT-компиляции.

Page 83: Scala performance под капотом

2: ФП vs императив

● подождите кидаться смотреть (байт)код!● scalac делает много оптимизаций:

○ инлайнинг лямбд и замыканий;○ dead code elimination;○ упрощение box+unbox;○ вот это всё.

Page 84: Scala performance под капотом

2: ФП vs императив

● подождите кидаться смотреть (байт)код!● scalac делает много оптимизаций:

○ инлайнинг лямбд и замыканий;○ dead code elimination;○ упрощение box+unbox;○ вот это всё.

● scalac -print выведет непосредственно всё то, что перегниёт в байткод:○ всё неявное становится явным;○ последствия оптимизаций уровня AST.

Page 85: Scala performance под капотом

squaresFold без сахара

Page 86: Scala performance под капотом

squaresFold без сахара

Page 87: Scala performance под капотом

squaresFold без сахара

● явное создание лямбды;● box/unbox;● кровь, кишки, специализация.

Page 88: Scala performance под капотом

squaresFold hottest methods

Page 89: Scala performance под капотом

squaresFold hottest methods

● java.lang.Object::<init> ест 17% CPU● похоже, мы активно плодим объекты:

Page 90: Scala performance под капотом

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

Page 91: Scala performance под капотом

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

Page 92: Scala performance под капотом

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

Page 93: Scala performance под капотом

Сравнили теплое с мягким

Тесты не идентичны:

примитив

объект

Page 94: Scala performance под капотом

Specialization: тернистый путь

@specialized - оптимизация scalac для избежания боксинга

Page 95: Scala performance под капотом

Specialization: тернистый путь

@specialized - оптимизация scalac для избежания боксинга:● для требуемого примитивного типа

создается своя копия метода:

● хотели как лучше, а вышло как всегда.

Page 96: Scala performance под капотом

Мама, я генерик

● scala.collection.*:○ не умеют в специализацию;○ являются генериками, List[T] = List[Object]○ все методы - тоже генерики,

list.fold(value:T)(...) == list.fold(value:Object)(...)

Page 97: Scala performance под капотом

Мама, я генерик

● 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

Page 98: Scala performance под капотом

Коллекции и примитивные типы

● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.

Page 99: Scala performance под капотом

Коллекции и примитивные типы

● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.

● Коллекции в скале пока дженерики:○ даже scala.Array этим иногда страдает[1];○ боксинг и тормоза во все поля;○ в далеких планах минибоксинг @miniboxed[2].

[1]: http://www.scala-lang.org/api/current/index.html#scala.Array[2]: http://scala-miniboxing.org

Page 100: Scala performance под капотом

Коллекции и примитивные типы

● хочешь быстрой работы с примитивами?○ опасайся боксинга;○ или перепиши все на С/С++.

● Коллекции в скале пока дженерики:○ даже 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

Page 101: Scala performance под капотом

О коллекциях

● Если очень хочется, то жить можно● Есть коллекции для примитивов: debox● Можно написать свою: @specialized

Page 102: Scala performance под капотом

О коллекциях

● Если очень хочется, то жить можно● Есть коллекции для примитивов: debox● Можно написать свою: @specialized

Page 103: Scala performance под капотом

В итоге

● Scala медленная: ○ легко написать тормозной, но красивый код;○ коллекции не дружат с примитивами;○ scalac может нагенерить сумрачный байткод.

Page 104: Scala performance под капотом

В итоге

● Scala медленная: ○ легко написать тормозной, но красивый код;○ коллекции не дружат с примитивами;○ scalac может нагенерить сумрачный байткод.

● Scala быстрая:○ при понимании внутренностей, красивый код

может работать со скоростью Java;○ коллекции можно подружить с примитивами при

помощи синей изоленты (а в Java - нельзя);○ JVM из сумрачного байткода может сделать

эффективный машинный код.

Page 105: Scala performance под капотом

Личный опыт

● Пишем на скале два года и ничё.● Баланс быстроты кода и разработки:

○ пишешь быстро - работает медленно; ○ пишешь медленно - работает быстро.

● Все бенчмарки доступны на гитхабе:○ https://github.com/shuttie/scala-perf-talk

Page 106: Scala performance под капотом

Вопросы?

Page 107: Scala performance под капотом
Page 108: Scala performance под капотом

Коллекции и объекты

● найти сумму длин всех строк● java.util.ArrayList vs scala.Array

Page 109: Scala performance под капотом

Scala collections

Page 110: Scala performance под капотом

Страх и ненависть в scala.Array

● scala.Array + ФП-примочки:○ Array + ArrayOps○ WrappedArray○ ArraySeq○ ArrayBuffer

Page 111: Scala performance под капотом

Как жить с примитивными типами

Debox сильно упрощает жизнь:● хитрый, канонический и наивный пример

Page 112: Scala performance под капотом

Примитивы, результат тестов

Page 113: Scala performance под капотом

Примитивы, результат тестов

● debox ~= while-цикл, yay!● baseline ~3 нс*● обработка 1 элемента - ~0.225 baseline.

* - 2.7GHz Core i5-3337U

Page 114: Scala performance под капотом

Немного x86_64 assembly

Page 115: Scala performance под капотом

Немного x86_64 assembly

● почему все инструкции повторяются?● loop-unrolling и out-of-order execution!

Page 116: Scala performance под капотом