Перейти к содержимому

Go Compiler: внутренности, SSA, inlining, BCE, escape analysis

Этот документ — экспертный разбор Go-компилятора (cmd/compile) изнутри. Уровень Middle 3 / Senior+: ожидается, что вы умеете читать SSA-dump, понимаете register-based ABI 1.17+, знаете, как inlining и BCE влияют на hot path в production. Знание pipeline и pragma — обязательная база для собеседов в Авито/Яндекс/Тинькофф на staff+.

  1. Краткое введение (для разогрева)
  2. Глубочайшее погружение
    • 2.1. Pipeline компилятора
    • 2.2. SSA: устройство и зачем
    • 2.3. Inlining: cost-based heuristic
    • 2.4. Bounds Check Elimination (BCE)
    • 2.5. Devirtualization
    • 2.6. Dead code elimination, constant folding
    • 2.7. Escape analysis deep
    • 2.8. Pragma reference
    • 2.9. PGO кратко (детали — файл 04)
    • 2.10. Эволюция 1.17 → 1.26
  3. Подводные камни
  4. Реальные production-кейсы
  5. Вопросы на собесе Middle 3
  6. Practice
  7. Источники

Go-компилятор — это монолитный self-hosted toolchain. После Go 1.5 написан на самом Go, расположен в src/cmd/compile/. На вход — .go файлы пакета, на выход — .o объектный файл, который потом линкуется cmd/link в исполняемый бинарь.

В отличие от LLVM, у Go свой кастомный backend, оптимизированный под:

  • Быструю компиляцию (типичная цель — sub-second для package среднего размера).
  • Простое cross-compiling (без зависимости от GCC/LLVM).
  • Невысокую цену debug-инфо (DWARF полностью свой).

Pipeline укрупнённо:

Source → Scan → Parse → Type Check → Walk (IR) → SSA → Lower → RegAlloc → Encode → Object

Что вы должны уметь делать на Middle 3:

  • Запустить go build -gcflags="-m -m" и прочесть escape-результат.
  • Получить SSA-дамп: GOSSAFUNC=Foo go buildssa.html.
  • Прочесть inlining-решения: -gcflags="-m=2".
  • Понять, почему ваша функция не инлайнится (cost > budget).
  • Включить/отключить BCE и измерить разницу.

Дальше — детали.


Файл-источник всей логики: src/cmd/compile/internal/gc/main.go (старый IR) и src/cmd/compile/internal/noder/ (новая IR, начиная с 1.18). После 1.18 фронтенд использует types2 (тот же type checker, что и go/types), а IR называется Unified IR.

Полная схема:

┌────────────────────────────────────────────────────────────────┐
│ cmd/compile pipeline │
│ │
│ .go files │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Scanner │ cmd/compile/internal/syntax/scanner.go │
│ │ (lexer) │ tokens: IDENT, INT, FLOAT, KEYWORD, ... │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Parser │ cmd/compile/internal/syntax/parser.go │
│ │ │ recursive descent → AST (syntax.Node) │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Type checker │ cmd/compile/internal/types2/ │
│ │ (types2) │ identical to go/types │
│ │ │ + generics instantiation │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Noder (IR gen) │ cmd/compile/internal/noder/ │
│ │ │ AST → typed IR (ir.Node) │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Walk │ cmd/compile/internal/walk/ │
│ │ │ desugaring: range → for-i; map → mapaccess1; │
│ │ │ string concat → concatstrings; defer → ... │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ Inliner │ cmd/compile/internal/inline/ │
│ │ │ cost-based; mid-stack inlining since 1.10 │
│ └──────────┘ │
│ │ │
│ ▼ │
│ ┌────────────┐ │
│ │ Escape An. │ cmd/compile/internal/escape/ │
│ │ │ -m flag; decides stack vs heap │
│ └────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Devirtualize │ cmd/compile/internal/devirtualize/ │
│ │ (since 1.19) │ interface call → direct call │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ SSA build │ cmd/compile/internal/ssa/ │
│ │ │ → values + blocks (CFG) │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ SSA optimization │ ~70 passes: opt, dse, prove, deadcode, │
│ │ (machine-indep.) │ fuse, nilcheckelim, BCE, schedule, ... │
│ └──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Lowering │ generic SSA → arch-specific opcodes │
│ │ (per-arch) │ AMD64 / ARM64 / RISCV64 / ... │
│ └─────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Reg allocation │ linear scan with hints; ABIInternal │
│ └────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Encode (asm) │ emit Plan 9 machine code (.o) │
│ └──────────────┘ │
│ │ │
│ ▼ │
│ .o object file → cmd/link → executable │
└────────────────────────────────────────────────────────────────┘

