InquirerMimaropa drug bust in Palawan yields ₱51,884 worth of ‘shabu’PunchMadrid apologise to Chinese people over Mbappé’s commentsThe Jerusalem PostIDF kills three terrorists in Gaza, including Hamas Nukhba who kidnapped Alexander Dancyg on Oct. 7Daily MaverickWHAT’S COOKING: Asparagus spears with rosemary-infused Parmesan butter creamESPNThe joy and pain that define baseball were in full force in Rays' incredible Game 4 winColliderMichael Bay’s ‘Black Hawk Down’ Replacement Officially Has a New Streaming BaseCapital FMRights groups sue State over alleged political violence ahead of 2027 pollsThe RegisterNeocloud thinks AI agents - not meatbags - will be its main customersZDF heuteAktuelle Pressemitteilungen des ZDFGhaflaLynn Ngugi Defends Mercy Masika’s Biblical Wife Masterclass Amid BacklashNHK 社会熊本地震3か月 イオン 一部エリア営業再開の意向 遺族に示すالنهارأستراليا تُشدّد الرقابة على مستحضرات الواقي من الشمس
The Daily Newsstand · Free, Always
Wednesday, October 28, 2026

Go в продакшене 2026: как язык вырос из микросервисов

Translate

Привет, Хабр! На связи Илья Данилкин, тимлид в Авито Авто. В этом материале я собрал подборку важных новостей из мира Go. Разберём, откуда берутся типичные утечки горутин и чем встроенный в рантайм детектор принципиально отличается от uber-go/goleak. Посмотрим, как устроен механизм GOEXPERIMENT и какие эксперименты стоит попробовать. Измерим, сколько реально даёт флаг GOAMD64 на современном железе. А также расскажу, почему языковые модели перестали быть просто предметом обсуждения и стали участниками самого процесса разработки — от поиска уязвимостей до профилирования сборщика мусора.

Содержание

Утечки горутин: как сборщика мусора научили ловить то, что он сам не мог собрать

Redowan Delowar сообщает, что пропоузал о добавлении в runtime/pprof нового профиля — goroutineleak — принят. В Go 1.26 профиль уже доступен как эксперимент (GOEXPERIMENT=goroutineleakprofile), а дальше войдёт в стандартную библиотеку без флага; официальный разбор есть и в блоге Go. Это не сторонняя библиотека и не линтер, а встроенный в рантайм механизм, который использует сборщик мусора, чтобы доказательно находить горутины, заблокированные навсегда.

Почему это вообще проблема, а не теоретическая придирка? Горутина не собирается сборщиком мусора, пока жив хотя бы один указатель на структуры, которые её удерживают — стек, канал, на котором она заблокирована, мьютекс, который она ждёт. GC прекрасно чистит память, но у него нет представления о «зависшем навсегда потоке исполнения» как о категории мусора. Формально горутина, заблокированная на канале, жива: у неё есть стек, у неё есть состояние, теоретически её может кто-то разбудить. Проблема в том, что для конкретной утечки этого «кого-то» уже никогда не будет.

Горутина утекла, если она заблокирована на канале или локе, до которых не может дотянуться ни одна другая работающая (runnable) горутина — ни напрямую, ни транзитивно через другие горутины.

Такая горутина будет висеть до конца жизненного цикла процесса, потихоньку съедая память под стек и создавая шум в рантайме.

Анатомия трёх типовых утечек

Прежде чем говорить про детектор, стоит закрепить, какие паттерны он призван ловить. Все три — не экзотика, а стандартные грабли, на которые наступает практически любой Go-разработчик, работающий с конкурентностью.

1. Early return и «осиротевший» отправитель

Утечка происходит по классической схеме: набор задач шлёт результаты в небуферизованный канал через wg.Go, а вызывающий код читает только до первой ошибки и возвращает управление раньше времени.

func run(tasks []func() error) error {
    errs := make(chan error) // unbuffered
    var wg sync.WaitGroup
    for _, task := range tasks {
        wg.Go(func() { errs <- task() }) // (1)
    }
    for range tasks {
        if err := <-errs; err != nil {
            return err // (2)
        }
    }
    wg.Wait()
    return nil
}

