Внутри цикла интерпретатора
Ignition исполняет байт-код циклом диспетчеризации: выбрать опкод, перейти к его обработчику, сделать работу, продвинуть указатель байт-кода, диспетчеризовать следующий. Аккумулятор протягивает значения между обработчиками
У вас есть байт-код. Теперь что-то должно его исполнить — одну инструкцию за другой, миллионы раз в секунду, попутно тихо ведя учёт того, насколько горячей становится каждая функция. Магии здесь нет: это цикл. Выбрать следующий опкод, перейти к коду, который его обрабатывает, сделать работу, продвинуться, повторить. Понимание этого цикла объясняет и почему интерпретируемый JavaScript предсказуем-но-не-быстр, и как движок узнаёт точный момент передать функцию JIT.
Цикл диспетчеризации
Предыдущий урок оставил нас с байт-кодом функции и набором общих обработчиков (по одному на опкод, написанных на Torque). Исполнение — это акт запуска этих обработчиков по очереди. Концептуально:
loop:
opcode = bytecode[pc] // выбрать текущий опкод
handler = handlerTable[opcode] // найти его обработчик
jump handler // сделать работу; обработчик продвигает pc, затем диспетчеризует следующийКаждая итерация выбирает опкод по текущему указателю байт-кода (pc), ищет обработчик этого опкода в таблице и переходит к нему. Обработчик читает свои операнды (из регистрового файла и аккумулятора), выполняет операцию, пишет результат (обычно обратно в аккумулятор), продвигает pc за операнды этой инструкции, а затем диспетчеризует следующую инструкцию. Гоняйте этот цикл до Return — и вы исполнили функцию.
Как переход делается быстрым: threaded-диспетчеризация
Наивный интерпретатор написал бы это как гигантский switch (opcode) внутри while-цикла. Это работает, но у этого есть скрытая цена: после каждого обработчика поток управления возвращается к началу цикла и заново входит в switch, и предсказатель переходов CPU видит один-единственный косвенный переход, общий для всех опкодов — поэтому он предсказывает плохо и тормозит.
Вместо этого Ignition использует threaded-диспетчеризацию: каждый обработчик завершается, напрямую диспетчеризуя следующий обработчик — исторически через вычисляемый goto (один косвенный переход на обработчик), а в Ignition конкретно через tail call из машинного кода одного обработчика прямо в следующий. Выигрыш — для предсказания переходов: точка диспетчеризации каждого опкода — это её собственный косвенный переход, поэтому предсказатель может выучить частого преемника каждого опкода (например, за сравнением обычно идёт ветвление), вместо того чтобы путаться в одном общем переходе. Меньше промахов предсказания — более плотный, быстрый цикл интерпретатора.
Аккумулятор — это то, что делает передачу между обработчиками чистой: вместо явной передачи операндов большинство обработчиков читают и пишут один общий регистр-аккумулятор, так что значение, произведённое одним байт-кодом, лежит ровно там, где его ждёт следующий байт-код. Цикл диспетчеризации и аккумулятор — две половины одного дизайна.
Профилирование во время интерпретации: обратная связь заполняется
Цикл не просто исполняет — он учится. Вспомните, что каждый чувствительный к типам байт-код несёт слот feedback vector. По мере того как эти обработчики работают, они заполняют слоты map (hidden classes) и типами, которые реально наблюдают: этот LdaNamedProperty увидел объект с map M; этот Add увидел два Smi; этот Call всегда нацеливался на функцию F. К тому моменту, как функция поработала какое-то время, её feedback vector — компактный профиль того, как она реально используется — ровно та информация, что нужна оптимизатору. Интерпретатор готовит JIT как побочный эффект исполнения программы.
Interrupt budget: как цикл решает повысить ярус
Как V8 узнаёт, что функция достаточно горячая для компиляции? Он не считает буквально каждую инструкцию. Каждая функция несёт interrupt budget (бюджет прерываний — счётчик обратных рёбер и вызовов, по достижении нуля запускающий компиляцию). Цикл уменьшает этот бюджет на событиях, коррелирующих с нагревом:
- Обратные рёбра — каждый раз, когда управление прыгает назад к началу цикла (одна итерация
for/while). Плотный цикл прожигает бюджет быстро. - Вызовы функций / инвокации — повторный вход в функцию.
Когда бюджет достигает нуля, цикл поднимает запрос на повышение яруса: скомпилировать эту функцию следующим ярусом (Sparkplug, затем Maglev, затем TurboFan — юнит 04). Для цикла, который всё ещё работает, когда он становится горячим, V8 может даже подменить работающий код прямо посреди цикла через on-stack replacement (OSR) (замена на стеке — механизм подмены интерпретируемого фрейма на скомпилированный без остановки исполнения), перепрыгнув из интерпретатора в свежескомпилированный код, не дожидаясь окончания цикла. Обратные рёбра — ключевой сигнал: долго работающий цикл — каноничный триггер «это горячо, оптимизируй сейчас».
- Стиль диспетчеризации в Ignition
- tail-call / threaded
- Значение между обработчиками через
- аккумулятор
- Бюджет уменьшается на
- обратных рёбрах + вызовах
- Бюджет достиг нуля ⇒
- запрос на повышение яруса
- Горячий цикл подменяется на лету через
- OSR
- Интерпретация vs оптимизация: скорость
- медленнее, но предсказуемо
Почему интерпретация сначала — безопасный дефолт
Когда профилируешь функцию и видишь, что она тратит время в интерпретаторе, это не баг — это движок делает ровно то, что должен, пока у него недостаточно данных для спекуляций. Интерпретация медленнее на инструкцию, чем оптимизированный машинный код — на каждом байт-коде есть накладные расходы выборки-и-диспетчеризации. Но она предсказуема: нет паузы на компиляцию (байт-код готов сразу) и нет деопта — интерпретатор не делает спекулятивных предположений о типах, поэтому он не может ошибиться в типах и быть вынужден откатиться. Оптимизированный код быстрее, но может деоптимизироваться обратно в этот цикл, когда предположение ломается (деопт всегда приземляет вас обратно в интерпретатор). Так что Ignition — это и дешёвая стартовая точка, и страховочная сеть, в которую падает JIT. Поэтому каждая функция начинает свою жизнь прямо здесь, в цикле диспетчеризации, и поэтому этот урок стоит на шве между «как исполняется JS» и машинерией JIT юнита 04.
Почему Ignition использует threaded (tail-call) диспетчеризацию вместо одного большого switch по опкоду?
Функция содержит плотный цикл, выполняющийся миллионы итераций. Что в интерпретаторе триггерит V8 скомпилировать её и через какое событие?
Расставьте по порядку одну итерацию цикла диспетчеризации Ignition для одного байт-кода.
- 1 Выбрать опкод по текущему указателю байт-кода (pc)
- 2 Найти обработчик этого опкода в таблице обработчиков
- 3 Перейти к обработчику; он читает операнды и обновляет аккумулятор
- 4 Обработчик продвигает pc и диспетчеризует следующий байт-код
▸Почему это работает
Почему деопт всегда приземляется обратно в интерпретатор, а не на более низкий ярус JIT? Потому что интерпретатор — единственный ярус, не делающий никаких спекулятивных предположений — он обрабатывает каждый тип, каждую форму, каждый краевой случай, определённый спецификацией, просто медленно. Когда оптимизированный код обнаруживает, что его ставка была неверна (значение, которое он считал Smi, оказалось HeapNumber), единственное место, гарантированно работающее корректно с этого точного смещения байт-кода, — это Ignition. Так что цикл диспетчеризации — универсальный запасной вариант: медленный, но всегда правильный. JIT может переоптимизировать позже, как только обратная связь стабилизируется.
- 01Пройдите одну итерацию цикла диспетчеризации Ignition и назовите роль аккумулятора.
- 02Что такое threaded-диспетчеризация и почему она быстрее интерпретатора на switch?
- 03Как цикл интерпретатора решает, что функция горячая, и что происходит дальше?
Исполнение байт-кода — это цикл диспетчеризации: выбрать опкод по указателю байт-кода, найти его обработчик в общей таблице обработчиков, перейти к обработчику (который читает операнды из регистрового файла и аккумулятора, вычисляет и пишет результат обратно в аккумулятор), продвинуть pc и диспетчеризовать следующий — повторяя до Return. Ignition не использует наивный switch; он использует threaded, tail-call диспетчеризацию, так что у каждого опкода своя точка косвенного перехода, которую предсказатель переходов CPU учит куда лучше, чем один общий переход, сокращая промахи. Аккумулятор — это чистая передача между обработчиками, вторая половина дизайна диспетчеризации. Во время интерпретации цикл также заполняет слоты feedback vector hidden classes и типами, которые реально наблюдает, профилируя программу бесплатно, чтобы JIT мог позже специализироваться. Чтобы решить, когда оптимизировать, каждая функция несёт interrupt budget, уменьшаемый на обратных рёбрах и вызовах; когда он достигает нуля, цикл поднимает запрос на повышение яруса (а всё ещё работающий горячий цикл может быть подменён через on-stack replacement). Интерпретация медленнее на инструкцию, но предсказуема — нет паузы на компиляцию и нет деопта — и это универсальный запасной вариант, в который деопт всегда возвращается, потому что интерпретатор не делает спекулятивных предположений о типах. Теперь, когда видишь функцию, дольше обычного застрявшую в интерпретируемом режиме, знаешь причину: не хватило обратных рёбер, чтобы исчерпать interrupt budget и запустить JIT.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем 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
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.