Деоптимизация: падение с обрыва
Деопт отбрасывает оптимизированный машинный код и возобновляет исполнение в Ignition на точном смещении байт-кода. Eager против lazy deopt, механизм трансляции фрейма, реконструирующий интерпретаторный фрейм
Функция, работавшая на 40 нс за вызов в вашем бенчмарке, вдруг стоит 4 мкс за вызов в продакшене — в сто раз медленнее, чем была бы нескомпилированной. Нет бесконечного цикла, нет блокирующего ввода-вывода, нет шторма GC. То, что вы наблюдаете, — это функция, которую оптимизируют и выбрасывают, оптимизируют и выбрасывают, десятки раз в секунду, потому что один из guards TurboFan продолжает падать на значении, которое он не может перестать видеть. Это цикл деоптимизации, и это самый дорогой способ ошибиться в типах в JavaScript.
Что такое деопт на самом деле
Провал guard (урок 04) не падает с ошибкой и не выдаёт тихо неверный ответ. Он запускает деоптимизацию: V8 отбрасывает оптимизированный машинный код этой функции и возобновляет исполнение в интерпретаторе Ignition на точном смещении байт-кода, соответствующем месту, где оптимизированный код сдался. Исполнение продолжается корректно, просто медленнее. Оптимизированный код отбрасывается (или помечается невалидным), а функция откатывается на нижний ярус; если она остаётся горячей и её обратная связь стабилизируется, она может быть переоптимизирована позже.
Есть две разновидности, различающиеся тем, что инвалидировало предположение.
Eager deopt (немедленный) происходит синхронно, когда guard падает в рантайме — функция исполняет оптимизированный код, доходит до CheckMaps / CheckSmi / CheckBounds, проверка падает, и управление откатывается прямо там. Триггер локальный: этот вызов увидел значение, которого спекуляция не допускала. В --trace-deopt появляется как DEOPT eager с причиной вроде wrong map или not a Smi.
Lazy deopt (ленивый) происходит, когда предположение инвалидируется извне, а не собственным исполнением функции. Вы переопределяете функцию, которую оптимизированный код встроил, изменяете прототип, от которого он зависел, добавляете свойство к разделяемому объекту или меняете глобал. V8 не может залезть во фрейм, который, возможно, исполняется, поэтому вместо этого он помечает весь зависимый оптимизированный код на деопт при следующем входе — код «лениво» деоптится в следующий раз, когда был бы вызван. В --trace-deopt появляется как DEOPT lazy. Классическая причина — monkey-patching: Array.prototype.map = ... после того, как горячий код был оптимизирован под оригинал.
Трансляция фрейма: как он возвращается чисто
Когда guard падает, V8 должен возобновиться в интерпретаторе — но два фрейма говорят на совершенно разных языках. Сложная часть деопта в том, что оптимизированный фрейм и интерпретаторный фрейм — это совершенно разные раскладки. Оптимизированный код держит значения в машинных регистрах и плотном стек-фрейме, часто в распакованных (unboxed) представлениях (сырой int32, сырой float64). Интерпретатор ожидает значения в собственном регистровом файле («регистры» интерпретатора — это слоты стека) в их тегированной, забоксенной форме. Чтобы возобновиться в интерпретаторе, V8 должен перестроить фрейм интерпретатора из оптимизированного.
Он делает это с помощью метаданных деопта (deopt metadata), записанных на компиляции: для каждой возможной точки деопта TurboFan хранит описание, отображающее каждую живую локацию оптимизированного фрейма (этот машинный регистр держит локальную i, этот слот стека держит total, это распакованный double, который надо перебоксить) на интерпретаторный регистр, которому она принадлежит. В момент деопта deoptimizer читает эти метаданные и выполняет трансляцию фрейма (frame translation) — он выделяет и заполняет свежий фрейм интерпретатора, помещая каждое живое значение туда, где его ожидает Ignition, перебоксивая распакованные значения, затем прыгает в интерпретатор на сохранённое смещение байт-кода. Трансляция одного деопта дёшева, порядка микросекунд.
Цикл деоптимизации: настоящая катастрофа
Один деопт — это нормально: микросекунды учётной работы, затем функция заново разогревается с обратной связью, теперь включающей неожиданный случай, переоптимизируется с более широким предположением и остаётся оптимизированной. Катастрофа — это цикл деоптимизации (deopt-loop): функцию раз за разом кормят формой или типом, который она не может удержать стабильным, так что она оптимизируется, деоптится, переоптимизируется (функция всё ещё горячая, так что V8 продвигает её снова), деоптится опять — бесконечно. Каждый цикл платит трансляцию деопта плюс свежую компиляцию TurboFan (десятки-сотни мс) плюс время, проведённое в медленном ярусе между ними. Функция в цикле деоптимизации на порядки медленнее, чем если бы вы просто отключили оптимизацию через --no-opt.
Когда вы видите в —trace-deopt функцию, мечущуюся между оптимизированным и деоптимизированным состоянием, ищите один из этих паттернов — каждый способ заставить guard падать на повторяющемся значении:
- Переполнение Smi (Smi overflow) — арифметический сайт, оптимизированный на
SignedSmall, производит значение за пределами диапазона Smi (больше ~2^30), проваливаяCheckSmiкаждый раз, когда большое значение повторяется. - Смена формы / расхождение hidden class — место вызова видит объекты с расходящимися картами;
CheckMapsпадает. Добавление свойств в разном порядке, условные поля,delete. - Смена типа поля — поле, оптимизированное как Smi, позже держит строку или объект, так что загрузки, охраняемые представлением поля, деоптятся.
- Чтение
argumentsв оптимизированных функциях — исторически форсировало деопт или блокировало оптимизацию; современный V8 обрабатывает многие случаи, но rest-параметры всё ещё безопасный выбор. - Выход за границы или дырявый доступ — провал
CheckBoundsили переход упакованного массива в дырявый elements kind.
Диагностируйте через --trace-deopt (каждый деопт с его функцией, смещением байт-кода и причиной) и, в d8 под --allow-natives-syntax, %GetOptimizationStatus(fn), чтобы прочитать биты яруса функции. Урок браузерного трека спекулятивный движок TurboFan и ловушка цикла деоптимизации проходит полную трассировку-и-починку; здесь упор на механизм и паттерн починки: отвести патологическое значение на отдельный медленный путь до горячего блока, чтобы быстрый путь оставался стабильным по типам.
- Трансляция одного деопта
- ~микросекунды
- Переоптимизация (TurboFan)
- десятки-сотни мс
- Замедление цикла деопта
- на порядки
- Триггер eager deopt
- guard падает в рантайме
- Триггер lazy deopt
- внешняя инвалидация
- Диапазон Smi
- ~31-битный знаковый
- Флаг диагностики
- --trace-deopt
- Интринсик статуса
- %GetOptimizationStatus(fn)
Вы делаете monkey-patch `Array.prototype.includes` в рантайме, после того как горячая функция, использовавшая его, была оптимизирована TurboFan. Какой вид деопта это вызывает?
Почему цикл деоптимизации медленнее, чем исполнение той же функции с `--no-opt` (оптимизация целиком отключена)?
Расставьте, что происходит во время одного eager deopt.
- 1 Оптимизированный код исполняется и доходит до guard (например, CheckSmi)
- 2 Guard падает: значение нарушает спекулируемый тип
- 3 Deoptimizer читает метаданные деопта времени компиляции
- 4 Он перестраивает интерпретаторный фрейм, перебоксивая живые значения в регистры интерпретатора
- 5 Исполнение возобновляется в Ignition на сохранённом смещении байт-кода
▸Частая ошибка
Тонкий цикл деоптимизации прячется в «защитном» коде, возвращающем смешанные типы: парсер, возвращающий число для валидного входа, но null для невалидного, питающий горячий арифметический сайт. Для 99% входов сайт Smi-стабилен и оптимизируется; редкий null проваливает CheckSmi и деоптит; сайт переоптимизируется снова на Smi, ведь null редок; следующий null деоптит опять. Починка — не «обрабатывать null быстрее», а разделить поток: валидировать и отбрасывать null до горячего цикла, чтобы арифметический сайт видел только числа.
- 01Различите eager и lazy deopt с триггером для каждого.
- 02Объясните трансляцию фрейма и зачем она нужна.
- 03Что такое цикл деоптимизации, почему он катастрофичен и как его починить?
Деоптимизация — это предохранительный клапан, удерживающий спекулятивную оптимизацию корректной. Когда предположение ломается, V8 отбрасывает оптимизированный машинный код и возобновляет исполнение в интерпретаторе Ignition на точном смещении байт-кода, где оптимизированный код сдался, — исполнение остаётся корректным, просто медленнее. Eager deopt (немедленный деопт) синхронен: собственный оптимизированный код функции доходит до падающего guard (CheckMaps ‘wrong map’, CheckSmi ‘not a Smi’, CheckBounds ‘out of bounds’). Lazy deopt (ленивый деопт) внешний: переопределение встроенной функции, изменение прототипа или смена глобала инвалидируют зависимый оптимизированный код, который V8 помечает на деопт при следующем входе, ведь не может пропатчить исполняющийся фрейм. Чистый возврат возможен благодаря трансляции фрейма (frame translation): TurboFan записывает метаданные деопта, отображающие каждое живое значение оптимизированного фрейма (в регистрах, часто распакованное) на интерпретаторный регистр, которому оно принадлежит, и в момент деопта deoptimizer перестраивает тегированный интерпретаторный фрейм и прыгает на сохранённое смещение. Один деопт стоит микросекунды и является системой, работающей по замыслу. Катастрофа — цикл деоптимизации: повторяющееся значение ломает guard практически на каждом вызове, так что V8 оптимизирует и деоптит без конца, платя свежую компиляцию каждый цикл и оказываясь на порядки медленнее —no-opt. Триггеры включают переполнение Smi, расхождение формы/hidden class, смену типа поля, чтение arguments и дырявый/выходящий за границы доступ. Диагностируйте через —trace-deopt и %GetOptimizationStatus; чините, отводя патологическое значение на медленный путь до горячего блока, чтобы быстрый путь оставался стабильным по типам. Теперь, когда вы видите в продакшене регрессию в 100 раз без очевидной причины — откройте —trace-deopt прежде профайлера: цикл деоптимизации объявит себя сам.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.