Суть проблемы в том, что небуферизованный канал синхронный, и отправка блокируется, пока кто-то не прочитает. Если код-получатель ушёл по early return после первой же ошибки, все остальные горутины, которые ещё не успели отправить результат, зависают на send навсегда. Никто их больше не читает.

Существующий инструментарий — uber-go/goleak — ловит такие случаи в тестах, сравнивая снимки списка горутин до и после. Решение здесь не в изменении логики раннего выхода (она часто и есть корректное поведение), а в развязке отправителей и получателя:

– дать каналу буфер, равный максимально возможному числу отправителей;

– дочитать (drain) все оставшиеся результаты перед возвратом, даже если они уже не нужны.

2. Гонка реплик за первым ответом

Аналогичная утечка горутин возникает, когда вы отправляете запрос нескольким репликам и сохраняете только первый ответ. Запрос уходит в несколько реплик одновременно, каждая из них при получении результата пытается первой отправить его в общий небуферизованный канал. Как только приходит первый ответ, вызывающий код возвращает управление. Все более медленные реплики блокируются на отправке навсегда — читать их уже некому.

func replicate(replicas []func() string) string {
    results := make(chan string) // unbuffered
    for _, r := range replicas {
        go func() { results <- r() }() // (1)
    }
    return <-results // (2)
}

Решение подобно предыдущему: буфер канала должен быть рассчитан на все реплики, а не на одну. Правка неказистая и очевидная, но именно её обычно забывают: в момент написания кода мысль занята логикой выбора первого ответа, а не судьбой опоздавших горутин.

3. Забытый close

Третий паттерн — это производитель потоковых данных, который отдаёт значения через range по каналу, но забывает закрыть канал по завершении. range по каналу — это блокирующий цикл: он продолжает тянуть значения, пока канал не будет явно закрыт. Если функция-producer возвращается без close(out), читающая горутина остаётся в вечном ожидании следующего значения, которое никогда не придёт.

func stream(work []int) {
    out := make(chan int)
    go func() {
        for v := range out { // (1)
            handle(v)
        }
    }()
    for _, v := range work {
        out <- v
    }
    // (2) no close(out)
}

Фикс также тривиален по формулировке — делать close(out) после последней отправки, но он легко теряется, когда функция обрастает несколькими путями выхода и хотя бы один из них не проходит через закрытие канала.

Утечки очевидны, как только вы их заметите, но их легко упустить из виду при раннем возврате или по мере роста окружающего кода. goleak обнаруживает их в тестах. В продакшене у вас есть обычный профиль /debug/pprof/goroutine. Он показывает, на чём заблокирована каждая горутина, но не говорит, разблокируется ли она когда-нибудь, — поэтому приходится гадать, какие из них зависли навсегда, а какие просто ждут работы.

Этот список далеко не исчерпывающий, и не каждая утечка находится в вашем собственном коде — её может принести и зависимость. Uber каталогизировал эти шаблоны в своём Go-монорепозитории.

Профиль утечек: теперь их умеет находить сама stdlib

Профиль, как и goleak, пришёл из Uber — его спроектировали Vlad Saioc и Milind Chabbi. Обнаружение построено на работе GC: горутина считается утёкшей, если она заблокирована на канале или локе, до которых не может добраться ни одна работающая горутина — ни напрямую, ни через другую горутину, которую работающая могла бы разблокировать. Ничто не может её разбудить, и сборщик мусора помечает её.

goleak и профиль goroutineleak отвечают на разные вопросы:

goleak

goroutineleak profile

на какой вопрос отвечает

что ещё работает, хотя не должно

что больше никогда не заработает

как решает

снимком списка горутин, без доказательства

доказательством достижимости с помощью сборщика мусора

где работает

в тестах, при тирдауне

в живом процессе

ложные срабатывания

есть — на живом сервере

нет: только доказательно застрявшие горутины

Разница — в том, где каждый из них работает. При завершении теста ничего не должно оставаться запущенным, и вернуть всё, что осталось, — ровно то, что нужно от goleak. На рабочем сервере ситуация обратная: большинство его горутин намеренно заблокированы в ожидании следующего запроса, и goleak не может отличить их от реальной утечки.

