(То есть, если вам нужно создать множество объектов в SpiderMonkey, движке JavaScript Firefox)

Это долгий путь, чтобы отследить Ошибку 1262210, в которой говорится, что SpiderMonkey работает хуже, когда конструктор класса пуст, по сравнению с конструктором с некоторыми наборами членов. Как новичок в движке JavaScript, научиться чему-то, начиная от решения странной проблемы с производительностью с помощью Gecko profiler и заканчивая просмотром сборки x64, сгенерированной движком JIT, — это замечательное смешанное приключение. с некоторой неуверенностью, тревогой и разочарованием.

Вот как я это проследил (коряво).

Отслеживание JIT

SpiderMonkey, как и другие популярные движки JavaScript, имеет многоуровневую архитектуру JIT. Вместо того, чтобы преобразовывать и выполнять байт-код после того, как внешний интерфейс анализирует и генерирует его, JIT-движки могут собирать больше информации о типах для оптимизации кода, а также сохранять наиболее часто используемые фрагменты в машинном коде, чтобы не было необходимости повторно компилировать один и тот же байт-код. :

Однако только те преимущества, которые предоставляют нам JIT-движки, делают отладку намного сложнее, чем отслеживание обычной ошибки движка. Например, хотя профилировщик Gecko ясно показал, что разное время выделения и GC между двумя случаями является основной причиной разрыва в производительности, это не сильно помогло, потому что родитель стека — это просто адрес (в частности, 0x10c7e7c22):

Поэтому, хотя я мог бы установить точки останова в этих функциях распределения и GC, чтобы увидеть, что происходит, у меня не будет возможности определить основную причину за пределами родительского адреса, если я не смогу тщательно отследить сборку x64, сгенерированную механизмом JIT. и мудро. Тем не менее, даже я мог, и на самом деле я попробовал это один раз, это был, по-видимому, очень неэффективный процесс. Так что я обратился к iongraph и JIT spew log для генерации MIR-графа, который движок будет строить во время компиляции:

К сожалению, на графике было слишком много информации и метаинформации без четкого объяснения. Несмотря на то, что благодаря команде дизассемблирования в оболочке JS и базовому знанию нашего механизма сборки мусора, график можно приблизительно сопоставить с исходным кодом JavaScript, он все же озадачен тем, как механизмы JIT выполняют код, как показано на графике. На самом деле, для бага не было четкого объяснения, почему движок на самом деле сгенерировал 4 разных графика как выше для одного и того же скрипта, пока я не обратился за помощью по нашему IRC-канал, и получил ответ, что каждый раз, когда аварийное восстановление происходит по разным причинам, зарегистрированным в журнале JIT spew log, текущий график будет полностью устаревшим. Затем будет сгенерирован следующий новый график, когда он повторно войдет в JIT второго уровня, IonMonkey, а затем начнется только с определенных точек, таких как начало блока 1 или линия, отмеченная как начало и osreentry. :

После этого, с дополнительными флагами ведения журнала и самим графом, JIT, наконец, мог быть проверен как хороший для создания нормального кода для демонстрационного случая, и все аварийные ситуации и повторные компиляции не потребовали значительного времени, чтобы вызвать проблему, хотя это все еще не помог мне понять, почему создание объектов из пустого конструктора займет гораздо больше времени выделения и GC.

Так что прямо сейчас дело казалось застопорившимся: до сих пор неизвестно, какой компонент сгенерировал код, заставивший пустой конструктор класса занять больше памяти и времени GC, чем тот, у которого два члена. Слава богу, только тогда я начал подозревать, почему вызовы функций при профилировании дают суммарно намного меньше, чем должно быть: аллокация вызывается всего около 6К раз, а цикл занял ровно 1e8 раз. Где пропавшие 99 994 000 звонков?

После того, как я это заметил, внезапно появилось множество предположений, и я не мог найти других умных методов для проверки этих предположений, кроме как вернуться к точкам останова и журналу консоли. После размещения журналов JavaScript вокруг каждой итерации и журналов C++ в функции распределения, а также нескольких предложений от уважаемых коллег и раскрутки JIT-кадров, чтобы получить очень ограниченную информацию о них, я, наконец, понял, что вызов не промахнулся: его только что оптимизировали и использовали в большинстве итераций, но иногда движок JIT возвращался к более медленному пути, который использует вызов функции VM для вызова метода C++ вместо полностью скомпилированных ассемблерный код в JIT-фреймах. Кроме того, согласно журналу была выявлена ​​схема такого отката:

Журнал все же привел к некоторым неуверенным догадкам. К счастью, Николя указал, что это должна быть проблема GC Nursery, что действительно спасло меня от трассировки вслепую.

Полный питомник и первопричина

В SpiderMonkey общий объект (по типу JSObject) будет сначала размещаться в питомнике, а затем перемещаться в Tenured, если он проживет достаточно долго, точно так же, как что делает классический генерационный GC. Однако для этой ошибки становится более интересным, когда решать, насколько большим должен быть такой объект.

Для объекта с пустым конструктором он выделит в качестве шаблона самую общую вещь размером 8 байт, так что на каждой итерации, независимо от того, является ли текущая JIT-компилятором или нет, он будет копировать этот шаблон для нового объекта. И если это справедливо и для конструктора с элементами, установленными внутри, мы должны получить ожидаемый результат без ошибок, потому что они будут заполнять питомник с той же скоростью, а затем одновременно возвращаться к медленному пути выделения.

Однако на самом деле есть другой способ определить размер объекта с не таким уж пустым конструктором. Несмотря на то, что 8 байт — это базовый размер выделения, наша среда выполнения GC может сгенерировать объект меньшего размера, если конструктор инициализирует все элементы объекта в момент его создания. Объект с таким UnboxedLayout может быть выделен только в том случае, если он создан из предопределенного конструктора или синтаксиса буквального объекта (объект JSON). И в этом баге такой объект мог иметь половинный размер вещи от созданного с помощью пустого конструктора, а это 32 против 64. Следовательно, цикл создает последний будет заполнять ясли быстрее, чем первый, строго в 2 раза.

Резюме, а может быть, Решение?

Лично я научился нескольким важным вещам от JIT до GC питомника:

  1. Профилировщик Gecko — отличный инструмент в качестве антибиотика широкого спектра действия: он с самого начала выявил важную информацию о первопричине, и результат в значительной степени соответствует тому, что он указал.
  2. Iongraph — лучший способ проверить, как IonMonkey компилирует и оптимизирует код (байт-код). Используя его с другим JIT-журналом, можно было проследить весь путь движения JIT-движка во время выполнения.
  3. Сборщик мусора может повлиять на проблемы с производительностью помимо пауз GC: для этой ошибки он также влияет на откат от более быстрого JIT-пути к более медленному пути. Если бы речь шла только о спасении JIT, не было бы возможности выяснить эту первопричину.
  4. Всегда спрашивайте других гуру, прежде чем копаться в головоломках: чаще обновляйте информацию об ошибках и даже догадках на Buzilla, а также задавайте вопросы в IRC — это самые эффективные способы получить отзывы и предложения. Хотя это также показывает тот факт, что нам крайне не хватает хороших документов как в статьях, так и в комментариях.
  5. DXR — самая мощная и мощная служба индексации кода Webby. Я не мог прожить без этого даже дня в нашей гигантской кодовой базе.

Что касается решения, Комментарий 36 фактически указывал на тот факт, что оптимизация может быть затруднена, потому что должна сохраняться предпосылка неупакованного объекта. В настоящее время я пытаюсь спросить, возможно ли сделать некоторую эвристическую оптимизацию для ускорения.