ЗАМЕР
bench/gogenerics/runs/toolchain-go124.txt
Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.
- Цитируется в статье
- /ru/go/typing/generics
Запись прогона
У этого замера записи прогона нет — только скрипт.
Скрипт
96 строкgo1.24.7 linux/amd64 | Intel(R) Xeon(R) Processor @ 2.80GHz
7 чередующихся раундов по 1s; в таблицах диапазон нс/оп по раундам
перекрываются диапазоны — значит разницы нет; между блоками не сравнивать
1. ВЫЗОВ МЕТОДА У ТЫСЯЧИ ОБЪЕКТОВ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
напрямую, []*user 413-449 0 0 x1.00
дженерик, [T Namer] 1899-1980 0 0 x4.60
интерфейс, []Namer 2230-2308 0 0 x5.40
Дженерик не даёт скорости прямого вызова: все указатели делят одно
тело, оно не знает, чей метод звать, и адрес берётся из словаря.
Но и скорости интерфейса он не даёт — он между. Из чего складываются
обе разницы, сам этот блок сказать не может: смотреть блоки 6 и 7.
6. ТО ЖЕ, НО ВСТРАИВАНИЕ НА ПРЯМОМ ПУТИ ЗАПРЕЩЕНО
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
напрямую, //go:noinline 1905-1978 0 0 x1.00
дженерик, [T Namer] 1903-1978 0 0 x1.00
интерфейс, []Namer 2258-2330 0 0 x1.19
Тот же тип, тот же обход, та же работа. Изменилось одно: метод
//go:noinline, поэтому прямой вызов теперь делается по-настоящему, а
не растворяется в чтении поля. Осталась разница ровно в том, как
берётся адрес.
Контроль — строка интерфейса. Интерфейсный вызов и так не встраивался,
запрет у него ничего не отнимает, и его число должно совпасть с
блоком 1. Если не совпало — уехал весь прогон, и блок читать нельзя.
7. СКОЛЬКО ИЗ ОТСТАВАНИЯ ИНТЕРФЕЙСА — ВООБЩЕ НЕ ДИСПЕТЧЕРИЗАЦИЯ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
чтение поля через []*user 416-440 0 0 x1.00
чтение поля через []Namer 773-821 0 0 x1.86
Ни одна строка не зовёт метод: поле читается напрямую, у интерфейсной
стороны через утверждение типа. Различаются только данные — указатель
занимает слово, интерфейсное значение два, и на тысяче элементов это
8 КиБ против 16 КиБ.
Эту разницу надо вычесть из разницы блока 1, прежде чем называть
остаток ценой косвенного вызова. Верхняя строка здесь и верхняя
строка блока 1 меряют одно и то же — после вкладывания метод и есть
чтение поля; их совпадение — контроль повторяемости.
2. ЧТЕНИЕ ТЫСЯЧИ INT64 ИЗ ГОТОВОГО СРЕЗА
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, []int64 446-492 0 0 x1.00
дженерик, [T ~int64] 444-457 0 0 x1.00
через []any 776-810 0 0 x1.74
Упаковка в any здесь произошла при подготовке данных и в замер не
входит: меряется только чтение. Ограничение арифметическое, методов в
нём нет, и от дженерика в машинном коде не остаётся ничего.
3. ПОСТРОЕНИЕ СРЕЗА ИЗ ТЫСЯЧИ ЗНАЧЕНИЙ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, []int64 3517-5841 8192 1 x1.00
дженерик, []T 4154-5264 8192 1 x1.18
через []any 21902-23876 24384 1001 x6.23
Здесь упаковка входит в замер, и она видна не во времени, а в двух
правых столбцах. Настоящая цена, которую платят чаще всего, — не
дженерик, а то, чем дженерик заменяют.
4. СОРТИРОВКА ТОЙ ЖЕ ТЫСЯЧИ ЧИСЕЛ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
slices.Sort 35771-37108 0 0 x1.00
sort.Ints 35034-37311 0 0 x0.98
sort.Sort 81909-90193 24 1 x2.29
sort.Slice 76103-82288 56 2 x2.13
Оговорка, без которой вторая строка вводит в заблуждение: с Go 1.22
sort.Ints — это буквально func Ints(x []int) { slices.Sort(x) }. Две
верхние строки зовут одну функцию, и совпадение их чисел — проверка
повторяемости замера, а не победа дженерика. Сравнивать надо с двумя
нижними: это тот код, который писали до дженериков.
5. ОДНО СРАВНЕНИЕ ДВУХ ЧИСЕЛ
----------------------------------------------------------------------
нс/оп Б/оп выдел. кратно
руками, int 0.6854-0.7905 0 0 x1.00
дженерик, [T ordered] 0.6946-0.7711 0 0 x1.01
встроенный min 0.6911-0.7489 0 0 x1.01
через any 0.9651-1.043 0 0 x1.41
Четвёртая строка НЕ показывает цену упаковки в any: minIface
встраивается, упакованное значение никуда не убегает, и анализ
убегания оставляет его на стеке. Цена упаковки видна в блоке 3.