ЗАМЕР
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(" а вызов через интерфейс обязан пройти через таблицу методов.")
}