Профиль же доказывает утечку. Он начинает с горутин, которые всё ещё могут работать, отслеживает то, до чего они могут добраться, и «спасает» любую заблокированную горутину, чей канал или лок всё ещё в игре. Всё, что осталось, недостижимо ни для кого — оно зависло навсегда. Uber уже пытался ловить утечки в продакшене сэмплирующим инструментом, но сэмплирование судит по косвенным признакам и даёт ложные срабатывания. GC сообщает только о тех горутинах, которые доказательно зависли, — это и есть гарантия отсутствия ложных срабатываний.

В отличие от goleak, у профиля нет готовых хелперов VerifyNone(t) и VerifyTestMain(m) — в разделе про тесты показано, как собрать свои.

API очень маленький. Нет нового типа или функции, только профиль с именем goroutineleak. Он регистрируется автоматически, и стандартные инструменты pprof читают его как любой другой профиль.

Получить доступ к профилю можно четырьмя обычными способами.

1. Из вашего кода

Вы находите профиль и записываете его. Начните с debug=0, который пишет сжатый protobuf в файл:

func main() {
    f, _ := os.Create("leak.pb.gz")
    pprof.Lookup("goroutineleak").WriteTo(f, 0)
}

pprof.Lookup возвращает профиль, а WriteTo запускает цикл сборки мусора для обнаружения утечек перед записью. Откройте файл с помощью go tool pprof — так же, как CPU- или heap-профиль.

Второй аргумент WriteTo — уровень debug. 1 и 2 дают текст, который можно писать напрямую в os.Stdout. При debug=1 обработчик сигнала позволяет выводить утечки по запросу через kill -USR1 <pid>:

// kill -USR1 <pid> to dump leaks
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGUSR1)
go func() {
    for range sig {
        pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
    }
}()

Текст прямо указывает на утекшие горутины:

goroutineleak profile: total 2
1 @ ...
#    0x...    main.leakSend.func1+0x27    formats/main.go:15
1 @ ...
#    0x...    main.leakRange.func1+0x33    formats/main.go:21

debug=2 — это полный дамп горутин, где утекшие помечены суффиксом (leaked):

goroutine 7 [chan send (leaked)]:
main.leakSend.func1()
    formats/main.go:15 +0x28
created by main.leakSend in goroutine 1
    formats/main.go:15 +0x6c
goroutine 8 [chan receive (leaked)]:
main.leakRange.func1()
    formats/main.go:21 +0x34
created by main.leakRange in goroutine 1
    formats/main.go:20 +0x6c

Обычный дамп показывает просто [chan send] и [chan receive]; суффикс (leaked) добавляет именно профиль.

2. В тестах

Один хелпер запускает обнаружение и возвращает то, что зависло:

func leaked() (string, bool) {
    p := pprof.Lookup("goroutineleak")
    if p == nil {
        return "", false // experiment off, nothing to detect
    }
    var b bytes.Buffer
    p.WriteTo(&b, 1)
    return b.String(), p.Count() > 0
}

Оберните его в verifyNone и вызывайте через defer:

// verifyNone mirrors goleak.VerifyNone.
func verifyNone(t testing.T) {
    t.Helper()
    if report, ok := leaked(); ok {
        t.Fatalf("leaked goroutines:\n%s", report)
    }
}
func TestRun(t testing.T) {
    defer verifyNone(t)
    // ... exercise the code under test ...
}

Для всего набора тестов напишите verifyTestMain и вызывайте его из TestMain:

// verifyTestMain mirrors goleak.VerifyTestMain.
func verifyTestMain(m testing.M) {
    code := m.Run()
    if code == 0 {
        if report, ok := leaked(); ok {
            fmt.Fprintf(os.Stderr, "leaked goroutines:\n%s", report)
            code = 1
        }
    }
    os.Exit(code)
}
func TestMain(m testing.M) {
    verifyTestMain(m)
}

3. Через HTTP

Импорт net/http/pprof регистрирует профиль в мультиплексоре по умолчанию. Обслуживайте этот мультиплексор с nil-обработчиком, и эндпоинт будет доступен без дополнительного кода:

// registers /debug/pprof/goroutineleak on http.DefaultServeMux
import _ "net/http/pprof"
http.ListenAndServe("localhost:6060", nil)

Затем считайте профиль из эндпоинта:

