open atlas
↑ К треку
Внутренности движка JavaScript JSE · 03 · 05

Monomorphic, polymorphic, megamorphic — и stub cache

Машина состояний IC: uninitialized → premonomorphic → monomorphic (1 map) → polymorphic (2-4 map, сканируемый список) → megamorphic (≥5 map, обращение к глобальному megamorphic stub cache, хеш-зонд намного медленнее и враждебный к JIT).

JSE Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Обзор в browser/03-v8-internals/04-inline-caches дал вам версию из четырёх слов: mono → poly → mega, и «megamorphic медленный». Но когда IC становится megamorphic, куда на самом деле уходит поиск? Он не просто пожимает плечами и делает поиск по имени каждый раз — у V8 есть глобальная, общая на весь процесс хеш-таблица, megamorphic stub cache, которую разделяет каждая megamorphic-точка на планете. Этот урок проходит полную машину состояний и вскрывает тот общий кэш плюс validity cell, которые охраняют prototype-chain IC.

Полная машина состояний, а не три слова

Слот load IC проходит больше состояний, чем намекал обзор. Полная лестница, односторонняя к megamorphic:

  • uninitialized — слот никогда не выполнялся.
  • premonomorphic — состояние V8 «видел раз, жду подтверждения». Самое первое выполнение часто записывает map здесь, ещё не выпуская специализированный handler, так что одноразовый путь не платит за IC, который никогда не переиспользует. Второе попадание с тем же map повышает до monomorphic.
  • monomorphic — ровно один map. Слот — это {map, handler} (урок 04). При попадании: загрузить map, сравнить, применить handler — пара инструкций. Это целевое состояние.
  • polymorphicот 2 до 4 map. Слот становится маленьким списком пар {map, handler}. Load сканирует список (несколько сравнений) и применяет совпавший handler. Всё ещё быстро, но цепочка ветвлений растёт с каждой формой.
  • megamorphic5 или более map. V8 полностью прекращает отслеживать map в этой точке. Слот помечается megamorphic, и поиски проваливаются в megamorphic stub cache.

Megamorphic stub cache: хеш-таблица на весь процесс

Это часть, о которой обзор никогда не упоминает. Когда точка megamorphic, V8 не делает свежий поиск свойства на каждый доступ. Вместо этого он обращается к единственной, глобальной (пер-изолят) хеш-таблице — megamorphic stub cache — с ключом (map, имя свойства), хранящей handler. Доступ становится:

  1. Вычислить хеш из map объекта и имени свойства.
  2. Зондировать stub cache (это маленькая, фиксированного размера таблица с открытой адресацией — первичная и вторичная таблицы).
  3. При попадании использовать закэшированный handler. При промахе сделать полный рантайм-поиск и вставить результат.

Так что megamorphic-точка — это хеш-зонд плюс вероятное применение handler — намного медленнее, чем monomorphic-сравнение-и-load (десятки-низкие сотни циклов против ~1), и поскольку таблица общая и фиксированного размера, разные megamorphic-точки вытесняют записи друг друга. Стоимость не только в зонде; она в том, что точку больше нельзя специализировать.

Почему megamorphic враждебен к JIT, а не просто медленнее

Более глубокая стоимость — что это делает с TurboFan. Monomorphic- или низко-polymorphic-точка даёт оптимизатору маленький, известный набор форм, так что он может заинлайнить load поля (или весь метод) и защититься дешёвой проверкой map. Megamorphic-точка не даёт TurboFan никакой пригодной информации о форме — feedback — это «что угодно» — так что TurboFan должен выпустить общий вызов к машинерии stub cache и не может заинлайнить доступ или что-либо достижимое через него. Один megamorphic-доступ к свойству в горячем заинлайненном методе поэтому может заблокировать целую цепочку оптимизаций, стоя намного больше, чем один лишь хеш-зонд на доступ.

Prototype-chain IC и validity cell

Многие реальные load’ы находят свойство не на самом объекте, а на его прототипе (методы живут на Class.prototype). Prototype-chain IC кэширует «свойство на N уровней вверх по цепочке, по этому смещению» — но это безопасно лишь пока цепочка прототипов не изменилась. V8 охраняет это validity cell: маленькой ячейкой кучи, разделяемой map’ами, зависящими от данного прототипа, которая «валидна», пока кто-то не мутирует тот прототип (добавит/удалит свойство на нём или переприсвоит его). Быстрый путь prototype IC: проверь map, проверь, что validity cell всё ещё валидна, затем грузи.

