Deep Engineering

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("  цена двух переключений — а не «горутины медленные».")
}