$ curl 'localhost:6060/debug/pprof/goroutineleak?debug=1'
goroutineleak profile: total 1
1 @ ...
#    0x...    main.main.func1+0x27    server/main.go:14

4. С помощью go tool pprof

go tool pprof читает его как любой другой профиль — можно указать на эндпоинт или на сохранённый дамп с debug=0:

$ go tool pprof -top 'http://localhost:6060/debug/pprof/goroutineleak'
Type: goroutineleak
     flat  flat%   sum%        cum   cum%
        1   100%   100%          1   100%  runtime.gopark
        0     0%   100%          1   100%  main.main.func1
        0     0%   100%          1   100%  runtime.chansend
        0     0%   100%          1   100%  runtime.chansend1

GC найдёт не каждую утечку — это цена гарантии отсутствия ложных срабатываний. Чтобы не пометить горутину, которая ещё может проснуться, сборщик мусора оставляет в покое все горутины, чьи каналы или локи всё ещё достижимы.

Тут ещё больше контента

Тут ещё больше контента

GOEXPERIMENT: как обкатывают рантайм

В релизы Go часто попадают экспериментальные функции. Они могут принимать различные формы: иногда это совершенно новые пакеты в стандартной библиотеке, иногда — изменения в компиляторе или среде выполнения, а очень редко — критические изменения в поведении самого Go. Материал подготовлен на основе поста Alex Edwards.

В большинстве случаев цель экспериментов — получить реальную обратную связь от пользователей, прежде чем фича выйдет официально. Если она вызывает регрессии или получает негативные отзывы, её изменят или вообще отменят.

Экспериментальные функции поставляются выключенными по умолчанию. Вы сами выбираете, хотите ли протестировать функцию, — обычно через переменную окружения GOEXPERIMENT.

Существуют три основных состояния экспериментов:

1. Выключен по умолчанию и находится на стадии оценки

2. Выключен по умолчанию и приостановлен/неактивен

3. Включён по умолчанию с временной возможностью отключения

Постоянные эксперименты

В Go есть несколько экспериментальных функций, которые на самом деле не являются «экспериментами» в обычном смысле слова. Они отключены по умолчанию, но не ждут оценки. Хотя они управляются переменной GOEXPERIMENT так же, как и другие эксперименты, по сути это дополнительные возможности Go, которые могут понадобиться в специализированных ситуациях. Будем называть их постоянными экспериментами.

Например, есть функция отслеживания полей, которая фиксирует, к каким полям структуры осуществляется доступ. Она доступна уже десять лет, и нет никаких планов когда-либо сделать её общедоступной. Или есть статическое ранжирование блокировок — диагностическая функция для обнаружения потенциальных взаимоблокировок в рантайме Go.

Какие эксперименты доступны прямо сейчас

Список всех доступных экспериментов можно получить командой $ go doc goexperiment.Flags.

Какие эксперименты включены по умолчанию, можно узнать из исходного кода src/internal/buildcfg/exp.go — по объявлению базовой переменной в функции ParseGOEXPERIMENT().

Начиная с Go 1.26, доступны следующие постоянные эксперименты:

название эксперимента

описание

статус

FieldTrack

Отслеживает, к каким полям структуры осуществляется доступ

По умолчанию выключен, останется навсегда

StaticLockRanking

Проверка порядка получения блокировок для выявления взаимоблокировок

По умолчанию выключен, останется навсегда

CgoCheck2

Проверка правил передачи указателей cgo; слишком затратно запускать по умолчанию

По умолчанию выключен, останется навсегда

BoringCrypto

Заменяет стандартную криптографию Go на BoringSSL, прошедший проверку FIPS; неактуален с Go 1.24

По умолчанию выключен, скоро будет удалён

PreemptibleLoops

Позволяет планировщику прерывать горутины на обратных рёбрах цикла; неактуален с Go 1.14, но всё ещё может быть полезен на платформах, где другие способы вытеснения не поддерживаются

По умолчанию выключен, останется навсегда

Текущие эксперименты, отключённые по умолчанию, и их статусы:

название эксперимента

описание

статус

HeapMinimum512KiB

Уменьшает минимальный размер кучи с 4 МБ до 512 КБ; полезно в условиях ограниченных ресурсов

