MEASUREMENT
bench/gosched/internals.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
235 lines//go:build ignore
// Планировщик изнутри: GOMAXPROCS, вытеснение и что видно из программы.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про GMP обычно рассказывают схемой из трёх букв, и из
// неё не следует ни одного проверяемого утверждения. Проверяемых здесь три, и
// каждое меряется отдельно:
//
// 1. GOMAXPROCS ограничивает число горутин, выполняющихся ОДНОВРЕМЕННО, а не
// число существующих. Видно по масштабированию счётной задачи.
// 2. Планировщик вытесняющий с Go 1.14: горутина, не делающая вызовов, всё
// равно снимается с процессора. До этого такой цикл вешал сборку мусора.
// 3. Цена переключения — это цена ПАРКОВКИ, и она видна на runtime.Gosched.
//
// ЧЕГО ЗДЕСЬ НЕТ. Внутренних очередей P, кражи работы и hand-off тут не видно:
// они не наблюдаемы из программы, и подпирать рассказ о них числом было бы
// подделкой. Про них в уроке сказано отдельно и без чисел.
//
// ЗАПУСК:
//
// go run bench/gosched/internals.go
package main
import (
"fmt"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
// ------------------------------------------------------- блок 1: GOMAXPROCS
// spin — чистый счёт без единого вызова: ничего не выделяет, никуда не ходит.
func spin(n int) uint64 {
var x uint64
for i := 0; i < n; i++ {
x = x*1664525 + 1013904223
}
return x
}
var sink uint64
// parallelWork запускает workers горутин, каждая крутит spin, и возвращает
// время выполнения всей пачки.
func parallelWork(workers, iters int) time.Duration {
var wg sync.WaitGroup
wg.Add(workers)
start := time.Now()
for w := 0; w < workers; w++ {
go func() {
defer wg.Done()
atomic.AddUint64(&sink, spin(iters))
}()
}
wg.Wait()
return time.Since(start)
}
func maxprocs() {
fmt.Println("GOMAXPROCS ОГРАНИЧИВАЕТ ОДНОВРЕМЕННЫХ, А НЕ СУЩЕСТВУЮЩИХ")
fmt.Println("─────────────────────────────────────────────────────────")
cpus := runtime.NumCPU()
fmt.Printf(" ядер у машины %d\n", cpus)
fmt.Printf(" GOMAXPROCS по умолчанию %d\n", runtime.GOMAXPROCS(0))
fmt.Println()
const iters = 20_000_000
base := runtime.GOMAXPROCS(0)
defer runtime.GOMAXPROCS(base)
fmt.Printf(" %-14s %12s %12s\n", "GOMAXPROCS", "1 горутина", "4 горутины")
for _, p := range []int{1, 2} {
if p > cpus {
continue
}
runtime.GOMAXPROCS(p)
// Прогрев: первый прогон после смены GOMAXPROCS ловит перестройку.
parallelWork(1, iters/10)
one := parallelWork(1, iters)
four := parallelWork(4, iters)
fmt.Printf(" %-14d %10.1f мс %10.1f мс\n", p,
float64(one.Microseconds())/1000, float64(four.Microseconds())/1000)
}
runtime.GOMAXPROCS(base)
fmt.Println()
fmt.Println(" Читать надо по строкам. При GOMAXPROCS=1 четыре горутины")
fmt.Println(" занимают вчетверо больше времени, чем одна: они выполняются")
fmt.Println(" по очереди на одном процессоре. При GOMAXPROCS=2 те же")
fmt.Println(" четыре укладываются примерно вдвое — работают по две сразу.")
fmt.Println()
fmt.Println(" Горутин при этом всё время четыре. Ограничивается не их")
fmt.Println(" число, а число тех, что выполняются ОДНОВРЕМЕННО.")
fmt.Println()
}
// ------------------------------------------------- блок 2: вытеснение
// preemption проверяет, снимет ли планировщик с процессора горутину, которая
// не делает ни вызовов, ни выделений, ни обращений к каналам.
//
// До Go 1.14 такая горутина не снималась вовсе: планировщик был кооперативным
// и переключался только в точках вызова. Цикл вроде этого при GOMAXPROCS=1
// вешал программу насмерть — в том числе сборку мусора, которой нужно
// остановить все горутины.
func preemption() {
fmt.Println("ВЫТЕСНЕНИЕ: ЦИКЛ БЕЗ ВЫЗОВОВ ВСЁ РАВНО СНИМАЕТСЯ")
fmt.Println("────────────────────────────────────────────────")
base := runtime.GOMAXPROCS(1)
defer runtime.GOMAXPROCS(base)
var progressed atomic.Bool
done := make(chan struct{})
// Горутина-счётчик: ни одного вызова внутри цикла.
go func() {
x := uint64(0)
for i := 0; i < 400_000_000; i++ {
x = x*1664525 + 1013904223
}
atomic.AddUint64(&sink, x)
close(done)
}()
// Вторая горутина: если её ни разу не пустят на процессор, флаг
// останется снятым.
go func() {
for {
select {
case <-done:
return
default:
progressed.Store(true)
time.Sleep(time.Millisecond)
}
}
}()
select {
case <-done:
case <-time.After(10 * time.Second):
}
fmt.Printf(" GOMAXPROCS на время опыта 1\n")
fmt.Printf(" соседняя горутина работала %v\n", progressed.Load())
fmt.Println()
fmt.Println(" Счётный цикл не делает ни вызовов, ни выделений — то есть")
fmt.Println(" не даёт планировщику ни одной точки, где тот мог бы")
fmt.Println(" переключиться по-старому. Соседняя горутина всё равно")
fmt.Println(" получила процессор: с Go 1.14 планировщик вытесняющий и")
fmt.Println(" снимает горутину сигналом, а не по её доброй воле.")
fmt.Println()
fmt.Println(" До Go 1.14 такой цикл при GOMAXPROCS=1 вешал программу")
fmt.Println(" целиком, включая сборку мусора: ей нужно остановить все")
fmt.Println(" горутины, а эту остановить было нечем.")
fmt.Println()
}
// ------------------------------------------------- блок 3: цена парковки
var counter atomic.Int64
func noYield(b *testing.B) {
for i := 0; i < b.N; i++ {
counter.Add(1)
}
}
func withYield(b *testing.B) {
for i := 0; i < b.N; i++ {
counter.Add(1)
runtime.Gosched()
}
}
func withSleep(b *testing.B) {
for i := 0; i < b.N; i++ {
counter.Add(1)
time.Sleep(0)
}
}
func switchCost() {
fmt.Println("ЦЕНА ПЕРЕКЛЮЧЕНИЯ НА ОДИНАКОВОЙ РАБОТЕ")
fmt.Println("──────────────────────────────────────")
plain := best(noYield)
yield := best(withYield)
sleep := best(withSleep)
fmt.Printf(" атомарный инкремент %9.2f нс\n", plain)
fmt.Printf(" он же + runtime.Gosched() %9.2f нс\n", yield)
fmt.Printf(" он же + time.Sleep(0) %9.2f нс\n", sleep)
fmt.Printf(" цена уступки процессора %9.2f нс\n", yield-plain)
fmt.Println()
fmt.Println(" Работа одинаковая во всех трёх строках — один атомарный")
fmt.Println(" инкремент. Разница целиком в том, что горутина отдаёт")
fmt.Println(" процессор и потом получает его обратно.")
fmt.Println()
fmt.Println(" Отсюда практический вывод про каналы и мьютексы: дорожает")
fmt.Println(" не операция, а ПАРКОВКА, когда операция заставляет ждать.")
fmt.Println(" Пока горутина не блокируется, планировщик в её цене не")
fmt.Println(" участвует вовсе.")
fmt.Println()
}
// ------------------------------------------------------------ утилиты
const rounds = 7
func nsPerOp(r testing.BenchmarkResult) float64 {
return float64(r.T.Nanoseconds()) / float64(r.N)
}
func best(f func(*testing.B)) float64 {
out := 0.0
for r := 0; r < rounds; r++ {
v := nsPerOp(testing.Benchmark(f))
if out == 0 || v < out {
out = v
}
}
return out
}
func main() {
fmt.Printf("%s %s/%s | планировщик изнутри\n\n",
runtime.Version(), runtime.GOOS, runtime.GOARCH)
maxprocs()
preemption()
switchCost()
}