MEASUREMENT
bench/gorace/detector.go
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/interview/golang/data-races
- How to run it
go run bench/gorace/internals.go go run bench/gorace/detector.go # собирает racy/* сам go run bench/gorace/limits.go # тоже go run bench/gorace/options.go go run bench/gorace/practice.go
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
Замеры для урока «Гонки данных»
| Файл | Что делает |
|---|---|
internals.go |
без -race: разорванные чтения структуры из двух слов и цикл по флагу без синхронизации |
detector.go |
собирает программы из racy/ с -race и без: отчёт детектора, гонка на невыполненном пути, состязание без гонки данных, цена режима -race по времени и памяти |
racy/counter |
гонка в чистом виде: две горутины пишут одну переменную |
racy/branch |
гонка только на редком пути, включаемом аргументом |
racy/checkact |
проверка и действие под разными захватами одного мьютекса |
racy/workload |
нагрузка без гонок для замера цены детектора |
limits.go |
границы детектора сверх detector.go: гонка без одновременности, выполненная гонка без отчёта, окно без runtime.Gosched(), go test -race на тесте с прошедшими проверками |
options.go |
код выхода по умолчанию и через GORACE=exitcode=N; сборка -race без cgo |
racy/seq |
запись и чтение, разнесённые на 100 мс, без синхронизации |
racy/missed |
гонка, которую в этом чередовании случайно упорядочил замок |
racy/testpkg |
тест с гонкой, все проверки которого проходят |
practice.go |
источник ответов двух практических задач урока |
go run bench/gorace/internals.go
go run bench/gorace/detector.go # собирает racy/* сам
go run bench/gorace/limits.go # тоже
go run bench/gorace/options.go
go run bench/gorace/practice.go
Записи прогонов — в runs/, go1.24.7 linux/amd64, GOMAXPROCS=2.
Что здесь важно прочитать правильно
В internals.go гонки намеренные. Это программа про эффект гонки, а не
про её обнаружение; запускать её с -race незачем.
Доли разорванных чтений шумят. От круга к кругу — от половины до трёх четвертей прочитанных пар. Устойчиво только то, что их не ноль. Поэтому в практической задаче печатается факт, а не доля.
Цикл for !done {} на go1.24.7 завершился. Это наблюдение, а не
гарантия: модель памяти про этот самый цикл говорит, что он может не
завершиться никогда. Урок обязан называть это так.
Что получилось
Детектор находит гонку, которая не испортила результат. racy/counter без
-race печатает правильные n = 2000; с -race — отчёт WARNING: DATA RACE
и код выхода 66.
Детектор не находит гонку, которая не выполнилась. racy/branch с -race
на обычном пути — код выхода 0 и ни одного отчёта; тот же бинарник с
аргументом rare — отчёт с указанием на main.go:24.
Детектор не находит состязание без гонки данных. racy/checkact: баланс
ушёл в минус в 936 попытках из 1000, отчётов ноль. Каждое обращение под
замком; ошибка — в том, что проверка и списание разнесены по двум захватам.
Число попыток зависит от явного runtime.Gosched() между проверкой и
действием — он поставлен, чтобы окно было видно в каждом прогоне.
Цена режима -race. Та же нагрузка, три серии подряд: по памяти
×4,8–5,0, по времени ×7,6–12,0. Время гуляет заметно, память — нет; поэтому
практическая задача спрашивает про память (×4,85 в её прогоне).
Одновременность детектору не нужна. racy/seq: запись и чтение разнесены
на 100 мс, синхронизации между ними нет — отчёт в 5 запусках из 5, в том числе
при GOMAXPROCS=1.
Гоночная программа может дать исполнение без гонки. racy/missed: запись
и чтение мимо замка, но в этих запусках Unlock в горутине случается раньше
Lock в main, обращения оказываются упорядочены, и в этом исполнении гонки
данных нет — 0 отчётов за 5 запусков. Детектор проверяет исполнение, а не все
чередования, которые программа допускает.
Без runtime.Gosched() окно остаётся. racy/checkact nogosched: 44, 20 и
2 ухода в минус из 1000 при нуле отчётов. С Gosched в том же прогоне — 943
(в записи detector.txt — 936: разброс между прогонами).
go test -race падает на гонке, а не на проверке. racy/testpkg: проверка
теста не срабатывает, пакет testing пишет
race detected during execution of test, код выхода go test — 1.
66 — значение по умолчанию. options.go: тот же отчёт с
GORACE=exitcode=13 завершается с кодом 13.
На linux -race требует cgo. CGO_ENABLED=0 go build -race отказывается:
-race requires cgo. По исходнику cmd/go go1.24.7 исключение — macOS.
Script
177 lines//go:build ignore
// Детектор гонок: что он находит, чего не находит и сколько стоит.
//
// ЗАЧЕМ ОТДЕЛЬНАЯ ПРОГРАММА. Отчёт детектора появляется только у бинарника,
// собранного с -race, а главные вопросы о детекторе — сравнительные: тот же
// код с флагом и без, тот же код с гонкой на выполненном пути и на
// невыполненном. Поэтому здесь программы из racy/ собираются дважды и
// запускаются как отдельные процессы, а их вывод и коды выхода печатаются.
//
// ЧЕТЫРЕ СЮЖЕТА:
//
// 1. Гонка, которая не испортила результат. Детектор её всё равно видит:
// он ищет не неверный ответ, а доступы без happens-before.
// 2. Гонка на пути, который не выполнился. Детектор её не видит.
// 3. Состязание без гонки данных (проверка и действие под разными захватами
// замка). Детектор молчит, а ошибка есть.
// 4. Цена: время и пиковая память той же нагрузки с -race и без.
//
// ОТЧЁТ ПЕЧАТАЕТСЯ НЕ ЦЕЛИКОМ. Адреса в памяти, абсолютные пути и смещения
// (+0x99) меняются от машины к машине и от сборки к сборке; они заменены, а
// строки с номерами строк исходника оставлены как есть.
//
// ЗАПУСК:
//
// go run bench/gorace/detector.go
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"syscall"
"time"
)
var here string
func build(dir string, race bool) string {
out := filepath.Join(os.TempDir(), "gorace-"+filepath.Base(dir)+map[bool]string{true: "-race", false: ""}[race])
args := []string{"build", "-o", out}
if race {
args = append(args, "-race")
}
args = append(args, ".")
cmd := exec.Command("go", args...)
cmd.Dir = filepath.Join(here, "racy", dir)
if b, err := cmd.CombinedOutput(); err != nil {
panic(fmt.Sprintf("сборка %s: %v\n%s", dir, err, b))
}
return out
}
type result struct {
out string
code int
wall time.Duration
maxrssKB int64
}
func run(bin string, args ...string) result {
var buf bytes.Buffer
cmd := exec.Command(bin, args...)
cmd.Stdout, cmd.Stderr = &buf, &buf
start := time.Now()
err := cmd.Run()
wall := time.Since(start)
code := 0
if ee, ok := err.(*exec.ExitError); ok {
code = ee.ExitCode()
} else if err != nil {
panic(err)
}
ru := cmd.ProcessState.SysUsage().(*syscall.Rusage)
return result{buf.String(), code, wall, ru.Maxrss}
}
var (
addr = regexp.MustCompile(`0x[0-9a-f]{6,}`)
offset = regexp.MustCompile(` \+0x[0-9a-f]+$`)
path = regexp.MustCompile(`\S*/racy/`)
)
func clean(s string) string {
var out []string
for _, line := range strings.Split(strings.TrimRight(s, "\n"), "\n") {
line = addr.ReplaceAllString(line, "0x…")
line = offset.ReplaceAllString(line, "")
line = path.ReplaceAllString(line, "racy/")
if strings.TrimSpace(line) == "" {
out = append(out, "")
continue
}
out = append(out, " "+line)
}
return strings.Join(out, "\n")
}
func show(title string) {
fmt.Println()
fmt.Println(title)
fmt.Println(strings.Repeat("─", len([]rune(title))))
}
func median(xs []float64) float64 {
sort.Float64s(xs)
return xs[len(xs)/2]
}
func main() {
_, file, _, _ := runtime.Caller(0)
here = filepath.Dir(file)
fmt.Printf("%s %s/%s, GOMAXPROCS=%d | детектор гонок\n", runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
show("1) ГОНКА, КОТОРАЯ НЕ ИСПОРТИЛА РЕЗУЛЬТАТ")
plain := run(build("counter", false))
fmt.Printf(" без -race: %s, код выхода %d\n", strings.TrimSpace(plain.out), plain.code)
raced := run(build("counter", true))
fmt.Printf(" с -race, код выхода %d:\n", raced.code)
fmt.Println(clean(raced.out))
show("2) ГОНКА НА ПУТИ, КОТОРЫЙ НЕ ВЫПОЛНИЛСЯ")
bin := build("branch", true)
common := run(bin)
fmt.Printf(" с -race, обычный путь: код выхода %d, отчётов %d\n",
common.code, strings.Count(common.out, "WARNING: DATA RACE"))
rare := run(bin, "rare")
fmt.Printf(" с -race, редкий путь: код выхода %d, отчётов %d\n",
rare.code, strings.Count(rare.out, "WARNING: DATA RACE"))
for _, l := range strings.Split(clean(rare.out), "\n") {
if strings.Contains(l, "main.go:") {
fmt.Println(" в отчёте:", strings.TrimSpace(l))
break
}
}
show("3) СОСТЯЗАНИЕ БЕЗ ГОНКИ ДАННЫХ")
ca := run(build("checkact", true))
fmt.Printf(" с -race: %s; код выхода %d, отчётов %d\n",
strings.TrimSpace(ca.out), ca.code, strings.Count(ca.out, "WARNING: DATA RACE"))
fmt.Println(" Каждое обращение к балансу под замком, поэтому гонки данных")
fmt.Println(" нет и детектору нечего сообщить. Ошибка — в том, что проверка")
fmt.Println(" и списание сделаны под РАЗНЫМИ захватами.")
show("4) ЦЕНА: ТА ЖЕ НАГРУЗКА С -race И БЕЗ")
const rounds = 5
plainBin, raceBin := build("workload", false), build("workload", true)
measure := func(b string) (float64, float64) {
var ts, ms []float64
for i := 0; i < rounds; i++ {
r := run(b)
ts = append(ts, float64(r.wall.Microseconds())/1000)
ms = append(ms, float64(r.maxrssKB)/1024)
}
return median(ts), median(ms)
}
fmt.Printf(" %-8s %10s %10s %9s %10s %10s %9s\n", "серия",
"время", "с -race", "кратн.", "память", "с -race", "кратн.")
for series := 1; series <= 3; series++ {
t0, m0 := measure(plainBin)
t1, m1 := measure(raceBin)
fmt.Printf(" %-8d %7.0f мс %7.0f мс %8.1fx %6.1f МиБ %6.1f МиБ %8.1fx\n",
series, t0, t1, t1/t0, m0, m1, m1/m0)
}
fmt.Println()
fmt.Printf(" В каждой серии — медиана из %d запусков. Кратность зависит от\n", rounds)
fmt.Println(" нагрузки и от прогона к прогону гуляет заметно — особенно по")
fmt.Println(" времени. Число переносить нельзя, порядок — можно: -race — это")
fmt.Println(" режим тестов и стендов, а не продакшена.")
}