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