Ключевые директории:

ЭтапПуть в src/
Scanner/parsercmd/compile/internal/syntax/
Type checkcmd/compile/internal/types2/
IR (Unified)cmd/compile/internal/ir/
Walk (desugar)cmd/compile/internal/walk/
Inliningcmd/compile/internal/inline/
Escape analysiscmd/compile/internal/escape/
Devirtualizationcmd/compile/internal/devirtualize/
SSAcmd/compile/internal/ssa/
PGO loadercmd/compile/internal/pgo/
Per-arch loweringcmd/compile/internal/{amd64,arm64,...}/

Static Single Assignment (SSA) — промежуточная форма, в которой каждая переменная присваивается ровно один раз. Если в исходном коде переменная переписывается, SSA вводит версии (x1, x2, …) и φ-функции (phi) на слияниях ветвей CFG.

Пример:

// Source
if cond {
x = 1
} else {
x = 2
}
y = x + 3
// SSA (схематично)
b1: if cond goto b2 else b3
b2: x1 = 1; goto b4
b3: x2 = 2; goto b4
b4: x3 = phi(x1, x2)
y1 = x3 + 3

Почему SSA?

  • Use-def chain тривиальна: один def на переменную.
  • Все оптимизации (constant propagation, dead code, GVN, BCE, register allocation) выражаются проще.
  • Detection invariants: prove pass работает над фактами SSA-значений (значит, BCE использует prove).

Структура в исходниках Go:

  • ssa.Func — функция в SSA.
  • ssa.Block — узел CFG.
  • ssa.Value — операция (Op + args + type + aux).
  • Op-коды: cmd/compile/internal/ssa/opGen.go (сгенерирован).

Получить дамп SSA для функции:

Окно терминала
GOSSAFUNC=ParseFloat go build ./...
# → ssa.html в текущей директории

Открываете ssa.html — там все passes в виде колонок, можно кликнуть на Value и увидеть как он трансформировался от build → opt → BCE → lower → regalloc.

~70 SSA passes (выдержка из ssa.Compile в compile.go):

number lines, early phielim, early copyelim, early deadcode, short circuit,
decompose user, opt, zero arg cse, opt deadcode, generic cse,
phiopt, gcse, prove, fuse plain, decompose builtin, softfloat, late opt,
dead auto elim, generic deadcode, check bce, branchelim, late phielim,
late copyelim, tighten, phi tighten, late nilcheckelim, late fuse,
insert resched checks, lower, lowered cse, elim unread autos, tighten tuple selectors,
lowered deadcode for cse, lowered deadcode, checkLower, late lower,
addressing modes, late nilcheckelim2, schedule, late schedule, flagalloc,
regalloc, loop rotate, stackframe, trim, ...

Inliner — это cmd/compile/internal/inline/inl.go. Решение «инлайнить или нет» принимается до SSA, на IR.

Бюджет (budget): по умолчанию 80 hairyness units (с 1.20 — inlineMaxBudget = 80, с 1.22 при PGO — может быть до 2000 для hot функций).

Каждая IR-операция имеет свою «стоимость»:

  • Arithmetic, comparison: 1.
  • Function call: 57 (почти весь бюджет).
  • panic: 1.
  • range over map: больше (часто полностью запрещает inlining до 1.18).
  • defer: запрещал inlining до 1.14, теперь разрешён с ограничениями.
  • select, switch с type assertion: высокая стоимость.
  • Closure captures: +1 на каждую переменную.

