Deep Engineering

MEASUREMENT

bench/goslice/racesrc/main.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/go/data-structures/slices

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

159 lines
// Подопытная программа для bench/goslice/race_shared.go.
//
// Четыре режима: два ломаных и два починенных. Ломаные существуют ровно для
// того, чтобы детектор гонок было на чём показать, — это материал защитный,
// а не рецепт. Оба лечатся, и лечение здесь же, рядом, в тех же терминах.
//
// Запускать этот файл руками не нужно, его запускает драйвер:
//
//	go run bench/goslice/race_shared.go
//
// Вручную, если хочется:
//
//	cd bench/goslice && go run -race ./racesrc overlap
package main

import (
	"fmt"
	"os"
	"slices"
	"sync"
)

// sinkA и sinkB не дают компилятору выбросить результат append как
// неиспользуемый. Их ДВА, и каждый пишется ровно одной горутиной: один общий
// sink сам стал бы гонкой и увёл бы отчёт детектора не туда.
var (
	sinkA []int
	sinkB []int
)

const iters = 2000

func main() {
	if len(os.Args) < 2 {
		fmt.Fprintln(os.Stderr, "режим: overlap | appendshared | fixedsplit | fixedclip")
		os.Exit(2)
	}
	switch os.Args[1] {
	case "overlap":
		overlap()
	case "appendshared":
		appendShared()
	case "fixedsplit":
		fixedSplit()
	case "fixedclip":
		fixedClip()
	default:
		fmt.Fprintln(os.Stderr, "неизвестный режим:", os.Args[1])
		os.Exit(2)
	}
}

// overlap — ЛОМАНЫЙ случай №1: два среза нарезаны из одного массива с
// перехлёстом. a[2] и b[0] — это один и тот же элемент buf[2].
// Ни один из индексов не выглядит подозрительно; общий у них массив, а не
// индекс.
func overlap() {
	buf := make([]int, 8)
	a := buf[0:4]
	b := buf[2:6] // перехлёст: buf[2] и buf[3] попали в оба среза

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			a[2] = i // == buf[2]
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			b[0] = i // == buf[2], тот же адрес
		}
	}()
	wg.Wait()
	fmt.Println("overlap: buf =", buf)
}

// appendShared — ЛОМАНЫЙ случай №2: тот же массив, но общий у срезов не
// элемент, а ЗАПАС. Оба среза имеют len=1 и cap=8, поэтому оба append пишут
// в buf[1] — в ячейку, которой ни в одном из срезов ещё нет.
// Это та же ошибка, от которой спасает slices.Clip.
func appendShared() {
	buf := make([]int, 1, 8)
	a := buf[:1] // len=1 cap=8
	b := buf[:1] // len=1 cap=8

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			sinkA = append(a, i) // пишет в buf[1]
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			sinkB = append(b, -i) // пишет в тот же buf[1]
		}
	}()
	wg.Wait()
	fmt.Println("appendshared: buf[:cap] =", buf[:cap(buf)])
}

// fixedSplit — ЛЕЧЕНИЕ случая №1: непересекающиеся половины.
// Массив по-прежнему один, и это нормально: гонка бывает не от общего
// массива, а от общей ЯЧЕЙКИ. Пока диапазоны не пересекаются, писать в них
// параллельно можно.
func fixedSplit() {
	buf := make([]int, 8)
	a := buf[0:4]
	b := buf[4:8]

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			a[2] = i // buf[2]
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			b[0] = i // buf[4] — другая ячейка
		}
	}()
	wg.Wait()
	fmt.Println("fixedsplit: buf =", buf)
}

// fixedClip — ЛЕЧЕНИЕ случая №2: отобрать у среза чужой запас.
// slices.Clip(s) — это s[:len(s):len(s)]. После него cap == len, и append
// обязан выделить новый массив, а значит горутины перестают делить ячейку.
func fixedClip() {
	buf := make([]int, 1, 8)
	a := slices.Clip(buf[:1]) // len=1 cap=1
	b := slices.Clip(buf[:1]) // len=1 cap=1

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			sinkA = append(a, i) // новый массив на каждый append
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < iters; i++ {
			sinkB = append(b, -i)
		}
	}()
	wg.Wait()
	fmt.Println("fixedclip: buf[:cap] =", buf[:cap(buf)], "(запас не тронут)")
}