MEASUREMENT
bench/gorecv/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.
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
268 lines//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()
}