Mid-stack inlining (Go 1.10+): функция может быть инлайнена, даже если внутри неё есть вызов другой функции (раньше — нет). Это сделало inline-кандидатами «mid-stack» функции (например, bytes.Equal, strings.IndexByte).

Pragma:

PragmaСемантика
//go:noinlineГарантированно НЕ инлайнить.
//go:inlineНет такой pragma. Compiler сам решает.
-gcflags="-l"Отключить inlining полностью.
-gcflags="-l=4"Aggressive inlining (повышенный budget). Debug only.
//go:registerparamsТестовый (ABI debug). Не для продакшна.

Как посмотреть решения:

Окно терминала
go build -gcflags="-m=2" ./...
# можно inline foo
# cannot inline bar: function too complex: cost 142 exceeds budget 80
# inlining call to baz

Penalties (типичные ошибки):

  • Любой defer стоит много — короткая функция с defer часто не инлайнится.
  • interface call без devirtualization — почти всегда блокирует inline.
  • Closure с захватами — растёт стоимость, может пробить budget.
  • range map — много hidden операций.
  • recover() — полностью запрещает inlining.

Совет: если критична производительность hot функции — избегайте defer и interface dispatch внутри. Сделайте interface call ровно на границе пакета.

Это самый «дешёвый бесплатный» perf-win в Go. SSA pass check_bce (см. cmd/compile/internal/ssa/check_bce.go) удаляет проверки i < len(s) для индексирования s[i], если может статически доказать, что условие выполнено.

Как доказывается: через SSA pass prove, который накапливает факты вида v >= 0, v < len(s), и propagates их через CFG.

Включить дебаг:

Окно терминала
go build -gcflags="-d=ssa/check_bce/debug=1" ./...
# foo.go:10:5: Found IsInBounds
# foo.go:11:5: Found IsInBounds
# foo.go:12:5: Found IsInBounds

Если строка присутствует в выводе — bounds check остался. Пустой вывод = все checks убраны.

Паттерны, помогающие BCE:

// (1) Hint в начале функции — фиксирует доказательство len(s) >= N
func sum4(s []int) int {
_ = s[3] // bounds check здесь — все последующие s[0..3] без проверок
return s[0] + s[1] + s[2] + s[3]
}
// (2) Range — обычно BCE работает
for i, v := range s {
_ = s[i] // часто оптимизируется (но не всегда — зависит от прохода)
}
// (3) len(s)-1 после length check
if len(s) > 0 {
_ = s[len(s)-1] // BCE
}
// (4) В цикле — i := 0; i < len(s); i++ — почти всегда BCE
for i := 0; i < len(s); i++ {
s[i] = 0
}

Что мешает BCE:

// (a) Двойное индексирование без hint
func badPair(s []int, i int) (int, int) {
return s[i], s[i+1] // два check'а
}
// Фикс:
func goodPair(s []int, i int) (int, int) {
_ = s[i+1] // один check, оба индекса доказаны
return s[i], s[i+1]
}
// (b) len(s) внутри цикла, который меняется (запись/append):
for i := 0; i < len(s); i++ {
if cond { s = append(s, x) } // len(s) непредсказуем
_ = s[i]
}
// (c) Индекс из interface assertion — prove не может доказать тип.

Реальный production-кейс: в hot-path парсера protobuf убрать всего 3 bounds check’а через _ = data[N] дало ~12% throughput.

С Go 1.19 (полноценно с 1.21) компилятор пытается доказать, что в interface-вызове конкретный тип известен — и заменяет dispatch на прямой вызов:

var w io.Writer = &bytes.Buffer{}
w.Write(data) // raw interface call (slow)

С девиртуализацией:

// Compiler видит, что w — *bytes.Buffer, и заменяет:
w.Write(data) → (*bytes.Buffer).Write(buf, data)
// Теперь это direct call → inline candidate → больше оптимизаций

Когда работает:

  • Локальная переменная с известным concrete type.
  • Аргумент interface, но из callsite известен тип (через PGO).
  • Single implementer (только один тип реализует interface в программе).

С PGO (Go 1.21+): компилятор использует профиль выполнения, видит «99% вызовов через этот interface — это bytesBuffer», и вставляет inline’d проверку:

