Deep Engineering

ЗАМЕР

bench/gorecv/internals.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/interview/golang/method-receivers

Запись прогона

У этого замера записи прогона нет — только скрипт.

Скрипт

268 строк
//go:build ignore

// Получатель метода изнутри: набор методов, копия и её цена по размеру.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про получателя обычно говорят двумя правилами —
// «указатель, если надо менять» и «значение дешевле» — и оба непроверяемы на
// слух. Первое проверяется набором методов: `reflect` показывает, что у `T` и
// у `*T` наборы РАЗНЫЕ, и это не стилистика, а причина ошибки компиляции.
// Второе проверяется лестницей размеров: «значение дешевле» верно до
// какого-то размера, и вопрос ровно в том, до какого.
//
// ЧТО ЗДЕСЬ НЕ МЕРЯЕТСЯ ВРЕМЕНЕМ. Первый и третий блоки времени не меряют
// вовсе: они печатают наблюдения. Это сделано намеренно — соблазн подпереть
// временем то, что временем не проверяется, и есть главный способ соврать
// числом.
//
// ЗАПУСК:
//
//	go run bench/gorecv/internals.go
package main

import (
	"fmt"
	"reflect"
	"runtime"
	"testing"
	"unsafe"
)

// ------------------------------------------------------------ блок 1: наборы

type Point struct{ X, Y int }

// String объявлен с получателем-УКАЗАТЕЛЕМ — и это решает всё дальнейшее.
func (p *Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }

// Len объявлен с получателем-значением и попадает в оба набора.
func (p Point) Len() int { return p.X*p.X + p.Y*p.Y }

type Stringer interface{ String() string }

func methodSets() {
	fmt.Println("НАБОР МЕТОДОВ: У ЗНАЧЕНИЯ И У УКАЗАТЕЛЯ ОН РАЗНЫЙ")
	fmt.Println("────────────────────────────────────────────────")

	valueType := reflect.TypeOf(Point{})
	ptrType := reflect.TypeOf(&Point{})

	list := func(t reflect.Type) []string {
		out := make([]string, 0, t.NumMethod())
		for i := 0; i < t.NumMethod(); i++ {
			out = append(out, t.Method(i).Name)
		}
		return out
	}

	fmt.Printf("  методы Point    %v\n", list(valueType))
	fmt.Printf("  методы *Point   %v\n", list(ptrType))
	fmt.Println()

	iface := reflect.TypeOf((*Stringer)(nil)).Elem()
	fmt.Printf("  Point  реализует Stringer:  %v\n", valueType.Implements(iface))
	fmt.Printf("  *Point реализует Stringer:  %v\n", ptrType.Implements(iface))
	fmt.Println()
	fmt.Println("  Метод с получателем-указателем в набор методов ЗНАЧЕНИЯ не")
	fmt.Println("  входит, а метод с получателем-значением входит в оба набора.")
	fmt.Println("  Отсюда несимметричность: `var s Stringer = Point{}` не")
	fmt.Println("  компилируется, `var s Stringer = &Point{}` компилируется.")
	fmt.Println()
	fmt.Println("  Причина не в синтаксисе: значение в интерфейсе — копия, и")
	fmt.Println("  адреса у неё нет. Метод с получателем-указателем не смог бы")
	fmt.Println("  изменить оригинал, а тихо менять копию хуже ошибки сборки.")
	fmt.Println()
}

// ------------------------------------------------------------ блок 2: цена

// ЛЕСТНИЦА РАЗМЕРОВ. Одно измерение на «большой структуре» доказывало бы
// только то, что большая структура большая. Правило, которое читателю нужно,
// звучит иначе: с какого размера копия вообще начинает стоить. Поэтому здесь
// пять размеров, и у каждого один и тот же метод — прочитать два поля.

type s1 struct{ d [1]int64 }
type s2 struct{ d [2]int64 }
type s8 struct{ d [8]int64 }
type s32 struct{ d [32]int64 }
type s128 struct{ d [128]int64 }

func (v s1) sumV() int64    { return v.d[0] }
func (v *s1) sumP() int64   { return v.d[0] }
func (v s2) sumV() int64    { return v.d[0] + v.d[1] }
func (v *s2) sumP() int64   { return v.d[0] + v.d[1] }
func (v s8) sumV() int64    { return v.d[0] + v.d[7] }
func (v *s8) sumP() int64   { return v.d[0] + v.d[7] }
func (v s32) sumV() int64   { return v.d[0] + v.d[31] }
func (v *s32) sumP() int64  { return v.d[0] + v.d[31] }
func (v s128) sumV() int64  { return v.d[0] + v.d[127] }
func (v *s128) sumP() int64 { return v.d[0] + v.d[127] }

