Open nin-jin opened 3 years ago
Солидный кейс
Здесь важно знать, что как минимум в V8 результаты бенчмарка зависят от количества полей в объекте.
В V8 для полей объектов есть несколько внутренних представлений: in-object properties, fast properties, slow/dictionary properties (более подробно: https://v8.dev/blog/fast-properties), различающихся по стоимости добавления поля и чтения значения поля. Поэтому сравнивать скорости работы с полями при разных способах создания объекта надо в зависимости от количества операций добавления полей в него: https://jsfiddle.net/osf3rgL6/
В моём экземпляре Chrome 95 хорошо виден переход к slow properties на двадцатой операции добавления поля в объект:
Ну и на 128 полях даже solid-объект (в терминах оригинального бенчмарка) всё равно переходит к slow properties:
В Firefox 93 такого различия нет, но зато видны ступенчатые изменения скорости после 8, 16 и 32 полей в объекте:
А вот ещё вариант с использованием https://github.com/sindresorhus/to-fast-properties. Скорее всего, здесь мы видим скорость работы Object.keys()
для всех трёх представлений (in-object properties, fast properties, slow/dictionary properties) в V8:
https://habhub.hyoo.ru/#!author=nin-jin/repo=HabHub/article=42/
Вы решительно все перепутали. 1) Мономорфность и полиморфность в рамках выполнения функции говорит о том, что возвращает функция. Но не о том, с чем эта функция работает. Параметры передаваемые в функцию, а точнее точки работы с ними обладают своими качествами полиморфности мономорфности и так далее. И эти качества никак на полиморфности или мономорфности самой функции не отражаются. Функция может оставаться мономорфной принимая параметр который будет мегаморфным.
2) JIT берется за оптимизации сразу. Есть целый ряд оптимизаций, которые выполняются вне зависимости от собранной статистики. И работа Array.prototype.reduce очень показательный тому пример.
3.1) При прогоне по первому массиву, вы получили: a) самую низкопроизводительную форму массива, b) оптимизированный вариант функции get s, и c) инлайн оптимизированного варината в тело функции reduce.
3.2) По второму массиву вы получили тоже самое с той лишь разницей, что прошли через три дополнительных цикла оптимизации геттера.
3.3) По третьему массиву тоже самое что и по второму, но на 5 варинате hiddenclass была исключена оптимзация которая делала inline кода геттера в тело редьюсера. При этом само тело геттера все так же оптимизировано.
4) Полевой кейс. Class A у вас при своем создании, генерирует 10 hidden class ов против одного в случае класа B и в случае С. Очевидно пояснять почему это медленно ненужно.
5) Фабричный кейс у вас явно содержит какую то ошибку. Самым быстрам должен быть Initializer. Empty должен генерировать три лишних хидден класса и быть вторым. Ну а с Object.assign и так все ясно. Он может быть оптимизирован только в случае если такие оптимизиации для него специально написаны.
6) Солидный кейс. Chaine вы создали в режиме словаря, самого медленного представления обьектов в V8. JSON.parse всегда создает обьекты в режиме fatsproperty самого быстрого представления обьектов в v8
4 - инициализаторы уже починили в v8. 5 - Object.assign в v8 тоже здорово ускорили.
@demimurych
Мономорфность и полиморфность в рамках выполнения функции говорит о том, что возвращает функция. Но не о том, с чем эта функция работает. Параметры передаваемые в функцию, а точнее точки работы с ними обладают своими качествами полиморфности мономорфности и так далее. И эти качества никак на полиморфности или мономорфности самой функции не отражаются. Функция может оставаться мономорфной принимая параметр который будет мегаморфным.
Первый раз вижу такую трактовку. Например, в https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html это описано так:
If we continue calling f with objects of different shapes it’s degree of polymorphism will continue to grow until it reaches a predefined threshold - maximum possible capacity for the inline cache (e.g. 4 for property loads in V8) - at that point cache will transition to a megamorphic state.
То есть мономорфность/полиморфность/мегаморфность кода функции - именно зависимость от того, что передаётся в функцию.
JIT за свои оптимизации берётся не сразу, а только после некоторого числа запусков одной и той же функции
JIT берется за оптимизации сразу
Никакого противоречия, а только недопонимание - JIT действительно включается не сразу, а только когда код становится горячим, то есть после некоторого числа запусков функции. До тех пор код выполняется в интерпретаторе. Но когда JIT начинает работать, он сразу оптимизирует то, что может.
Прив. Я прошу прощения что задержался с ответом. Как то все события так совпали что минута появилась только сейчас.
Я знаю этот материал. Вячеслав Егоров был лучом света в царстве JS болота. И к сожалению, уже очень давно он не занимается V8. Когда то давно, если я верно помню, он был брошен на Dart. Ну а печальную историю Dart наверняка всем на ночь рассказывают.
Документ устарел в деталях, но при этом сохранил свою информативность в плоскости описания общего (для подавляющего числа рантаймов) алгоритма инлайн кешей.
Вопросы мономорфности, в рамках таких документов, всех сбивают с толку своей неоднозначной трактовкой как раз в части терминологии. Откуда и возник термин - полиморфность / мономорфность функции, которые как минимум не до конца корректен, и как максимум допускает двоякое трактование
⧼Суть⧽ Строго говоря, термин monomorphic polymorphic или megamorphic касается только инлайн кешей.
В терминологии V8 обычно просто пишут IC, а конкретное место в коде, где используется IC называют SLOT. О чем и рассказывается в материале, где термин мономорфная функция *нигде *не используется. Часто пишется мономорфность кода, подразумевая общий принцип формирования кода, но не функцию.
При этом, есть разница в том, как создается тот или иной IC. Типичным примером такой разницы является: IC для доступа к Object property, IC для вызова функции.
О чем также написано в материале Вячеслава Егорова в главе Not all caches are the same. https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html#not-all-caches-are-the-same И это то, о чем я и говорил.
⧼Резюме⧽ То есть первое что нужно для себя всегда держать на подкорке - говоря о мономорфности, мы всегда говорим о состоянии конкретного слота конкретного кеша.
При этом помнить, что работа кеша в разных ситуациях может радикально отличаться. Примером чему является доступ к свойствам обьекта, и вызов функции.
⧼Пример 1⧽ Простой пример иллюстрирующий это: var func = (a,b,c) => a+b; %PrepareFunctionForOptimization( func ); func(1,2,3); %OptimizeFunctionOnNextCall( func ); func(1,2,3); func(1,2,'acsasda');
Вызов func будет мономорфным несмотря на то, что третий параметр то SMI то String. И никакой деоптимизации не будет. Потому, что третий параметр функции, не принимает никакого участия в работе самой функции, как следствие под него не создается slot для ic, как следствие функция остается оптимизированной.
⧼Пример 2⧽ var bcfunc = ( a, b, c ) => a + b + c; var bcmain = ( a, b, c ) => bc__func( a, b, c );
%PrepareFunctionForOptimization( bcmain ); %PrepareFunctionForOptimization( bcfunc ); bcmain( 1, 2, 3 ); %OptimizeFunctionOnNextCall( bcmain ); %OptimizeFunctionOnNextCall( bcfunc ); bcmain( 1, 2, 3 ); bc__main( 1, 2, 'stroka' );
Функция bcfunc и функция bcmain были оптимизированы исходя из того что параметры всегда строго SMI Только в случае кода bcfunc были созданы IC для конкретных типов (SMI) а в случае bcmain были созданы IC для вызова функции bc__func.
При повторном вызове bcmain, где третий параметр вместо SMI стал String мы не получим де-оптимизации bcmain, так как в ее случае IC создавался для мономорфного вызова bcfunc но получим де-оптизацию для кода bcfunc где IC создавались уже с учетом типов передаваемых данных.
чт, 10 февр. 2022 г. в 23:00, Victor Homyakov @.***>:
@demimurych https://github.com/demimurych
Мономорфность и полиморфность в рамках выполнения функции говорит о том, что возвращает функция. Но не о том, с чем эта функция работает. Параметры передаваемые в функцию, а точнее точки работы с ними обладают своими качествами полиморфности мономорфности и так далее. И эти качества никак на полиморфности или мономорфности самой функции не отражаются. Функция может оставаться мономорфной принимая параметр который будет мегаморфным.
Первый раз вижу такую трактовку. Например, в https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html это описано так:
If we continue calling f with objects of different shapes it’s degree of polymorphism will continue to grow until it reaches a predefined threshold
- maximum possible capacity for the inline cache (e.g. 4 for property loads in V8) - at that point cache will transition to a megamorphic state.
То есть мономорфность/полиморфность/мегаморфность кода функции - именно зависимость от того, что передаётся в функцию.
— Reply to this email directly, view it on GitHub https://github.com/nin-jin/HabHub/issues/42#issuecomment-1035511258, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEFIJH54JN7ANXZJYDCYFLU2QROFANCNFSM4YBX5BBA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.
You are receiving this because you were mentioned.Message ID: @.***>
Зачем функции принимать параметр, который в ней не используется, остаётся загадкой.
Не имеет принципиального значения вопрос - зачем. Имеет принципиальное значение то, что понятие момноморфности полиморфности (и т.д.) не касаются функций, но касаются только данных каторые попадают в категорию, для которых делается inline cache.
Потому говорить о мономорфности функции можно только в одной плоскости - в плоскости результата который она возвращает.
Это основная мысль которую я пытался донести.
пт, 11 мар. 2022 г. в 15:27, nin-jin @.***>:
Зачем функции принимать параметр, который в ней не используется, остаётся загадкой.
— Reply to this email directly, view it on GitHub https://github.com/nin-jin/HabHub/issues/42#issuecomment-1065112524, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAEFIJCKOLHJ33UPSN6WPUDU7NC4ZANCNFSM4YBX5BBA . Triage notifications on the go with GitHub Mobile for iOS https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675 or Android https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub.
You are receiving this because you were mentioned.Message ID: @.***>
Зачем функции принимать параметр, который в ней не используется, остаётся загадкой.
Например, есть несколько реализаций API, и в одной из них параметр не нужен, но сохранён для читабельности кода https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgYgVwHYGMoF4oAoCGAuKJBAWwCMIAnAGilIKLMppXpPIoEoMA+QtygNwAoISgD2SAM7AoAM2QoAjAXgKM2HDVLMu6XjigBqWkagph4qTPmoATCsSp1uLTQD6u-adLCgA
https://page.hyoo.ru/#!=btunlj_fp1tum
Здравствуйте, меня зовут Дмитрий Карловский и раньше я.. ежедневно измерял свою пипирку, но у распространённых линеек никак не хватало точности для измерения столь малых размеров.
Поэтому я решил, что хватит это терпеть! .. и выстругал свою с нанометровыми делениями, поддержкой прохладного и разгорячённого измерения, тестами, шарингом и прочими вольностями. Так что приглашаю и вас присоединиться к этой спец олимпиаде по измерению скорости своего JS кода.
Для начала разберём этот кейс:
Наследственный кейс
Тут у нас 3 варианта исполнения:
JIT за свои оптимизации берётся не сразу, а только после некоторого числа запусков одной и той же функции. Поэтому если ваш код исполняется лишь единожды, то ни на какие оптимизации вы можете не рассчитывать. Но если он исполняется много раз, то такой код может быть довольно быстрым. Если, конечно, вы не сломаете оптимизации, например, мегаморфностью.
Так что при измерении скорости кода важно знать и время прохладного запуска (голубая пипирка), и время разгорячённого (оранжевая пипирка), чтобы хорошо понимать на какую скорость вы можете рассчитывать в зависимости от частоты его использования.
Мегаморфность может появиться у вас совершенно случайно. Например, вы создали класс с 10 полями и геттером, их суммирующим:
Теперь добавим пяток наследников:
И создадим 3 массива с разной степенью вариативности типов элементов:
При прогоне редьюсера по первому массиву, у нас всё хорошо - JIT оптимизировал его по самое неболуйся, заинлайнив всё, что возможно. При прогоне по второму массиву из-за полиморфности производительность как прохладного, так и разгорячённого кода падает в 2 раза. А на третьем массиве все оптимизации теряются и разгорячённый код исполняется почти с той же скоростью, что и прохладный.
Отдельно стоит разобрать повторный запуск редьюсера на первом массиве - его максимальная производительность немного просела. Дело тут в том, что не смотря на то, что хоть сам код редьюсера и мономорфный, суммирующий геттер остаётся мегаморфным, ведь он уже запускался на экземплярах разных классов и JIT выпилил его оптимизацию.
И единственный способ это побороть - это создавать по отдельной функции для каждого наследника. Тогда каждый из них будет мономорфным и JIT будет оптимизировать их независимо. Это даст высокую производительность после прогрева, но ценой огромного потребления памяти.
На всякий случай, уточню, что при создании множества замыканий одним и тем же кодом, нативная функция будет создана только одна. А в каждое замыкание движок помещает лишь ссылку на эту функцию и ссылку на текущий контекст её исполнения.
Полевой кейс
Есть и ещё менее очевидные способы всё испортить. Например, воспользоваться инициализаторами полей класса:
Chrome 88 выдаёт следующую картину:
Видимо эту фичу прикручивали в V8 противники преждевременной оптимизации, которые всё ещё ждут подходящего багрепорта, чтобы героически ускорить создание классов в 10 раз. А вот над Firefox 85 работают сторонники своевременной оптимизации:
С другой стороны, как видно из графиков, V8 умеет оптимизировать создание объектов сразу при прохладном старте, а вот Firefox дожидается разгорячённого, но делает это чуть-чуть получше.
Фабричный кейс
И раз уж мы заговорили про создание объектов, то это тоже можно делать сильно по разному. Например, в $mol у нас стояла такая задача: необходимо при старте приложения создавать большое число объектов, при этом у каждого объекта есть несколько десятков методов, часть из которых может быть переопределена при создании. У меня получилось 3 варианта с разной производительностью..
Принимать в универсальном конструкторе словарь с переопределениями методов:
Принимать функцию инициализации вместо словаря:
Ну или попросту оставить конструктор пустым, а переопределения задавать сразу после создания объекта.
Результаты в Chrome 88 несколько противоречивы:
Словарь хуже оптимизируется, зато даже прохладный он работает довольно быстро. А вот в Firefox 85 ситуация существенно иная:
Последний вариант тут оказывается самым быстрым при любом варианте исполнения. А так как Chrome в любом варианте оказывается быстрее, чем Firefox в самом быстром, то ориентироваться лучше именно на Firefox, чтобы показывать приемлемую производительность даже в худшем для нас случае. Поэтому мы остановились варианте c переопределениями после создания экземпляра.
Солидный кейс
Возможно вас смутила копипаста в некоторых примерах. Ведь её же можно заменить на цикл с добавлением свойств. Однако, при пипиркомерстве важно знать, что цикл вовсе не эквивалентен копипасте. Порой он выходит быстрее за счёт оптимизаций. А порой - драматически медленнее. Создадим, для примера, два одинаковых объекта. Первый наполним полями итеративно:
А второй создадим сразу с нужными полями. Чтобы не копипастить, воспользуемся хаком:
Ну, и для полной картины, добавим ещё и словарик:
В Chrome 88 мы видим просто реактивную работу цельносозданного объекта.
Видимо цепочка скрытых классов, которая в этом случае не создаётся, - довольно дорогое удовольствие. А вот в Firefox 85 это не так - тут всё одинаково медленно:
Асинхронный кейс
Наконец, давайте замерим, насколько асинхронный код медленнее синхронного, на примере рекурсивной версии функции Фибоначчи:
В Chrome 88 выглядит это примерно так:
Да, так любимый всеми асинхронный код - это крайне медленно. И пусть вас не обольщают относительно хорошие показатели разгорячённого асинхронного варианта, ибо моя пипиркомерка замеряет лишь синхронную часть асинхронного вызова. Но большая часть работы идёт асинхронно, уже после того, как замер завершён. Firefox 85 же вообще после замера подвисает на десятки минут и выедает всю память.
Натуральный кейс
Ну да ладно, всё это была неэкологичная синтетика. Давайте погоняем что-то натуральное. Например, библиотеки для работы с датами и временем. Возьмём наиболее популярные из них:
И поехали измерять. Начнём с базовой задачи - парсинг строки в формате ISO8601:
Логично, что нативное API быстрее всех, потом идёт dayjs, который является обёрткой над нативным API, и мой велосипед, который уже не является обёрткой и парсит самостоятельно. Потом dateFns, который хоть и возвращает нативный Date, но парсит его зачем-то самостоятельно и, как видим, довольно медленно. Ну и в конце все остальные лузеры.
Штош, замерим и обратную операцию - сериализацию в ISO8601:
Примечательно, что сериализатор JSJoda оказался тут сильно быстрее всех. А вот
dateFns
тут нет, так как она работает с экземплярами нативного Date, а его мы померили самым первым.Для пользователя, однако, обычно используется не ISO8601 представление, а более удобное для человека. Так что замерим и кастомное форматирование:
Тут уже отрыв JSJoda не такой существенный. Всё дело в том, что производительность $mol_time сильнее деградирует по мере усложнения паттерна форматирования. Так что его ещё есть куда улучшать.
А в Firefox 85 JSJoda и вовсе сливает:
Причина, по всей видимости, в том, что создание объектов в Firefox происходит не очень быстро, а в JSJoda упоролись по паттернам. Так бывает, когда оптимизируешь библиотеку лишь под один движок, и совсем забиваешь на альтернативные. Вместо хорошей производительности везде, получается, что на одном движке всё летает, а на другом еле ползает.
Как это работает
Интерфейс моей пипиркомерки состоит из следующих блоков:
Общий код
Располагается слева. Он разделён на часть, которая исполняется до замера, и часть, которая уже после. Первая подходит для общей предварительной инициализации. А вторая - для тестов, ведь важно убедиться, что измеряли время работы мы именно того поведения, что хотели.
Время работы общего кода в результаты, конечно же не входит.
Для инициализации и тестов порой важно знать сколько итераций будет прогоняться измеряемый вариант. Для получения этого числа доступен специальный макрос
{#}
.Например, мы хотим, чтобы для каждой итерации у нас был уникальный объект, но не хотим, чтобы его создание влияло на замеры. Тогда мы можем заранее создать массив из нужного числа объектов:
Вариативный код
Располагается посередине. Каждый вариант состоит из двух частей:
В замеряемом коде так же доступен макрос
{#}
, но тут он означает уже не число итераций, а номер текущей итерации. Например, мы можем взять из ранее объявленного массива уникальный объект и что-то с ним сделать:Результаты
Появляется справа после запуска тестов и исчезает при изменении кода. Для каждого варианта выводит два результата:
Код разгорячённой функции измерения получается примерно таким:
А код прохладной чутка другим:
Как видно, они практически не отличаются по выполняемой работе. Однако, важно отметить, что в замер попадает не только замеряемый код, но и один вызов функции. Это даёт дополнительно примерно 20 наносекунд. Стоит учитывать это при замере экстремально быстрого кода.
Подключение библиотек
Если нужно подключить какую-либо библиотеку, то можно воспользоваться модулем $mol_import, чтобы загрузить её из CDN:
Тесты
Для тестов можно воспользоваться функциями $mol_assert:
Послесловие
perf.js.hyoo.ru - собственно, мой инструмент для нанобенчмаркинга. Примеряйте его к своему коду, не стесняйтесь. Выкладывайте скриншоты с интересными сравнениями - обсудим почему всё именно так.
bench.hyoo.ru - ещё один инструмент, но уже не для нанобенчмаркинга кода, а для бенчмаркинга целых приложений. Я рассказывал о нём в статье: bench.hyoo.ru: готовим JS бенчмарки быстро и просто.
А если вы мейнтейнер какой-либо JS библиотеки, то пишите в личку, если хотите присоединиться к нашему полузакрытому чату, где мы в кругу мейнтейнеров обсуждаем всякие такие, не интересные обычным разработчикам, темы.