if reflect.TypeOf(w) == typeBytesBuffer {
(*bytes.Buffer).Write(w, data) // fast path
} else {
w.Write(data) // slow fallback
}

Это называется speculative devirtualization.

  • Constant folding: 2 + 35 на этапе типизации.
  • Dead code elimination (DCE) — SSA pass: блоки, недостижимые из entry, удаляются. Также — недостижимый код после panic/return/goexit.
  • Linker DCE: cmd/link дополнительно вырезает функции, на которые нет ссылок (это убирает unused stdlib функции и режет размер бинаря).

⚠️ Linker DCE отключается, если:

  • Используется reflect (Type.MethodByName и пр.) — компилятор сохраняет всё.
  • go:linkname ссылается на функцию из другого пакета.
  • init() функции — никогда не выкидываются.

Файл-источник: cmd/compile/internal/escape/escape.go. Pre-SSA анализ, который решает, где аллоцировать объект — на стеке (дешево, авто-освобождение) или на heap (GC, дорого).

Базовые правила (Middle 1, повтор):

  • Если на объект берётся &x, и он «убегает» (escape) за пределы функции — heap.
  • Если объект «слишком большой» (более ~64KB) — heap.
  • Если размер объекта не известен на compile-time (slice с runtime length) — heap.

Тонкости Middle 3:

// 1) Возврат указателя — escape всегда
func newPoint() *Point {
return &Point{} // escape to heap
}
// 2) Передача в interface — escape (interface storage)
func log(x any) { ... }
log(42) // 42 boxed → heap (исключение: small allocations <= word могут быть inline-boxed)
// 3) Captures в closure
func make() func() int {
x := 0
return func() int { x++; return x } // x escape — heap
}
// 4) Slice append с unknown growth
s := make([]int, 0, 10)
for i := 0; i < n; i++ { s = append(s, i) } // s escape если n > 10 → heap

Утечка через interface{} — самая частая:

type Buf struct{ b [256]byte }
var sink any
func leak() {
b := Buf{} // на стеке
sink = b // boxing → b escape → ВСЁ выделение в heap
}

Трюки удержать на стеке:

  1. Pool вместо new:

    var pool = sync.Pool{New: func() any { return new(Buf) }}
  2. Заранее объявленные locals:

    var local [1024]byte
    process(local[:n]) // если process не сохраняет ссылку — на стеке
  3. Inline-friendly setter:

    func (b *Buf) Reset() { *b = Buf{} } // (b inlined) → нет нового объекта
  4. fmt.Sprintf всегда escape. Если перф критичен — используйте strconv.AppendInt + []byte.

Множественный escape report:

Окно терминала
go build -gcflags="-m -m" ./pkg
# foo.go:42:7: parameter x leaks to {heap} for fn$1 with derefs=0
# foo.go:50:7: &Buf{} escapes to heap: flow=<...>

-m -m (двойной) даёт цепочку, кто куда «утекает» — обязательное чтение в profiling-сессиях.

Полный список directives (исходник: cmd/compile/internal/ir/syntax.go + cmd/compile/internal/inline/):

PragmaНазначение
//go:noinlineЗапретить inlining.
//go:nosplitНе вставлять stack-grow check (опасно — для рантайма).
//go:noescapeАргументы НЕ escape. Используется с asm-реализациями.
//go:linkname local remoteПеревязать local symbol к remote (даже unexported).
//go:notinheapТип не может быть на heap. Только в рантайме (deprecated в публичных пакетах).
//go:nowritebarrierЗапретить write barrier. Очень опасно — нарушает GC. Только runtime.
//go:nowritebarrierrecЗапрет рекурсивно.
//go:yeswritebarrierrecCancel rec.
//go:systemstackФункция выполняется на системном стеке (g0). Только runtime.
//go:registerparamsТест ABI. Не для продакшна.
//go:uintptrescapesuintptr-args считаются escape (для unsafe wrappers).
//go:wasmimportИмпорт WASM-функции.
//go:build tagBuild constraint (раньше // +build).

