В этой статье мы обсудим универсальные реализации Golang.
Генераторы Golang, вероятно, были самой востребованной функцией в сообществе Golang с версии 1.0, когда они объявили, что собираются добавить дженерики в Go, начались дискуссии о реализации дженериков, мы обсудим две самые популярные реализации и, наконец, текущую реализацию Go1.18.
Трафарет
Использование трафарета — это один из подходов, которые мы можем использовать при реализации дженериков в компиляторе, в этом подходе, если у нас есть универсальная функция, такая как
func max[T comparable](a, b T) bool { if a > b { return a } else { return b } }
для каждого вызова в нашей кодовой базе приходил компилятор и генерировал новый экземпляр этой функции только для этого вызова, например:
max[int](1,2) // this will generate something like max_int_1 max[float32)(1.0, 2.0) // this will generate something like max_float32_1 max[int](3,4) // this will also generate a new max_int_2
Как вы можете видеть, в этом подходе мы должны генерировать экземпляр функции для каждого вызова, и даже когда экземпляры одинаковы или похожи, у нас есть дубликаты.
Обратите внимание, что использование трафаретов обеспечивает наилучшую производительность во время выполнения, но требует длительного времени компиляции и больших двоичных файлов. С++ использует этот подход.
Словари
использование словарей — еще один подход к использованию дженериков в компиляторе. Словари — это полная противоположность Stenciling
, так как они будут иметь единственный экземпляр функции, и вся необходимая информация о параметрах типа передается в структуру словаря этой функции.
func max[T comparable](a, b T) bool { if a > b { return a } else { return b } }
рассмотрите приведенный выше код как общую максимальную функцию.
max[int](1,2) max[float32)(1.0, 2.0) max[int](3,4)
все вышеперечисленные вызовы относятся к одному и тому же экземпляру функции max
, и различаются только данные словаря, например, для первого вызова словарь содержит тип int
и некоторую другую информацию о том, как внутренние функции, такие как make/len/new/etc...
, должны работать для целых чисел.
Этот подход имеет самую низкую производительность во время выполнения, поскольку для каждого универсального вызова необходимо создать и заполнить этот словарь, а функциям также необходимо обращаться к этому словарю для большинства своих операций. Этот подход генерирует меньше ассемблерного кода и меньшие двоичные файлы, но код компилятора намного сложнее, чем Stenciling
.
Перейти к реализации
Реализация Golang в основном представляет собой золотую середину между двумя вышеуказанными методами. Генераторы Go знакомят нас с концепцией под названием GCShape
, которая в основном представляет собой группу схожих типов, типов, имеющих один и тот же базовый тип, или типов, являющихся типами-указателями, которые находятся в одной группе GCShape, например:
type a int type b int type c = int // above are same GCShape
помните, что типы, которые находятся в одном и том же GCShape, должны иметь абсолютно одинаковое поведение для всех внутренних операций компилятора, например, для оператора +
они должны вести себя одинаково, поэтому даже int16
и int32
не находятся в одном и том же GCShape. Компилятор Go создает тип GCShape, когда видит параметр типа, который не принадлежит ни одному GCShape, поэтому, например, для string
он создаст go.shape.string_0
, обратите внимание, что 0 означает, что это первый параметр типа.
Компилятор Go выполняет трафарет для каждого GCShape вместо каждого универсального вызова, а затем на каждом сайте вызова создает словарь, содержащий информацию о типе для этого конкретного типа, а затем вызывает функцию, поэтому, когда типы находятся в одном и том же GCShape
, они используют один и тот же экземпляр функции.
поэтому для той же функции max
у нас было
max[int](1,2) max[int](3,4)
вышеприведенные вызовы функций совместно используют один и тот же экземпляр функции. вызов функции ниже также использует тот же экземпляр, поскольку базовый тип — int, и мы можем выполнять все внутренние операции компилятора так же, как и сам int, обратите внимание, что словарь, созданный для вызовов выше, отличается от словаря, созданного для вызова ниже, поскольку они разные типов, но поскольку они находятся в одном GCShape, они могут совместно использовать один и тот же экземпляр универсальной функции.
type a int max[a](1,2)
Вы также можете увидеть процесс реализации Golang на картинке ниже.
Как видите, реализация дженериков в Go1.18 — лучшее из обоих миров, она быстро компилируется и не создает толстых двоичных файлов. У него есть ограничения, как было объявлено в примечаниях к выпуску Go1.18.
Обобщения — самая большая функция и изменение языка Go. С каждым днем мы должны видеть все больше и больше случаев использования дженериков, поскольку они делают возможным код, который было невозможно написать.