Deep Engineering

MEASUREMENT

bench/gointerface/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.

Cited in
/en/interview/golang/interfaces

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

204 lines
//go:build ignore

// Интерфейс изнутри: два слова, упаковка и цена утверждения типа.
//
// ЗАЧЕМ ЭТОТ ФАЙЛ. Про интерфейс в Go рассказывают словами «он хранит тип и
// значение», и на этом останавливаются. Из этой фразы не следует ни одного
// проверяемого числа, а следствий у неё три, и все три спрашивают:
//
//   * почему nil-указатель в интерфейсе НЕ равен nil — потому что слов два,
//     и пусто должно быть в обоих;
//   * почему упаковка иногда выделяет память, а иногда нет;
//   * во что обходится утверждение типа и чем оно отличается от type switch.
//
// ЗАПУСК:
//
//	go run bench/gointerface/internals.go
package main

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

func rule(title string) {
	fmt.Println()
	fmt.Println(title)
	fmt.Println(strings.Repeat("─", len([]rune(title))))
}

// ---------------------------------------------------------------- раскладка

type Stringer interface{ String() string }

type Point struct{ X, Y int }

func (p Point) String() string { return "точка" }

func layout() {
	rule("ДВА СЛОВА: ЧТО ЗАНИМАЕТ ИНТЕРФЕЙСНАЯ ПЕРЕМЕННАЯ")

	var empty any
	var named Stringer
	var ptr *Point
	fmt.Printf("  any        %2d байт  — тип и значение\n", unsafe.Sizeof(empty))
	fmt.Printf("  Stringer   %2d байт  — таблица методов и значение\n", unsafe.Sizeof(named))
	fmt.Printf("  *Point     %2d байт  — просто адрес\n", unsafe.Sizeof(ptr))
	fmt.Printf("  Point      %2d байт  — сама структура\n", unsafe.Sizeof(Point{}))
	fmt.Println()
	fmt.Println("  Шестнадцать байт — это ДВА слова, и в этом вся причина")
	fmt.Println("  знаменитой ловушки: интерфейс равен nil, только когда пусты")
	fmt.Println("  ОБА слова. Положили в него нулевой указатель — второе слово")
	fmt.Println("  пусто, а первое нет, и сравнение с nil даёт false.")
}

// ------------------------------------------------------------- упаковка

// ПОЧЕМУ ЗДЕСЬ ПЕРЕМЕННЫЕ, А НЕ ЛИТЕРАЛЫ.
//
// Первая редакция писала в интерфейс константы — `box = 42` и `box = 1000` — и
// получила ноль выделений в обеих строках. Это не результат про упаковку: у
// константы значение известно на этапе компиляции, и компилятор кладёт в
// интерфейс адрес статического объекта, какого бы размера ни было число.
// Чтобы увидеть упаковку, значение обязано приходить во время выполнения.

var (
	box     any
	small   = 42
	large   = 1000
	pt      = Point{1, 2}
	ptrToPt = &Point{1, 2}
)

func boxSmall()  { box = small }
func boxLarge()  { box = large }
func boxStruct() { box = pt }
func boxPtr()    { box = ptrToPt }

func boxing() {
	rule("УПАКОВКА: КОГДА ПОМЕЩЕНИЕ В ИНТЕРФЕЙС ВЫДЕЛЯЕТ ПАМЯТЬ")

	cases := []struct {
		name string
		fn   func()
	}{
		{"any = переменная int, значение 42", boxSmall},
		{"any = переменная int, значение 1000", boxLarge},
		{"any = переменная-структура Point", boxStruct},
		{"any = готовый указатель *Point", boxPtr},
	}
	for _, c := range cases {
		allocs := testing.AllocsPerRun(1000, c.fn)
		fmt.Printf("  %-38s выделений: %.0f\n", c.name, allocs)
	}
	fmt.Println()
	fmt.Println("  Второе слово интерфейса — это УКАЗАТЕЛЬ на значение, а не само")
	fmt.Println("  значение. Поэтому указатель кладётся как есть, а всё остальное")
	fmt.Println("  надо куда-то положить — и это выделение в куче.")
	fmt.Println("  Исключение видно на маленьком целом: у рантайма есть готовый")
	fmt.Println("  массив чисел от 0 до 255, и для них берётся адрес оттуда.")
}

// ------------------------------------------------- утверждение типа

// ПОЧЕМУ ОБЕ ФОРМЫ ДЕЛАЮТ ОДНО И ТО ЖЕ.
//
// Первая редакция сравнивала утверждение, которое складывало только целые, с
// type switch, который вдобавок считал длину строк, — и получила разницу почти
// вдвое. Разница была не между формами, а между объёмами работы: замер сравнил
// несравнимое. Здесь обе формы складывают целые и игнорируют остальное.

var (
	values = func() []any {
		out := make([]any, 4096)
		for i := range out {
			if i%2 == 0 {
				out[i] = i
			} else {
				out[i] = "строка"
			}
		}
		return out
	}()
	got int
)

func assertOK(b *testing.B) {
	for i := 0; i < b.N; i++ {
		n := 0
		for _, v := range values {
			if x, ok := v.(int); ok {
				n += x
			}
		}
		got += n
	}
}

func typeSwitch(b *testing.B) {
	for i := 0; i < b.N; i++ {
		n := 0
		for _, v := range values {
			switch x := v.(type) {
			case int:
				n += x
			}
		}
		got += n
	}
}

const rounds = 7

// bestWorst — лучший и худший круг из rounds.
//
// Разброс здесь не украшение: без него нельзя сказать, значима ли разница
// между двумя формами. Если диапазоны перекрываются, замер их не различает.
func bestWorst(f func(*testing.B)) (float64, float64) {
	lo, hi := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		res := testing.Benchmark(f)
		ns := float64(res.T.Nanoseconds()) / float64(res.N)
		if lo == 0 || ns < lo {
			lo = ns
		}
		if ns > hi {
			hi = ns
		}
	}
	return lo, hi
}

func asserting() {
	rule("УТВЕРЖДЕНИЕ ТИПА ПРОТИВ TYPE SWITCH НА ОДИНАКОВОЙ РАБОТЕ")

	aLo, aHi := bestWorst(assertOK)
	sLo, sHi := bestWorst(typeSwitch)
	fmt.Printf("  if x, ok := v.(int); ok       %8.0f%.0f нс\n", aLo, aHi)
	fmt.Printf("  switch x := v.(type) { int }  %8.0f%.0f нс\n", sLo, sHi)
	fmt.Printf("  отношение лучших кругов       %8.2f\n", sLo/aLo)
	overlap := sLo <= aHi && aLo <= sHi
	fmt.Printf("  диапазоны перекрываются:      %8v\n", overlap)
	fmt.Println()
	if overlap {
		fmt.Println("  Диапазоны перекрываются — значит замер эти две формы НЕ")
		fmt.Println("  различает, и говорить, что одна быстрее, нельзя.")
	} else {
		fmt.Println("  Диапазоны не перекрываются: разница устойчива в пределах")
		fmt.Println("  этого прогона, но она мала и на выбор формы не влияет.")
	}
	fmt.Println("  Обе формы сравнивают указатель на тип. Выбирают их не по")
	fmt.Println("  скорости, а по тому, сколько типов надо различить: одну ветвь")
	fmt.Println("  пишут утверждением, несколько — переключателем.")
}

func main() {
	fmt.Printf("%s %s/%s | интерфейс изнутри\n", runtime.Version(), runtime.GOOS, runtime.GOARCH)
	layout()
	boxing()
	asserting()
}