⚠️ Все //go: pragma только для одного следующего объявления. Должна быть на отдельной строке без пустых строк между pragma и кодом, иначе игнорируется.

Profile-Guided Optimization (Go 1.21+):

  1. Запускаете прод-сборку, собираете cpu.pprof.
  2. Кладёте в корень main пакета как default.pgo.
  3. go build автоматически применяет PGO (-pgo=auto по умолчанию с 1.21).

Что улучшается:

  • Inline budget увеличен для hot функций (до 2000 единиц).
  • Devirtualization speculative — на основе профиля.
  • Block layout — горячие блоки рядом (cache locality).
  • Register allocation hints.

Бенчмарки: 2–7% throughput в среднем, до 14% в специфических случаях.

ВерсияЧто нового в компиляторе
1.17Register-based calling convention (ABIInternal) для AMD64. Аргументы — в регистры.
1.18Generics (типовые параметры). Unified IR. types2 как фронтенд.
1.19Soft memory limit (GOMEMLIMIT). Доработки devirtualization.
1.20PGO preview. Comparable types для interfaces.
1.21PGO GA. Speculative devirtualization. clear, min, max builtins.
1.22range over int. PGO стабильность профилей. range-over-func preview.
1.23range-over-func GA. //go:linkname strict mode (запрещает для non-stdlib без opt-in).
1.24Generic type aliases. Map performance overhaul (Swiss tables в map, faster GC marking).
1.25Container-aware GOMAXPROCS (читает cgroup). Greenteagc preview (experimental GC).
1.26(планируется) Дальнейшие улучшения PGO, BCE в loops, оптимизации range-over-func.

  1. Pragma после blank line — игнорируется.

    //go:noinline
    func foo() {} // pragma НЕ применилась
  2. //go:linkname теперь strict (Go 1.23+). В user-коде нельзя linkname-нуть в runtime без -ldflags=-checklinkname=0. Часть либ (gopsutil, pebble) сломались.

  3. go:noescape без asm-реализации — UB. Pragma говорит «не escape», компилятор верит. Если внутри Go-реализация всё-таки escape — память может перезаписаться (use-after-free).

  4. Mid-stack inlining делает stack traces длиннее, но без видимых frames. В pprof увидите только outer-функцию. Используйте -gcflags=-l для debug-сборки.

  5. -trimpath ломает symbolication, если нет debug-инфо. В прод-сборках желательно: -ldflags="-s -w" для размера, но -trimpath без -s -w сохраняет всё.

  6. PGO профиль из старого билда — OK (с 1.22). Но не из другой архитектуры.

  7. BCE требует, чтобы len(s) был invariant в loop. Если в цикле есть append(s, ...) — BCE сломан.

  8. Closure capture by value vs by pointer.

    for i := 0; i < n; i++ {
    go func() { use(i) }() // pre-1.22: все горутины видят финальный i
    }

    В 1.22 семантика per-iteration исправлена (loopvar experiment стал стандартом).

  9. defer в hot path — много penalty. Compiler выбирает open-coded defer (для ≤8 defer на функцию), но всё равно cost ≈ 35 единиц.

  10. recover() в функции запрещает inlining её самой. Если хотите recover — используйте wrapper:

    func safe(f func()) { defer func() { recover() }(); f() }
  11. interface{} boxing — escape даже для int.

    var x any = 42 // boxes int into interface → heap (для int >= ?? имеет небольшую оптимизацию)

    На 1.21+ есть оптимизация для маленьких int — staticuint64s.

  12. Generics не сильно медленнее, но stencil/dictionary подход Generic функция инстанцируется per-shape (ptr / non-ptr / int / float — отдельно). Это даёт «среднее» между mono- и polymorph-кодом. Может приводить к code bloat.

  13. go:nosplit функция должна быть мала (≤ 512 байт по умолчанию). Иначе compile error: nosplit stack overflow.

  14. go:linkname к unexported runtime функции — может сломаться в любой минорной версии. Реальные кейсы: 1.20 поменял имя runtime.fastrandruntime.cheaprand → много libs сломалось.

  15. -gcflags="all=..." vs -gcflags="...". Без all= — только текущий пакет. С all= — все пакеты, включая stdlib (медленно).


  • Hot path: парсинг строк JSON, 10M req/min.
  • Профилировка: 18% CPU на bounds checks внутри tokenizer.
  • Фикс: 3 hint вида _ = data[i+4] перед циклом разбора 4-байтных токенов.
  • Результат: +11% throughput, без изменения логики.
  • Interface call Scorer.Score(doc) на каждом документе (тысячи на запрос).
  • Profile показал — 92% времени один concrete тип *BM25Scorer.
  • Включили PGO с собранным production-профилем.
  • Devirtualization + inline + BCE → −18% CPU на ранкинг.
  • defer Unlock() в hot mutex method.
  • Профиль: 8% CPU только в runtime.deferreturn.
  • Заменили на manual unlock + recover-wrapper для panic-safety.
  • Результат: +6% RPS, увеличение complexity — допустимое (review-only функция).
  • Кастомный crypto-код в asm.
  • При обновлении на 1.20 — asm-функция стала медленнее на 4%.
  • Причина: ABIInternal v2 (1.17+) изменил calling convention; старый asm имел лишний MOVQ перед RET.
  • Фикс: переписать prologue + epilogue под новый ABI; +5% обратно.
  • 30+ микросервисов на Go 1.21.
  • Собрали production-профили через continuous profiling (Pyroscope), агрегировали по сервисам.
  • Включили -pgo=auto в CI.
  • Среднее улучшение: 3.2% latency, 2.8% CPU. Cloud cost saving: ~$2M/год.
  • Latency-критичная функция ValidateTx(tx Tx) error.
  • В hot path был fmt.Errorf("bad tx: %v", id) для логирования ошибок.
  • fmt.Errorf форсирует heap-аллокацию (interface boxing + Sprintf).
  • Замена: pre-allocated error structs + atomic ID encoding.
  • −40% heap allocations, GC pauses снизились на 25%.

