Deep Engineering

ЗАМЕР

bench/gointerface/practice.go

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

Цитируется в статье
/ru/interview/golang/interfaces

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

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

Скрипт

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

// Две задачи урока про интерфейсы: обе отвечают прогоном, а не рассуждением.
//
// ЗАЧЕМ ОТДЕЛЬНЫЙ ФАЙЛ ПОД ПРАКТИКУ. Читателю в задаче говорят «вы ответили
// неверно». Если верный ответ назначен редакцией, то ошибается не читатель, а
// материал. Поэтому обе задачи урока печатаются здесь, и в уроке стоит ровно
// то, что напечатала эта программа.
//
// ЗАДАЧА 1 — ПРЕДСКАЗАТЬ ВЫВОД. Самая известная ловушка Go: функция вернула
// НУЛЕВОЙ УКАЗАТЕЛЬ в интерфейсной обёртке, а проверка `err != nil` считает
// такую ошибку существующей. Интерфейс хранит ДВА слова, и nil он равен,
// только когда оба пусты; здесь тип заполнен, а данные пусты.
//
// ЗАДАЧА 2 — ОЦЕНИТЬ КРАТНОСТЬ. Во сколько раз вызов через интерфейс дороже
// прямого вызова того же метода. Кратность печатается самой программой:
// деление в уме на стороне редакции — ровно тот шаг, где вкрадывается ошибка,
// которую потом нечем поймать.
//
// ЗАПУСК:
//
//	go run bench/gointerface/practice.go
package main

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

// ---------------------------------------------------------------- задача 1

type NotFound struct{ Key string }

func (e *NotFound) Error() string { return "не найдено: " + e.Key }

// find возвращает *NotFound — и в этом вся беда: тип конкретный, а значение nil.
func find(ok bool) error {
	var missing *NotFound
	if ok {
		return nil
	}
	return missing
}

// typedNil печатает то, что читатель должен предсказать.
//
// Код внутри намеренно записан так же, как он показан в уроке: проверка
// scripts/validate-practice.mjs сверяет показанные строки с этим файлом
// построчно, чтобы читатель предсказывал вывод именно этой программы.
func typedNil() {
	err := find(false)
	fmt.Println(err == nil)
	var p *NotFound
	fmt.Println(p == nil)
	fmt.Println(reflect.TypeOf(err), err == nil)
	fmt.Println(find(true) == nil)
}

// ---------------------------------------------------------------- задача 2

// ПОЧЕМУ ЗДЕСЬ ДВА КОНКРЕТНЫХ ТИПА И СРЕЗ, А НЕ ОДНА ПЕРЕМЕННАЯ.
//
// Первая редакция этого замера сравнивала вызов на переменной конкретного типа
// с вызовом на переменной интерфейсного — и получила кратность 0,87, то есть
// «через интерфейс быстрее». Это был не результат, а расписка в том, что замер
// негоден: у пакетной переменной интерфейсного типа компилятор знает
// единственный конкретный тип, девиртуализует вызов и встраивает его, а
// прямой вызов сворачивает в константу. Обе строки мерили пустоту.
//
// Чтобы вызов остался вызовом, нужны ДВА условия. Первое: в срезе лежат разные
// конкретные типы, поэтому предсказать цель на этапе компиляции нельзя.
// Второе: данные приходят из памяти, а не из литерала, поэтому результат не
// сворачивается.

type Shape interface{ Area() int }

type Rect struct{ w, h int }

func (r Rect) Area() int { return r.w * r.h }

type Square struct{ side int }

func (s Square) Area() int { return s.side * s.side }

const N = 4096

var (
	// Один и тот же набор фигур, разложенный двумя способами.
	shapes = func() []Shape {
		out := make([]Shape, N)
		for i := range out {
			if i%2 == 0 {
				out[i] = Rect{i % 17, i % 13}
			} else {
				out[i] = Square{i % 11}
			}
		}
		return out
	}()
	rects = func() []Rect {
		out := make([]Rect, N)
		for i := range out {
			out[i] = Rect{i % 17, i % 13}
		}
		return out
	}()
	sink int
)

// direct — вызов на конкретном типе: компилятор знает тело и встраивает его.
func direct(b *testing.B) {
	for i := 0; i < b.N; i++ {
		total := 0
		for j := range rects {
			total += rects[j].Area()
		}
		sink += total
	}
}

// through — тот же метод через интерфейс: цель берётся из таблицы методов, и
// предсказать её нельзя, потому что типов в срезе два.
func through(b *testing.B) {
	for i := 0; i < b.N; i++ {
		total := 0
		for j := range shapes {
			total += shapes[j].Area()
		}
		sink += total
	}
}

const rounds = 7

func nsPerOp(r testing.BenchmarkResult) float64 {
	return float64(r.T.Nanoseconds()) / float64(r.N)
}

func main() {
	fmt.Println("ЗАДАЧА 1 — предсказать вывод")
	fmt.Println()
	typedNil()
	fmt.Println()

	d, t := 0.0, 0.0
	for r := 0; r < rounds; r++ {
		// Круг: оба варианта подряд, чтобы просадка машины досталась обоим.
		dd := nsPerOp(testing.Benchmark(direct))
		tt := nsPerOp(testing.Benchmark(through))
		if d == 0 || dd < d {
			d = dd
		}
		if t == 0 || tt < t {
			t = tt
		}
	}

	fmt.Println("ЗАДАЧА 2 — во сколько раз вызов через интерфейс дороже прямого")
	fmt.Println()
	fmt.Printf("  прямой вызов, %d фигур      %9.0f нс\n", N, d)
	fmt.Printf("  через интерфейс             %9.0f нс\n", t)
	fmt.Printf("  кратность                   %9.2f\n", t/d)
	fmt.Printf("  она же округлённо           %9.1f\n", t/d)
	fmt.Println()
	fmt.Printf("  Лучший из %d чередующихся кругов. %s %s/%s\n",
		rounds, runtime.Version(), runtime.GOOS, runtime.GOARCH)
	fmt.Println()
	fmt.Println("  Дорожает не сам вызов, а то, что компилятор перестаёт видеть,")
	fmt.Println("  какой код будет выполнен: прямой вызов он встраивает целиком,")
	fmt.Println("  а вызов через интерфейс обязан пройти через таблицу методов.")
}