Отключён по умолчанию, вероятно, неактуален

Arenas

Реализация арен памяти

Отключён по умолчанию, приостановлен из-за негативных отзывов

NewInliner

Переписанный инлайнер компилятора с улучшенной эвристикой для мест вызова функций

Отключён по умолчанию, на стадии оценки, доступен с Go 1.22

JSONv2

Новый пакет encoding/json/v2 с улучшенным кодированием/декодированием JSON

Отключён по умолчанию, на стадии оценки, доступен с Go 1.25

RuntimeSecret

Новый пакет runtime/secret с функциями для обнуления памяти; доступен только на Linux amd64/arm64

Отключён по умолчанию, на стадии оценки, доступен с Go 1.26

GoroutineLeakProfile

Добавляет тип pprof-профиля goroutineleak

Отключён по умолчанию, на стадии оценки, доступен с Go 1.26

SIMD

Новый пакет simd/archsimd, даёт доступ к архитектурно-специфичным SIMD-операциям; доступен только на amd64

Отключён по умолчанию, на стадии оценки, доступен с Go 1.26

RuntimeFreegc

Позволяет немедленно повторно использовать память, не дожидаясь цикла сборки мусора, когда это безопасно

Отключён по умолчанию, на стадии оценки, доступен с Go 1.26

SizeSpecializedMalloc

Включает реализации malloc, специализированные под каждый класс размера

Отключён по умолчанию, на стадии оценки, доступен с Go 1.26

А вот эксперименты, которые в данный момент включены по умолчанию:

название эксперимента

описание

статус

LoopVar

Область видимости переменной цикла для каждой итерации

Включён по умолчанию с Go 1.22, возможность отключения сохранена для редких случаев

Dwarf5

Генерация отладочной информации в формате DWARF 5; уменьшает размер бинарного файла

Включён по умолчанию, с временной возможностью отключения, которая может быть убрана в будущих релизах

RandomizedHeapBase64

Рандомизирует базовый адрес кучи при запуске в качестве меры безопасности

Включён по умолчанию, с временной возможностью отключения, которую ожидаемо уберут в будущих релизах

GreenTeaGC

Новый сборщик мусора с улучшенной производительностью; недоступен на darwin/ios/aix

Включён по умолчанию, с временной возможностью отключения, которую ожидаемо уберут в Go 1.27

RegabiWrappers

ABI-обёртки для вызовов между функциями ABI0 и ABIInternal; только на 64-битных архитектурах

Включён по умолчанию; отключение работает только на s390x и будет убрано в Go 1.27

RegabiArgs

Включает передачу аргументов/результатов через регистры во всех скомпилированных функциях Go; только на 64-битных архитектурах

Включён по умолчанию; отключение работает только на s390x и будет убрано в Go 1.27

Как включить и отключить эксперименты

Эксперименты управляются переменной окружения GOEXPERIMENT. Если хотите попробовать эксперименты, отключённые по умолчанию, укажите их названия в GOEXPERIMENT через запятую, строчными буквами. Например, собрать приложение с включёнными JSONv2 и GoroutineLeakProfile можно так:

$ GOEXPERIMENT=jsonv2,goroutineleakprofile go build ./...

Если есть эксперимент, включённый по умолчанию, который вы хотите отключить, добавьте no перед его названием (тоже строчными). Например, собрать приложение с отключёнными GreenTeaGC и RandomizedHeapBase64:

$ GOEXPERIMENT=nogreenteagc,norandomizedheapbase64 go build ./...

Можно смешивать включённые и выключенные эксперименты:

$ GOEXPERIMENT=jsonv2,nogreenteagc go build ./...

Обратите внимание: если собирать один и тот же пакет с разными значениями GOEXPERIMENT, Go считает это разными сборками и хранит для них отдельные записи в кэше сборок.

В примерах выше использовалась команда go build, но тот же шаблон работает с go run и go test. Например, напишем программу, использующую экспериментальный пакет encoding/json/v2:

package main

import (
    "encoding/json/v2"
    "fmt"
)

type Person struct {
    Name string json:"name"
    Age  int    json:"age"
    City string json:"city"
}

