Deep Engineering

ЗАМЕР

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.