Deep Engineering

MEASUREMENT

bench/gocontext/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/context

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

142 lines
//go:build ignore

// Две задачи урока про context: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Четыре вещи, которые путают: отмена родителя
// доходит до потомка, но не наоборот; Err() у отменённого по таймауту и по
// вызову cancel — РАЗНЫЕ значения; Done() у context.Background() никогда не
// закрывается; и значение, положенное в потомка, невидимо родителю.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз дороже достать значение из
// контекста, вложенного на десять уровней, чем на один. Поиск идёт по
// цепочке — ровно как у errors.Is.
//
// ЗАПУСК:
//
//	go run bench/gocontext/practice.go
package main

import (
	"context"
	"errors"
	"fmt"
	"runtime"
	"testing"
	"time"
)

// ---------------------------------------------------------------- задача 1

type ctxKey string

const userKey ctxKey = "user"

func task1() {
	// Отмена родителя доходит до потомка.
	parent, cancelParent := context.WithCancel(context.Background())
	child, cancelChild := context.WithCancel(parent)
	defer cancelChild()
	cancelParent()
	<-child.Done()
	fmt.Println(errors.Is(child.Err(), context.Canceled))

	// А отмена потомка родителя не трогает.
	p2, cancelP2 := context.WithCancel(context.Background())
	defer cancelP2()
	_, cancelC2 := context.WithCancel(p2)
	cancelC2()
	fmt.Println(p2.Err() == nil)

	// Таймаут и ручная отмена дают РАЗНЫЕ ошибки. Две строки подряд —
	// намеренно: одна без другой не показывает, что они не взаимозаменяемы.
	tctx, cancelT := context.WithTimeout(context.Background(), time.Millisecond)
	defer cancelT()
	<-tctx.Done()
	fmt.Println(errors.Is(tctx.Err(), context.DeadlineExceeded))
	fmt.Println(errors.Is(tctx.Err(), context.Canceled))

	// Значение видно потомку, но не родителю.
	base := context.Background()
	withUser := context.WithValue(base, userKey, "roman")
	fmt.Println(withUser.Value(userKey))
	fmt.Println(base.Value(userKey))
}

// ---------------------------------------------------------------- задача 2

// ПОЧЕМУ СРАВНИВАЕТСЯ ГЛУБИНА, А НЕ «ЦЕНА WithValue».
//
// Спор про context.Value почти всегда идёт о том, «дорого ли класть». Класть
// дёшево — это одно выделение. Дорого ДОСТАВАТЬ, и цена зависит от того,
// сколько слоёв придётся пройти: Value идёт вверх по цепочке родителей, как
// errors.Is по цепочке обёрток.

const deep = 10

var (
	shallow = context.WithValue(context.Background(), userKey, "roman")
	nested  = buildChain(deep)
	sink    any
)

func buildChain(n int) context.Context {
	ctx := context.WithValue(context.Background(), userKey, "roman")
	for i := 0; i < n; i++ {
		ctx = context.WithValue(ctx, ctxKey(fmt.Sprintf("k%d", i)), i)
	}
	return ctx
}

func valueShallow(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sink = shallow.Value(userKey)
	}
}

func valueDeep(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sink = nested.Value(userKey)
	}
}

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()

	one, ten := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		oo := nsPerOp(testing.Benchmark(valueShallow))
		tt := nsPerOp(testing.Benchmark(valueDeep))
		if one == 0 || oo < one {
			one = oo
		}
		if ten == 0 || tt < ten {
			ten = tt
		}
	}

	fmt.Println("ЗАДАЧА 2 — во сколько раз дороже Value на глубине десяти слоёв")
	fmt.Println()
	fmt.Printf("  Value, один слой            %9.2f нс\n", one)
	fmt.Printf("  Value, десять слоёв         %9.2f нс\n", ten)
	fmt.Printf("  кратность                   %9.2f\n", ten/one)
	fmt.Printf("  она же округлённо           %9.1f\n", ten/one)
	fmt.Println()
	fmt.Printf("  Лучший из %d чередующихся кругов. %s %s/%s\n",
		rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Println()
	fmt.Println("  Работа одинаковая: достать одно и то же значение. Отличается")
	fmt.Println("  длина цепочки родителей — Value идёт по ней вверх, слой за")
	fmt.Println("  слоем. Отсюда и правило: контекст не хранилище, и класть в")
	fmt.Println("  него то, что читают в горячем пути, не стоит.")
}