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

Preview:

Citation preview

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!

Recommended