Замыкания, feedback-векторы и полиморфизм точки вызова
У каждого замыкания свой feedback vector, но байт-код общий через SharedFunctionInfo. Вызов через переменную образует call IC: monomorphic для одной callee, megamorphic для многих. Горячий диспетчер с многими идентичностями функций нельзя заинлайнить.
Вы построили чистый диспетчер: один горячий цикл, вызывающий handler(event), где handler — это какой угодно из сорока колбэков, который вернул реестр. Это элегантно — и медленно, медленнее, чем сорок отдельных циклов. Причина не в вашей логике; причина в том, что V8 следит, какая функция проходит через эту единственную точку вызова, и, увидев сорок разных идентичностей функций, сдаётся в попытке предсказать и заинлайнить, откатываясь к общему косвенному вызову на самой горячей строке вашей программы.
Общий код, приватная обратная связь
Почему одно и то же тело функции иногда оптимизируется хорошо в одном месте и остаётся медленным в другом? Ответ в том, что каждый экземпляр замыкания ведёт свою приватную запись того, что видел во время работы — и именно по ней TurboFan компилирует. Когда вы создаёте много экземпляров одной функции — каждую стрелку, возвращённую из фабрики, каждое замыкание, запушенное в цикле — они не несут каждый свою копию байт-кода. Они делят один SharedFunctionInfo (SFI): байт-код, ScopeInfo (информацию об областях видимости), метаданные исходника. Что каждый экземпляр замыкания несёт отдельно — это его feedback vector (вектор обратной связи): массив на экземпляр, куда движок записывает наблюдения за типами во время работы для точек вызова и доступов к свойствам этого экземпляра.
Это разделение важно для производительности. Общий SFI держит память низкой и позволяет оптимизатору скомпилировать тело один раз. Feedback vector на экземпляр означает, что два замыкания из одной фабрики могут специализироваться по-разному, если им скармливают разные типы — и означает, что замыкание начинает собирать полезную обратную связь лишь после того, как этот экземпляр выполнился достаточно раз.
Точки вызова тоже образуют inline cache
Вы уже знаете, что чтения свойств образуют inline cache, ключуемые по hidden class (см. урок про mono/poly/mega в юните о hidden-классах). Вызовы делают то же. Выражение вызова вида fn(x) — где fn это переменная или свойство, держащее функцию — это точка вызова со своим IC-слотом в feedback vector. Этот слот фиксирует, какая функция (какая идентичность callee) здесь вызывалась:
- Monomorphic — точка вызывала только одну идентичность функции. V8 может спекулировать жёстко: он знает точную цель и может заинлайнить её тело.
- Polymorphic — небольшое число (до ~4) различных callee. V8 держит маленькую таблицу и ветвится между ними.
- Megamorphic — слишком много различных callee. V8 перестаёт отслеживать идентичности и выпускает общий косвенный вызов через то, что
fnдержит сейчас.
Почему инлайнинг — это вся игра
Инлайнинг — это оптимизация, которая открывает остальные. Когда TurboFan инлайнит monomorphic callee в вызывающего, он затем может свернуть константы через границу, устранить накладные расходы на передачу аргументов и специализировать слитый код по наблюдаемым типам. Megamorphic точка вызова блокирует всё это: TurboFan не знает, какая функция выполнится, поэтому обязан выпустить косвенный вызов — положить аргументы, прыгнуть через указатель на функцию, без инлайнинга, без оптимизации через границу. На горячем цикле диспетчеризации эти расходы ложатся на каждую итерацию.
// Megamorphic: одна точка, много идентичностей callee.
const registry = { click: onClick, hover: onHover, /* …ещё 38 */ };
function dispatch(type, ev) {
return registry[type](ev); // эта точка видит десятки идентичностей -> megamorphic
}
// Дружелюбно к monomorphic: маршрутизируй на несколько стабильных, отдельно скомпилированных путей.
function dispatchFast(type, ev) {
switch (type) {
case 'click': return onClick(ev); // у каждой точки вызова ОДНА идентичность
case 'hover': return onHover(ev); // monomorphic, инлайнится
default: return onOther(ev);
}
}Версия со switch имеет много точек вызова, каждая monomorphic, вместо одной megamorphic точки. Каждая становится инлайнимой по отдельности. «Элегантная» версия с единой диспетчеризацией концентрирует всё разнообразие идентичностей на одной строке и отравляет её.
Функции высшего порядка: держите колбэки единообразными
Тот же эффект бьёт по map/filter/reduce и любому хелперу высшего порядка. Общий хелпер, вызываемый каждый раз с другим колбэком, имеет внутри одну точку вызова (cb(item)), которая видит много идентичностей — megamorphic. Он всё ещё работает, но не может заинлайнить колбэк. Паттерны, которые держат его быстрым:
- Передавайте ту же идентичность колбэка, когда можете, а не свежую инлайн-стрелку на каждый вызов. Литерал
arr.map(x => f(x))создаёт новую идентичность функции каждый вызов;arr.map(f)переиспользует одну. - Специализируйте горячие хелперы. По-настоящему горячая свёртка над числами может быть рукописным циклом с заинлайненной операцией, а не общим
reduce(fn), чья точкаfn(acc, x)megamorphic. - Держите формы аргументов единообразными тоже. Даже точки с monomorphic callee деоптятся, если аргументы дико меняют hidden class; важны и единообразные callee, и единообразные формы аргументов.
- Различных callee: monomorphic
- 1
- Различных callee: polymorphic
- 2–4
- Различных callee: megamorphic
- ~5+
- Monomorphic вызов
- инлайнится
- Megamorphic вызов
- косвенный, не инлайн.
- Общее на семейство функций
- SharedFunctionInfo
- Приватное на экземпляр замыкания
- feedback vector
Одна горячая строка `registry[type](ev)` диспетчеризует на ~40 разных функций. В какое состояние IC придёт эта точка вызова и что сделает TurboFan?
Расставьте по порядку, что происходит с одной точкой вызова, когда через неё со временем проходит всё больше различных идентичностей функций.
- 1 Первый вызов: IC-слот неинициализирован, фиксирует первую идентичность callee
- 2 Та же идентичность повторяется: точка monomorphic, TurboFan может заинлайнить callee
- 3 Появляется ещё несколько идентичностей: точка становится polymorphic с маленькой таблицей диспетчеризации
- 4 Через неё проходит много идентичностей: точка становится megamorphic — общий косвенный вызов, без инлайнинга
▸Почему это работает
Это можно наблюдать напрямую. Запустите с --trace-ic и отфильтруйте CallIC; деградирующий диспетчер показывает переходы 0 -> 1 -> P -> N (неинициализирован → monomorphic → polymorphic → megamorphic). Подтвердите потерю инлайнинга через --trace-turbo-inlining: monomorphic callee появляется как заинлайненный, megamorphic точка — нет. Для быстрого локального эксперимента %GetOptimizationStatus(fn) под --allow-natives-syntax в d8 скажет, оптимизировался ли сам диспетчер.
- 01Что делят соседние замыкания, а что приватно для каждого, и почему это важно?
- 02Каковы состояния IC точки вызова и что каждое значит для инлайнинга?
- 03Как не дать горячему диспетчеру или хелперу высшего порядка стать megamorphic?
Много экземпляров одной функции делят единственный SharedFunctionInfo — байт-код, ScopeInfo, метаданные — но каждый экземпляр замыкания несёт свой feedback vector, фиксирующий наблюдения во время работы на его точках вызова и доступах к свойствам, поэтому соседние замыкания специализируются независимо и не объединяют обратную связь. Выражение вызова, чья callee живёт в переменной или свойстве, само является точкой вызова с IC-слотом, ключуемым по идентичности callee: monomorphic, когда видна лишь одна идентичность (TurboFan может заинлайнить callee), polymorphic для нескольких (маленькая таблица диспетчеризации) и megamorphic для многих (общий косвенный вызов, который нельзя заинлайнить). Поскольку инлайнинг — это то, что открывает сворачивание констант и специализацию через границу, megamorphic вызов на горячей строке диспетчеризации — настоящий обрыв производительности: накладные расходы косвенного вызова ложатся на каждую итерацию. Лекарства концентрируют разнообразие идентичностей прочь от горячей строки — заменить одну многоцелевую диспетчеризацию несколькими одноцелевыми точками через switch, передавать стабильные идентичности колбэков в хелперы высшего порядка, а не свежие инлайн-стрелки, специализировать по-настоящему горячие свёртки как явные циклы и держать формы аргументов единообразными. Это взгляд со стороны замыканий и областей на ту же машинерию mono/poly/mega, которую юнит о hidden-классах ввёл для доступа к свойствам, теперь применённую к тому, какая функция проходит через точку вызова — наблюдаемо через —trace-ic и —trace-turbo-inlining. Теперь, когда горячий цикл диспетчеризации работает медленнее ожидаемого, проверьте, сколько различных идентичностей функций проходит через его единственную точку вызова — если больше четырёх, у вас megamorphic-бутылочное горлышко и switch, который нужно написать.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.