func main() {
    p := Person{Name: "Ada", Age: 36, City: "Vienna"}
    data, _ := json.Marshal(p, json.StringifyNumbers(true))
    fmt.Println(string(data))
}

Если запустить программу обычным способом, она не скомпилируется, и вы получите сообщение об ошибке:

$ go run main.go
package command-line-arguments
        imports encoding/json/v2: build constraints exclude all Go files in /usr/local/go/src/encoding/json/v2

Но если включить эксперимент JSONv2, программа будет работать как положено:

$ GOEXPERIMENT=jsonv2 go run main.go
{"name":"Ada","age":"36","city":"Vienna"}

Какие эксперименты действительно стоит попробовать

GreenTeaGC: если используете Go 1.26, он уже включён по умолчанию. Но если заметите проблемы с производительностью или поведением, помните, что его всё ещё можно отключить.

Dwarf5: если используете Go 1.25 или новее, он уже включён по умолчанию. Его тоже пока можно отключить.

JSONv2: если пишете много кода, работающего с JSON, стоит поэкспериментировать с новым пакетом encoding/json/v2, ознакомиться с его возможностями и сообщить о проблемах.

GoroutineLeakProfile: это полезный параметр, который стоит включить, если подозреваете утечку горутин и нужно её отладить.

RuntimeSecret: стоит поэкспериментировать и сообщить о результатах, если пишете криптографический код или обрабатываете конфиденциальные данные.

RuntimeFreegc: если приложение сильно нагружает сборщик мусора, стоит прогнать бенчмарки с включённым параметром и посмотреть, улучшится ли производительность.

GOAMD64: сколько даёт современное железо

Daniel Lemire, профессор информатики в Университете Квебека (TELUQ) и известный специалист по производительности и SIMD, написал пост о том, сколько на практике дают микроархитектурные уровни amd64 в Go. При компиляции программы на Go для 64-битного процессора Intel или AMD компилятор по умолчанию ориентируется на набор инструкций, которому больше 20 лет. Полученный бинарный файл работает практически на любом чипе x64, но при этом не использует ни одной инструкции, добавленной после 2003 года.

Каждый уровень микроархитектуры включает в себя набор расширений, наличие которых можно считать гарантированным:

Уровни микроархитектуры AMD64

Уровень

Добавляет (приблизительно)

1

базовый набор AMD64 (SSE2)

2

popcnt, SSE4.2

3

AVX2

4

AVX-512 (F/BW/DQ/VL)

По мнению автора, эта лестница уже несколько устарела: её заморозили примерно в 2020 году, а железо с тех пор ушло вперёд. Пришлось бы добавить новейшие расширения AVX-512 (VBMI, VBMI2, VNNI, BF16, FP16, VPOPCNTDQ), которые поддерживаются современными серверными и потребительскими чипами, но не требуются для v4. Хотя уровни 1–4 — полезный общий язык, реалистичная цель «использовать все возможности этого процессора» сегодня потребовала бы как минимум v5, а возможно, всю схему стоило бы заменить более точным определением возможностей CPU.

В любом случае инструментарий Go даёт доступ к уровням 1–4 через переменную окружения GOAMD64. GOAMD64=v3 сообщает компилятору, что он может использовать всё вплоть до AVX2 включительно. По умолчанию используется v1 — наименьший общий знаменатель.

Но тут встаёт вопрос: если взять реальную, критичную к производительности библиотеку и перекомпилировать её на каждом уровне, сколько действительно можно выиграть? Берём Roaring Bitmaps — сжатую битовую структуру данных, используемую в базах данных и поисковых системах. Она хранит набор 32-битных целых чисел, разбивая 32-битное пространство на блоки по 65 536 значений (ключом служат старшие 16 бит), а каждый блок хранит в контейнере, содержащем только младшие 16 бит. Контейнер бывает трёх типов, и библиотека всегда выбирает тот, который меньше:

– array-контейнер: отсортированный список 16-битных значений, используется, когда блок разреженный (не более нескольких тысяч элементов);

– bitmap-контейнер: плоский битовый вектор размером 8 КБ (65 536 бит, по одному на каждое возможное значение), используется, когда блок плотный;

– run-контейнер: список интервалов [начало, длина], используется, когда установленные биты идут подряд.