Зубы: мутация прототипа после того, как объекты существуют, инвалидирует validity cell, что деоптимизирует каждый IC и каждую скомпилированную TurboFan’ом функцию, опиравшуюся на неё. Поэтому monkey-patching Array.prototype или присваивание SomeClass.prototype.method во время работы (после того как горячие пути разогрелись) может вызвать внезапное, ощущаемое как глобальное замедление — вы задели validity cell, которой доверяли тысячи точек.

Состояния IC и stub cache (V8)
Мономорфная загрузка
~1 цикл (сравнение + load)
Полиморфный
2-4 map, скан короткого списка
Порог megamorphic
5 различных map в точке
Поиск megamorphic
хеш-зонд глобального stub cache (десятки-сотни циклов)
Область stub cache
пер-изолят, фиксированный размер, общий для всех mega-точек
Страж prototype IC
validity cell (инвалидируется при мутации proto)
Осмотр
--trace-ic печатает MONO/POLY/MEGA на точку
Викторина

Точка доступа к свойству стала megamorphic. Что теперь делает доступ механически?

Викторина

Приложение разогревается, затем библиотека делает `Array.prototype.last = function(){...}` во время работы. Горячий код по массивам внезапно замедляется повсеместно. Почему?

Расставь шаги по порядку

Расставьте по порядку состояния IC, через которые проходит точка load, наблюдая 1, затем 2, затем 5 различных map.

  1. 1 uninitialized — никогда не выполнялась
  2. 2 premonomorphic — первый map увиден, ждёт подтверждения переиспользования
  3. 3 monomorphic — один map, {map, handler}
  4. 4 polymorphic — от 2 до 4 map, сканируемый список пар
  5. 5 megamorphic — 5+ map, проваливается в глобальный stub cache
Почему это работает

Зачем глобальный stub cache вообще помогает, вместо того чтобы просто делать рантайм-поиск в каждой megamorphic-точке? Потому что, хотя одна точка видит много форм, комбинаций (map, имя), которые трогает вся программа, гораздо меньше, чем число доступов. Кэшировать их в одной общей таблице значит, что во второй раз, когда любая megamorphic-точка грузит свойство x у map M, она получает handler из кэша, а не выводит его заново. Это структура контроля ущерба: она не может восстановить инлайнинг или monomorphic-скорость, но удерживает megamorphic-точку от полного поиска каждый раз.

Вспомните перед уходом
  1. 01
    Пройдите полную машину состояний IC, включая состояние, опущенное обзором, и число map для каждого.
  2. 02
    Что такое megamorphic stub cache и почему переход в megamorphic хуже, чем просто «несколько лишних сравнений»?
  3. 03
    Что такое validity cell и как мутация прототипа вызывает широкое замедление?
Итог

Inline cache поднимается по односторонней лестнице: uninitialized → premonomorphic (состояние, пропущенное обзором, где V8 записывает первый map, но ждёт подтверждения переиспользования перед специализацией) → monomorphic (ровно один map, слот {map, handler}, читаемый за ~1 цикл) → polymorphic (от 2 до 4 map, короткий сканируемый список пар) → megamorphic (5 или более map). На megamorphic V8 отказывается от пер-точечного отслеживания map и маршрутизирует поиски через megamorphic stub cache: единственную пер-изолятную фиксированного размера хеш-таблицу с ключом (map, имя), разделяемую всеми megamorphic-точками, так что доступы становятся хеш-зондами, а несвязанные точки вытесняют друг друга. Более глубокая кара в том, что megamorphic-feedback не несёт формы, так что TurboFan не может заинлайнить или специализировать доступ — один такой load может заблокировать цепочку оптимизаций. Prototype-chain IC охраняются validity cell; мутация прототипа после разогрева инвалидирует общую ячейку и деоптимизирует каждый зависимый IC и скомпилированную функцию разом. Поскольку состояния односторонние, нельзя «раз-megamorphic’ить» живую точку — её чинят выше по потоку, держа формы стабильными и создание консистентным, чтобы каждая горячая точка видела один map. Это напрямую питает потребление type-feedback TurboFan’ом в юните 04. Теперь, когда увидишь функцию, которая не выходит на верхний ярус или постоянно деоптируется, первая остановка — --trace-ic: найди точку, перешедшую в MEGA, проследи формы до места расхождения и почини порядок создания там.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки
встречается в184

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.