ЗАМЕР
bench/gogenerics/runs/cost.txt
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/go/typing/generics
Запись прогона
У этого замера записи прогона нет — только скрипт.
Скрипт
96 строкgo1.24.7 linux/amd64 | Intel(R) Xeon(R) Processor @ 2.10GHz
7 чередующихся раундов по 1s; в таблицах диапазон нс/оп по раундам
перекрываются диапазоны — значит разницы нет; между блоками не сравнивать
1. ВЫЗОВ МЕТОДА У ТЫСЯЧИ ОБЪЕКТОВ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
напрямую, []*user 321-328 0 0 x1.00
дженерик, [T Namer] 1342-1462 0 0 x4.18
интерфейс, []Namer 2185-2243 0 0 x6.81
Дженерик не даёт скорости прямого вызова: все указатели делят одно
тело, оно не знает, чей метод звать, и адрес берётся из словаря.
Но и скорости интерфейса он не даёт — он между. Из чего складываются
обе разницы, сам этот блок сказать не может: смотреть блоки 6 и 7.
6. ТО ЖЕ, НО ВСТРАИВАНИЕ НА ПРЯМОМ ПУТИ ЗАПРЕЩЕНО
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
напрямую, //go:noinline 1143-1198 0 0 x1.00
дженерик, [T Namer] 1585-1629 0 0 x1.39
интерфейс, []Namer 2198-2253 0 0 x1.92
Тот же тип, тот же обход, та же работа. Изменилось одно: метод
//go:noinline, поэтому прямой вызов теперь делается по-настоящему, а
не растворяется в чтении поля. Осталась разница ровно в том, как
берётся адрес.
Контроль — строка интерфейса. Интерфейсный вызов и так не встраивался,
запрет у него ничего не отнимает, и его число должно совпасть с
блоком 1. Если не совпало — уехал весь прогон, и блок читать нельзя.
7. СКОЛЬКО ИЗ ОТСТАВАНИЯ ИНТЕРФЕЙСА — ВООБЩЕ НЕ ДИСПЕТЧЕРИЗАЦИЯ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
чтение поля через []*user 317-348 0 0 x1.00
чтение поля через []Namer 539-564 0 0 x1.70
Ни одна строка не зовёт метод: поле читается напрямую, у интерфейсной
стороны через утверждение типа. Различаются только данные — указатель
занимает слово, интерфейсное значение два, и на тысяче элементов это
8 КиБ против 16 КиБ.
Эту разницу надо вычесть из разницы блока 1, прежде чем называть
остаток ценой косвенного вызова. Верхняя строка здесь и верхняя
строка блока 1 меряют одно и то же — после вкладывания метод и есть
чтение поля; их совпадение — контроль повторяемости.
2. ЧТЕНИЕ ТЫСЯЧИ INT64 ИЗ ГОТОВОГО СРЕЗА
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, []int64 307-312 0 0 x1.00
дженерик, [T ~int64] 308-319 0 0 x1.00
через []any 541-572 0 0 x1.76
Упаковка в any здесь произошла при подготовке данных и в замер не
входит: меряется только чтение. Ограничение арифметическое, методов в
нём нет, и от дженерика в машинном коде не остаётся ничего.
3. ПОСТРОЕНИЕ СРЕЗА ИЗ ТЫСЯЧИ ЗНАЧЕНИЙ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, []int64 3303-3598 8192 1 x1.00
дженерик, []T 3290-3557 8192 1 x1.00
через []any 16114-17002 24384 1001 x4.88
Здесь упаковка входит в замер, и она видна не во времени, а в двух
правых столбцах. Настоящая цена, которую платят чаще всего, — не
дженерик, а то, чем дженерик заменяют.
4. СОРТИРОВКА ТОЙ ЖЕ ТЫСЯЧИ ЧИСЕЛ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
slices.Sort 12804-13565 0 0 x1.00
sort.Ints 12814-13259 0 0 x1.00
sort.Sort 62112-63593 24 1 x4.85
sort.Slice 50244-55476 56 2 x3.92
Оговорка, без которой вторая строка вводит в заблуждение: с Go 1.22
sort.Ints — это буквально func Ints(x []int) { slices.Sort(x) }. Две
верхние строки зовут одну функцию, и совпадение их чисел — проверка
повторяемости замера, а не победа дженерика. Сравнивать надо с двумя
нижними: это тот код, который писали до дженериков.
5. ОДНО СРАВНЕНИЕ ДВУХ ЧИСЕЛ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, int 0.4426-0.4694 0 0 x1.00
дженерик, [T ordered] 0.4523-0.4941 0 0 x1.02
встроенный min 0.4363-0.4672 0 0 x0.99
через any 0.9188-0.982 0 0 x2.08
Четвёртая строка НЕ показывает цену упаковки в any: minIface
встраивается, упакованное значение никуда не убегает, и анализ
убегания оставляет его на стеке. Цена упаковки видна в блоке 3.