Автор взял последнюю версию библиотеки и прогнал её собственный набор бенчмарков четыре раза — по одному разу для каждого уровня, по восемь замеров в каждом случае. Всё это на одном процессоре Intel Xeon Gold 6548N (Emerald Rapids, поддерживает все четыре уровня, включая AVX-512) под Go 1.26.2 с Roaring v2.18.2.

Popcount (population count, вес Хэмминга) — это просто число битов, установленных в единицу, в машинном слове. Roaring постоянно на него опирается: мощность bitmap-контейнера, то есть количество значений в нём, — это сумма popcount его 1024 64-битных слов. Современные процессоры x86 имеют специальную инструкцию popcnt, которая делает это за одну операцию, но она доступна только начиная с уровня v2 (SSE4.2, 2008). Без неё компилятору приходится генерировать многоинструкционную последовательность побитовых операций.

Самый очевидный результат — подсчёт установленных битов в bitmap-контейнере. В базовой версии v1 инструкция popcnt недоступна, поэтому Go генерирует программный резервный вариант. Как только переходим к v2, popcnt становится доступна, и время выполнения сокращается почти вдвое:

Время выполнения popcount в bitmap-контейнере

Время выполнения popcount в bitmap-контейнере

Это снижение на 43%, и оно бесплатное: никаких изменений в исходном коде, только флаг компилятора. v3 и v4 ничего нового не дают — одной инструкции popcnt уже достаточно; с точки зрения компилятора Go, AVX2 и AVX-512 здесь ничего не добавляют.

Ускорение popcount — очевидный выигрыш. А что насчёт остальной части библиотеки? Ещё один очевидный выигрыш — создание контейнера из плотной битовой карты. Бенчмарк FromDense для array-контейнеров берёт необработанный битовый вектор размером 8 КБ и строит для него наиболее компактный контейнер: считает popcount каждого слова, чтобы узнать мощность множества, а затем сканирует позиции установленных битов. Этот цикл подсчёта и сканирования по словам — именно то, что компилятор может автоматически векторизовать, как только станут доступны 256-битные регистры, поэтому выигрыш будет и после v2:

Время создания array-контейнера из плотной битовой карты

Время создания array-контейнера из плотной битовой карты

В v2 уже достигается сокращение времени на 21% за счёт скалярных инструкций popcnt/tzcnt, а в v3 (AVX2) это сокращение почти удваивается, достигая 38%. Как и в случае с popcount, v4 ничего не добавляет.

Операции с множествами демонстрируют ту же закономерность. Бенчмарк IntersectionCardinality считает, сколько общих значений у двух битовых карт: для bitmap-контейнеров он выполняет попарное AND над словами и подсчитывает результат. Здесь v2 по сути ничего не даёт, но v3 позволяет компилятору расширить цикл AND-and-count до 256-битных регистров, сокращая время на 22%:

Время определения мощности пересечения

Время определения мощности пересечения

Вывод автора: v2 — практически бесплатный выигрыш, и такой бинарник запустится почти на любом x86-64 процессоре, выпущенном за последние 15 лет (но на более старом CPU он упадёт при старте — это стоит помнить). v3 имеет смысл, если вы контролируете железо. А v4 в Go пока почти ничего не даёт: компилятор плохо использует AVX-512.

Жми сюда!

Жми сюда!

LLM как симптом зрелости

Самый резонансный материал этого сезона — статья «Отчёты об уязвимостях больше не являются чем-то особенным» Filippo Valsorda, бывшего лида безопасности в команде Go. Его тезис: годами репорты об уязвимостях считались особым видом обращений — исследователь оказывал проекту услугу, предоставляя дефицитную экспертизу и временную конфиденциальность, поэтому взамен ему полагались быстрая реакция и признание заслуг. Но если языковые модели способны находить примерно те же самые баги, что и опытный исследователь, — а Valsorda утверждает, что к 2026 году это уже так, — то ни экспертиза, ни конфиденциальность больше не дефицитный ресурс: атакующие точно так же могут прогнать код через LLM. Настоящим узким местом становится не поиск потенциальных проблем, а триаж — отделение реальных уязвимостей от шума.

