MEASUREMENT
bench/gosched/practice.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/scheduler
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
This measurement has no recorded run — only the script.
Script
148 lines//go:build ignore
// Две задачи урока про горутины и планировщик: обе отвечают прогоном.
//
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ ПОД ПРАКТИКУ. Читателю в задаче говорят «вы ответили
// неверно». Если верный ответ назначен редакцией, то ошибается не читатель, а
// материал. Поэтому обе задачи урока печатаются здесь.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Сколько стоит горутина по памяти и сколько
// живёт после выхода main. Первое число — размер стартового стека, который
// почти всегда называют неверно (обычно «2 КБ» помнят, но не проверяют).
// Второе — про то, что main не ждёт никого.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз запуск горутины дороже
// обычного вызова функции. Сравнивается одна и та же работа: инкремент
// счётчика под атомарной операцией.
//
// ЗАПУСК:
//
// go run bench/gosched/practice.go
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"testing"
)
// ---------------------------------------------------------------- задача 1
func task1() {
// Сколько горутин работает в пустой программе.
fmt.Println(runtime.NumGoroutine())
var wg sync.WaitGroup
wg.Add(1)
before := runtime.NumGoroutine()
go func() {
defer wg.Done()
}()
// Печатаем разницу, а не абсолютное число: абсолютное зависит от того,
// успела ли горутина завершиться, и было бы недетерминированным.
fmt.Println(runtime.NumGoroutine() - before)
wg.Wait()
// Память под тысячу спящих горутин, поделённая на тысячу.
//
// ПОЧЕМУ ДВА ЧИСЛА, А НЕ ОДНО. Первая редакция этого замера считала
// только HeapAlloc и получила ~500 байт — число, которое противоречит
// общеизвестному «горутина стоит два килобайта». Противоречия нет:
// стек горутины в HeapAlloc не входит вовсе, он учитывается отдельно,
// в StackInuse. Одно число без другого даёт неверный ответ на вопрос
// «сколько стоит горутина», поэтому печатаются оба.
var m1, m2 runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m1)
const n = 1000
stop := make(chan struct{})
var started sync.WaitGroup
started.Add(n)
for i := 0; i < n; i++ {
go func() {
started.Done()
<-stop
}()
}
started.Wait()
runtime.ReadMemStats(&m2)
// Стек — отдельной статистикой. Округляем до сотен байт: точное
// значение шумит, а порядок величины — то, что и спрашивают.
fmt.Println((m2.StackInuse - m1.StackInuse) / n / 100 * 100)
fmt.Println((m2.HeapAlloc - m1.HeapAlloc) / n / 100 * 100)
close(stop)
}
// ---------------------------------------------------------------- задача 2
// ПОЧЕМУ СРАВНИВАЕТСЯ ИМЕННО ЗАПУСК, А НЕ РАБОТА. Вопрос собеседования звучит
// как «горутины дешёвые?», и честный ответ требует числа именно для ЗАПУСКА:
// работа внутри у обоих вариантов одинаковая — один атомарный инкремент.
//
// Ожидание завершения включено в замер намеренно: без него мерился бы не
// запуск горутины, а постановка её в очередь, и результат был бы занижен.
var counter atomic.Int64
func direct(b *testing.B) {
for i := 0; i < b.N; i++ {
counter.Add(1)
}
}
func spawned(b *testing.B) {
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
wg.Add(1)
go func() {
counter.Add(1)
wg.Done()
}()
wg.Wait()
}
}
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func main() {
fmt.Println("ЗАДАЧА 1 — предсказать вывод")
fmt.Println()
task1()
fmt.Println()
d, s := 0.0, 0.0
for r := 0; r < rounds; r++ {
// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
dd := nsPerOp(testing.Benchmark(direct))
ss := nsPerOp(testing.Benchmark(spawned))
if d == 0 || dd < d {
d = dd
}
if s == 0 || ss < s {
s = ss
}
}
fmt.Println("ЗАДАЧА 2 — во сколько раз запуск горутины дороже вызова")
fmt.Println()
fmt.Printf(" прямой вызов %9.2f нс\n", d)
fmt.Printf(" через горутину с ожиданием %9.2f нс\n", s)
fmt.Printf(" кратность %9.2f\n", s/d)
fmt.Printf(" она же округлённо %9.1f\n", s/d)
fmt.Println()
fmt.Printf(" Лучший из %d чередующихся кругов. %s %s/%s, GOMAXPROCS=%d\n",
rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH, runtime.GOMAXPROCS(0))
fmt.Println()
fmt.Println(" Работа одинаковая: один атомарный инкремент. Отличается")
fmt.Println(" только то, что во втором случае её выполняет новая горутина,")
fmt.Println(" а вызывающий её дожидается. Это и есть цена запуска плюс")
fmt.Println(" цена двух переключений — а не «горутины медленные».")
}