var (
	a1   s1
	a2   s2
	a8   s8
	a32  s32
	a128 s128
	sink int64
)

type row struct {
	name  string
	bytes uintptr
	value func(*testing.B)
	ptr   func(*testing.B)
}

func rows() []row {
	return []row{
		{"1 слово", unsafe.Sizeof(a1),
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a1.sumV()
				}
			},
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a1.sumP()
				}
			}},
		{"2 слова", unsafe.Sizeof(a2),
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a2.sumV()
				}
			},
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a2.sumP()
				}
			}},
		{"8 слов", unsafe.Sizeof(a8),
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a8.sumV()
				}
			},
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a8.sumP()
				}
			}},
		{"32 слова", unsafe.Sizeof(a32),
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a32.sumV()
				}
			},
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a32.sumP()
				}
			}},
		{"128 слов", unsafe.Sizeof(a128),
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a128.sumV()
				}
			},
			func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					sink += a128.sumP()
				}
			}},
	}
}

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 costLadder() {
	fmt.Println("ЦЕНА КОПИИ ПО РАЗМЕРУ ПОЛУЧАТЕЛЯ")
	fmt.Println("────────────────────────────────")
	fmt.Printf("  %-10s %7s %12s %12s %10s\n", "размер", "байт", "значение", "указатель", "кратность")
	for _, r := range rows() {
		v := best(r.value)
		p := best(r.ptr)
		fmt.Printf("  %-10s %7d %9.2f нс %9.2f нс %10.2f\n", r.name, r.bytes, v, p, v/p)
	}
	fmt.Println()
	fmt.Println("  Читать эту таблицу надо снизу вверх. Внизу видно то, о чём")
	fmt.Println("  спрашивают, — копия стоит денег. Наверху видно то, о чём")
	fmt.Println("  забывают: на одном-двух словах разницы нет, потому что")
	fmt.Println("  копировать нечего — значение и так лежит в регистрах.")
	fmt.Println()
	fmt.Println("  Поэтому «получатель-указатель быстрее» — не правило, а")
	fmt.Println("  зависимость от размера. Правило выбора другое, и оно не про")
	fmt.Println("  скорость: указатель нужен, когда метод меняет получателя или")
	fmt.Println("  когда тип и так живёт по указателю.")
	fmt.Println()
}

// ------------------------------------------------------------ блок 3: копии

// Три места, где копия получателя возникает НЕЗАМЕТНО. Все три — частые
// вопросы «найдите ошибку», и ни одно не ловится компилятором.

type Acc struct{ n int }

func (a *Acc) Add() { a.n++ }

func hiddenCopies() {
	fmt.Println("ГДЕ КОПИЯ ВОЗНИКАЕТ НЕЗАМЕТНО")
	fmt.Println("─────────────────────────────")

	// 1. range по срезу значений: v — копия элемента.
	byRange := []Acc{{}, {}}
	for _, v := range byRange {
		v.Add()
	}
	fmt.Printf("  range по значению        %d  (изменения ушли в копию)\n", byRange[0].n)

	byIndex := []Acc{{}, {}}
	for i := range byIndex {
		byIndex[i].Add()
	}
	fmt.Printf("  range по индексу         %d\n", byIndex[0].n)

	// 2. значение из карты не адресуемо: `m["k"].Add()` не компилируется
	//    вовсе. Печатаем то, что компилируется, — и это уже другой смысл.
	m := map[string]*Acc{"k": {}}
	m["k"].Add()
	fmt.Printf("  карта указателей         %d  (с map[string]Acc не собралось бы)\n", m["k"].n)

	// 3. присваивание структуры — копия целиком.
	src := Acc{n: 5}
	cp := src
	cp.Add()
	fmt.Printf("  присваивание структуры   src=%d cp=%d\n", src.n, cp.n)

	fmt.Println()
	fmt.Println("  Общее у всех трёх: вызов выглядит одинаково. Компилятор сам")
	fmt.Println("  берёт адрес, когда переменная адресуема, и молчит; когда")
	fmt.Println("  адреса нет — как у элемента карты, — он отказывается собирать.")
	fmt.Println("  Между этими двумя случаями и живёт ошибка.")
	fmt.Println()
}

func main() {
	fmt.Printf("%s %s/%s | получатель метода изнутри\n\n",
		runtime.Version(), runtime.GOOS, runtime.GOARCH)
	methodSets()
	hiddenCopies()
	costLadder()
}