Monomorphic, polymorphic, megamorphic — и stub cache
Машина состояний IC: uninitialized → premonomorphic → monomorphic (1 map) → polymorphic (2-4 map, сканируемый список) → megamorphic (≥5 map, обращение к глобальному megamorphic stub cache, хеш-зонд намного медленнее и враждебный к JIT).
Обзор в 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. Всё ещё быстро, но цепочка ветвлений растёт с каждой формой. - megamorphic — 5 или более map. V8 полностью прекращает отслеживать map в этой точке. Слот помечается megamorphic, и поиски проваливаются в megamorphic stub cache.
Megamorphic stub cache: хеш-таблица на весь процесс
Это часть, о которой обзор никогда не упоминает. Когда точка megamorphic, V8 не делает свежий поиск свойства на каждый доступ. Вместо этого он обращается к единственной, глобальной (пер-изолят) хеш-таблице — megamorphic stub cache — с ключом (map, имя свойства), хранящей handler. Доступ становится:
- Вычислить хеш из map объекта и имени свойства.
- Зондировать stub cache (это маленькая, фиксированного размера таблица с открытой адресацией — первичная и вторичная таблицы).
- При попадании использовать закэшированный 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, которой доверяли тысячи точек.
- Мономорфная загрузка
- ~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 uninitialized — никогда не выполнялась
- 2 premonomorphic — первый map увиден, ждёт подтверждения переиспользования
- 3 monomorphic — один map, {map, handler}
- 4 polymorphic — от 2 до 4 map, сканируемый список пар
- 5 megamorphic — 5+ map, проваливается в глобальный stub cache
▸Почему это работает
Зачем глобальный stub cache вообще помогает, вместо того чтобы просто делать рантайм-поиск в каждой megamorphic-точке? Потому что, хотя одна точка видит много форм, комбинаций (map, имя), которые трогает вся программа, гораздо меньше, чем число доступов. Кэшировать их в одной общей таблице значит, что во второй раз, когда любая megamorphic-точка грузит свойство x у map M, она получает handler из кэша, а не выводит его заново. Это структура контроля ущерба: она не может восстановить инлайнинг или monomorphic-скорость, но удерживает megamorphic-точку от полного поиска каждый раз.
- 01Пройдите полную машину состояний IC, включая состояние, опущенное обзором, и число map для каждого.
- 02Что такое megamorphic stub cache и почему переход в megamorphic хуже, чем просто «несколько лишних сравнений»?
- 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-уровень. Открой, попробуй, потом открой ответ.
встречается в184
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Что такое отношение: таблицы, строки, ключи и ограниченияjunior
- Ограничения, ключи и типы данных Postgresmiddle
- Нормальные формы, денормализация и почему схемы «прилипают»middle
- JSONB, массивы и когда side table побеждаетmiddle
- Heap-хранилище, TOAST и выравнивание колонокsenior
- Целостность схемы: deferral, версионирование и сбои в продакшнеsenior
- Реляционная модель vs документные, wide-column, граф и key-valuesenior
- Index-only scan, Visibility Map и INCLUDEsenior
- Типичные сбои в продакшне и аудит индексовsenior
- pg_statistic, ANALYZE и производственная наблюдаемостьmiddle
- Производственные режимы отказа и стабильность плановsenior
- MVCC: как Postgres раздаёт согласованные снимкиjunior
- Заголовок tuple и механика снимковmiddle
- HOT-обновления и уровни изоляцииmiddle
- VACUUM, bloat и autovacuummiddle
- CLOG, XID wraparound и MultiXactsenior
- SSI и production-тюнинг autovacuumsenior
- Реальные провалы MVCC, deployment-паттерны и распределённые снимкиsenior
- Connection pool: зачем амортизировать стоимость backend Postgresjunior
- Режимы PgBouncer: session, transaction и statementmiddle
- Размер пула: формула (ядра × 2) + шпинделей и двухуровневый стекmiddle
- Исчерпание пула и idle-in-transaction: сценарий отказа в 3 ночиmiddle
- Миграция на transaction mode: план развёртывания и prepared statements в PgBouncer 1.21middle
- Процессная модель Postgres и почему увеличение max_connections снижает производительностьsenior
- Ландшафт пулеров 2026, serverless connection storms и полная таксономия отказовsenior
- Что такое миграция схемы и почему она заменяет ad-hoc DDLjunior
- ADD COLUMN: мгновенно в PG 11+ против перезаписи в старом Postgresjunior
- Режим отказа очереди блокировок: почему мгновенный DDL может заморозить базуmiddle
- Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операцийmiddle
- Expand-contract: нулевой простой для ломающих изменений схемыmiddle
- Advisory-блокировки, инструменты миграций и координация деплояsenior
- Таксономия сбоев миграций и дисциплина продакшнаsenior
- Зачем нужно шардирование: потолок одного Postgresjunior
- Выбор ключа шарда: стратегии hash, range, list и directorymiddle
- Партиционирование против шардирования: одно слово, два разных понятияmiddle
- Ко-локация и Citus: инвариант, делающий шардирование пригодным к использованиюmiddle
- Режим отказа hot shard: обнаружение, изоляция и долгосрочная политикаmiddle
- Schema-based шардирование и альтернативы мультиарендностиsenior
- Онлайн-решардинг, 2PC и операционная стоимость шардированияsenior
- Семь актов: от CREATE TABLE до Citusjunior
- Акты 1–3 в глубину: схема, индексы и статистика планировщикаmiddle
- Акты 4–6 в глубину: MVCC bloat, connection pooling и безопасные миграцииmiddle
- Акт 7 в глубину: шардинг, co-location и семиуровневый каскад трейдоффовmiddle
- Наблюдаемость, антипаттерны и производственный триажsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.