Материал спровоцировал бурную дискуссию на Hacker News и Lobsters. Там указывали на проблему дублирования репортов: один и тот же баг в проекте вроде containerd независимо находят сразу несколько исследователей, — что как раз показывает: LLM воспроизводимо натыкаются на одни и те же проблемы вне зависимости от того, кто их запускает. А slop никуда не делся, просто теперь он лучше замаскирован под правдоподобный отчёт.

В этом же контексте показателен и курьёзный, но символический кейс из самого Go: уязвимость обхода проверки checksum в module proxy (потенциально позволяющая недоверенному прокси подсунуть изменённые модули или даже сам тулчейн) была обнаружена именно языковой моделью [нужна ссылка на issue/CVE] — живая иллюстрация тезиса Valsorda не в теории, а на практике, причём в инфраструктуре самого языка.

LLM как напарник в профилировании и отладке GC

Второй пример — это не поиск багов, а анализ производительности, который традиционно требует отдельной, довольно узкой экспертизы: чтения CPU-профилей, трейсов, ассемблерных дампов.

Инженер Red Hat описал эксперимент, где Claude выступил партнёром по анализу производительности нового сборщика мусора Go — Green Tea GC — на архитектуре POWER10 с использованием набора бенчмарков sweet. Штатный go tool pprof умеет находить узкие места вплоть до конкретных инструкций, но интерпретация профиля и формулирование гипотез — по-прежнему ручная, требующая опыта работа. По описанию автора, модель помогала находить конкретные направления для оптимизации прямо в горячих точках, размеченных pprof, — вплоть до предложений вроде подстройки GC, снижающих число событий mark assist.

go tool pprof BleveIndexBatch100-1212217505-cpu.prof
File: bleve-index-bench
Build ID: 81f203eb5714d9755a42c833024d5f60afd94e03
Type: cpu
Time: 2026-03-10 16:10:56 IST
Duration: 5.40s, Total samples = 15.18s (280.91%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
Showing nodes accounting for 8830ms, 58.17% of 15180ms total
Dropped 208 nodes (cum <= 75.90ms)
Showing top 10 nodes out of 103
  flat  flat%   sum%    cum   cum%
 3170ms 20.88% 20.88%   4260ms 28.06%  runtime.tryDeferToSpanScan
 1100ms  7.25% 28.13%   2970ms 19.57%  runtime.scanObjectSmall
 840ms  5.53%  33.66%    900ms  5.93%  github.com/blevesearch/segment.segmentWords
 710ms  4.68%  38.34%   7180ms 47.30%  runtime.scanSpan
 670ms  4.41%  42.75%   2940ms 19.37%  runtime.scanObjectsSmall
 520ms  3.43%  46.18%    970ms  6.39%  github.com/blevesearch/bleve/index/store/gtreap.itemCompare
 520ms  3.43%  49.60%    3610ms 23.78%  github.com/steveyen/gtreap.(*Treap).union
 440ms  2.90%  52.50%    2070ms 13.64%  github.com/steveyen/gtreap.(*Treap).split
 430ms  2.83%  55.34%      450ms  2.96%  runtime.extractHeapBitsSmall
 430ms  2.83%  58.17%     1690ms 11.13%  runtime.mallocgcSmallScanNoHeader

Есть в этом и сюжет в сюжете: сам объект профилирования — Green Tea GC — совсем свежий компонент рантайма. Его архитектура, ориентированная на блоки памяти и обрабатывающая мелкие объекты (до 512 байт) на гранулярности 8 KiB span, задумана ради лучшей cache locality. Разработчики оптимизируют рантайм языка, а LLM помогает разработчикам понять, что именно они оптимизировали.

У этих историй общий знаменатель: LLM не заменяют экспертизу Go-инженера, а меняют то, на что эта экспертиза тратится. Меньше времени уходит на ручной разбор потока security-репортов и на чтение сырых профилей и трейсов. Больше времени остаётся на то, что модель пока не умеет делать хорошо: решать, какая из десяти находок реально важна и что с ней делать дальше.

При этом важна и обратная сторона: те же материалы прямо предупреждают, что slop никуда не делся, а допуск LLM к операциям в рабочем окружении добавляет новых вопросов про безопасность самого тулчейна. Так что расширение возможностей приходит с расширением того, что стоит перепроверять вручную.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.