Deep Engineering

MEASUREMENT

bench/gogenerics/runs/toolchain-go124.txt

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/go/typing/generics

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

This measurement has no recorded run — only the script.

Script

96 lines
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.