5. Вопросы на собесе Middle 3 (экспертный уровень)

Заголовок раздела «5. Вопросы на собесе Middle 3 (экспертный уровень)»
  1. Опишите pipeline Go-компилятора от .go до .o. Какие IR используются на разных этапах?

  2. Что такое SSA? Зачем компилятору SSA-форма? Что такое φ-функция?

  3. Где находится GOSSAFUNC дамп и как его читать? Покажите, как найти, где исчезает bounds check.

  4. Какие SSA-passes отвечают за оптимизации в Go? Назовите 5-7 ключевых.

  5. Как работает inliner? Что такое cost-based heuristic? Дефолтный budget?

  6. Какие конструкции в Go увеличивают inline-cost существенно?

  7. Расскажите про mid-stack inlining (1.10+). Что это даёт?

  8. Pragma //go:noinline — когда полезна? А //go:inline существует?

  9. Как заставить компилятор инлайнить функцию, для которой cost > budget? (-gcflags=-l=4, упрощение, разбиение).

  10. Что такое Bounds Check Elimination? Какой SSA pass отвечает?

  11. Покажите паттерн _ = s[len(s)-1] — зачем он? Когда не работает?

  12. Чем отличается escape analysis от GC? Когда escape != heap allocation?

  13. Какие 5 типичных случаев вызывают escape объекта на heap?

  14. Что выведет -gcflags="-m -m" и зачем -m -m (двойной)?

  15. Что такое devirtualization в Go? С какой версии?

  16. Speculative devirtualization при PGO — как работает на низком уровне?

  17. ABI Internal (Go 1.17+) — что это? Какие регистры для аргументов на AMD64?

  18. Как Go ABI отличается от System V AMD64 ABI? Можно ли вызвать Go-функцию из C напрямую?

  19. Что такое linker DCE и когда отключается?

  20. Pragma //go:linkname — для чего? Что изменилось в 1.23 (strict mode)?

  21. //go:noescape — когда корректно использовать? Что произойдёт при misuse?

  22. Объясните, почему fmt.Sprintf всегда escape.

  23. Closure capture: какие переменные escape, какие могут остаться на стеке?

  24. Generics: чем «stencil/dictionary» подход отличается от monomorphization?

  25. Что покажет go tool objdump -s "myFunc" binary?

  26. Какие отличия compile flags -l, -N, -l=4? Когда используются?

  27. PGO в Go 1.21+: как профили влияют на inlining/devirtualization?

  28. Почему defer имеет высокий cost для inliner?

  29. Что такое open-coded defer (1.14+) и когда он применяется?

  30. Compile-time vs runtime panic — где происходит type assertion check?

  31. Что такое FUNCDATA/PCDATA pseudo-instructions в Plan 9 ассемблере? (см. файл 03)

  32. Почему recover() в функции полностью запрещает её inlining?

  33. Range-over-func (1.23+) — как реализован на уровне компилятора?

  34. Map в 1.24 переведён на Swiss tables — что это и какой выигрыш?

  35. Container-aware GOMAXPROCS в 1.25 — как теперь читается cgroup?


  1. SSA-дамп. Возьмите простую функцию Sum(s []int) int с циклом. Сгенерируйте ssa.html через GOSSAFUNC. Найдите, на каком pass исчезает bounds check. Запишите имя pass.

  2. Inline diagnostics. Напишите функцию, которая должна инлайниться, но не инлайнится из-за defer. Запустите -gcflags=-m=2, найдите cost-сообщение. Перепишите так, чтобы стала inline.

  3. Escape report. Дано:

    func Build(name string) *User {
    u := User{Name: name}
    return &u
    }

    Предскажите вывод -gcflags=-m. Проверьте. Объясните.

  4. BCE benchmark. Напишите два варианта функции Sum4(s []int) int:

    • Без hint
    • С _ = s[3] в начале

    Сделайте benchmark, сравните ns/op. Объясните разницу через -d=ssa/check_bce/debug=1.

  5. Devirtualization. Напишите interface Doer{Do()} и единственный implementer. Соберите без PGO и с PGO (нужен профиль). Сравните go tool objdump — найдите место, где interface call заменён прямым вызовом.

  6. Pragma puzzle. Объясните вывод:

    //go:noinline
    func a() int { return 1 }
    func b() int { return a() + 2 }

    Что вы увидите в objdump для b? Будет ли инлайн a в b?

  7. Linkname. Прочитайте runtime/time.go и найдите функцию, к которой через go:linkname обращается пакет time. Опишите цепочку.

  8. PGO experiment. Возьмите HTTP-handler с парсингом JSON. Соберите cpu.pprof под wrk/vegeta. Положите как default.pgo. Пересоберите. Запустите тот же бенчмарк. Зафиксируйте Δ throughput.


  1. Go compiler READMEsrc/cmd/compile/README.md (карта пакетов).
  2. SSA package docssrc/cmd/compile/internal/ssa/README.md.
  3. «Introduction to the Go Compiler» — Vladimir Vivien, GopherCon 2018.
  4. «Inside the Go Compiler» — Cherry Mui, GopherCon 2022.
  5. «Profile-Guided Optimization in Go 1.21» — Michael Pratt, Go Blog (2023).
  6. «Generics in Go: Implementation Overview» — Keith Randall, GopherCon 2022.
  7. «Register-based Go calling convention» — Cherry Zhang, design doc (design/40724-register-calling.md).
  8. «Escape Analysis in Go» — Dmitry Vyukov, golang internals talks.
  9. «BCE in Go» — Russ Cox, разрозненные посты в golang-dev mailing list.
  10. «Devirtualization in Go 1.21» — Aaron Patterson, Go Blog.
  11. «Go 1.22 PGO Stability» — Michael Pratt, Go Blog (2024).
  12. «Swiss Tables in Go 1.24» — Austin Clements, GopherCon 2024.
  13. «The State of Go Compiler» — Cherry Mui, Cherry Mui’s blog (cherrymui.dev).
  14. «Go’s pragma reference» — Dave Cheney, dave.cheney.net.
  15. Go source codesrc/cmd/compile/ (читайте напрямую, особенно inline/, escape/, ssa/check_bce.go).