Deep Engineering

MEASUREMENT

bench/gosync/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/sync-atomic

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

197 lines
//go:build ignore

// Две задачи урока про sync и atomic: обе отвечают прогоном.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Три вещи, которые путают: инкремент без
// синхронизации теряет обновления; sync.Once выполняет функцию ровно один раз
// даже при гонке; а копия структуры с мьютексом — это уже другой мьютекс.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз мьютекс дороже атомарной
// операции на ОДНОМ И ТОМ ЖЕ счётчике без конкуренции. Именно без
// конкуренции: это тот случай, о котором и спорят, и в нём разница видна
// чище всего.
//
// ЗАПУСК:
//
//	go run bench/gosync/practice.go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
	"testing"
)

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

const (
	workers = 8
	perGo   = 10000
)

// unsynced — инкремент обычной переменной из восьми горутин. Печатается не
// само значение (оно недетерминированное), а факт потери: меньше ли оно
// ожидаемого.
//
// ПОЧЕМУ ЗДЕСЬ ЯВНАЯ УСТУПКА ПРОЦЕССОРА. Первая редакция писала просто `n++`
// в цикле — и один из прогонов дал РОВНО 80 000, то есть «потери нет». Это не
// опровержение гонки, а её главное свойство: она проявляется не всегда.
// Задача, ответ которой меняется от запуска к запуску, негодна, поэтому
// чтение и запись здесь разведены явно. Так гонка становится наблюдаемой
// каждый раз — а в обычном `n++` она остаётся ровно такой же ошибкой,
// просто менее заметной.
func unsynced() bool {
	n := 0
	var wg sync.WaitGroup
	wg.Add(workers)
	for w := 0; w < workers; w++ {
		go func() {
			defer wg.Done()
			for i := 0; i < perGo; i++ {
				v := n
				runtime.Gosched()
				n = v + 1
			}
		}()
	}
	wg.Wait()
	return n < workers*perGo
}

// syncedAtomic — тот же счётчик атомарно.
func syncedAtomic() int64 {
	var n atomic.Int64
	var wg sync.WaitGroup
	wg.Add(workers)
	for w := 0; w < workers; w++ {
		go func() {
			defer wg.Done()
			for i := 0; i < perGo; i++ {
				n.Add(1)
			}
		}()
	}
	wg.Wait()
	return n.Load()
}

// onceCount — сколько раз выполнится функция под sync.Once, если её дёрнуть
// из восьми горутин сразу.
func onceCount() int64 {
	var once sync.Once
	var calls atomic.Int64
	var wg sync.WaitGroup
	wg.Add(workers)
	for w := 0; w < workers; w++ {
		go func() {
			defer wg.Done()
			once.Do(func() { calls.Add(1) })
		}()
	}
	wg.Wait()
	return calls.Load()
}

// counter — структура с мьютексом внутри. Копия такой структуры получает
// СВОЙ мьютекс и свои данные.
type counter struct {
	mu sync.Mutex
	n  int
}

func (c *counter) inc() {
	c.mu.Lock()
	c.n++
	c.mu.Unlock()
}

// copiedStruct показывает, что копия — это другой объект.
func copiedStruct() (int, int) {
	orig := &counter{}
	orig.inc()
	cp := *orig //nolint:govet // копирование мьютекса здесь и есть предмет опыта
	cp.inc()
	return orig.n, cp.n
}

func task1() {
	fmt.Println(unsynced())
	fmt.Println(syncedAtomic())
	fmt.Println(onceCount())
	a, b := copiedStruct()
	fmt.Println(a, b)
}

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

// ПОЧЕМУ БЕЗ КОНКУРЕНЦИИ. Спор «мьютекс или atomic» почти всегда идёт про
// счётчик, к которому ходят из многих горутин. Но чтобы увидеть разницу самих
// ПРИМИТИВОВ, конкуренцию надо убрать: иначе замер покажет цену ожидания, а
// она у обоих своя и зависит от числа горутин.
//
// Замер с конкуренцией сделан отдельно, в internals.go, — и там картина
// другая, что и есть главный вывод урока.

var (
	mu      sync.Mutex
	plain   int64
	atomCnt atomic.Int64
)

func withMutex(b *testing.B) {
	for i := 0; i < b.N; i++ {
		mu.Lock()
		plain++
		mu.Unlock()
	}
}

func withAtomic(b *testing.B) {
	for i := 0; i < b.N; i++ {
		atomCnt.Add(1)
	}
}

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

	m, a := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		mm := nsPerOp(testing.Benchmark(withMutex))
		aa := nsPerOp(testing.Benchmark(withAtomic))
		if m == 0 || mm < m {
			m = mm
		}
		if a == 0 || aa < a {
			a = aa
		}
	}

	fmt.Println("ЗАДАЧА 2 — во сколько раз мьютекс дороже atomic без конкуренции")
	fmt.Println()
	fmt.Printf("  mu.Lock/Unlock + инкремент  %9.2f нс\n", m)
	fmt.Printf("  atomic.Int64.Add            %9.2f нс\n", a)
	fmt.Printf("  кратность                   %9.2f\n", m/a)
	fmt.Printf("  она же округлённо           %9.1f\n", m/a)
	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("  см. bench/gosync/internals.go.")
}