В Go все привыкли писать set вот так:
type set[K comparable] struct {
data map[K]struct{}
}
Ибо многие знают, что в Go пустая структура не занимает память. Давайте посмотрим на формулировку из стандарта:
A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero.
поэтому, используя ее как значение в мапе, мы оптимизируем потребление памяти, ведь в реализации множества нам от мапы нужны только ключи.
😢😢 Но в Go 1.24 это сломали
Как было до Go 1.24?
Старая мапа была основана на бакетах, которые хранили отдельно список ключей и отдельно список значений. Грубо говоря, бакет можно было представить такой структурой:
type bmap struct {
tophash [8]uint8
keys [8]K
values [8]V
overflow *bmap
}
Массив пустых структур, как и сама пустая структура, не занимает память, значит, используя ее в качестве значения, мы выигрываем по памяти.
Что случилось в Go 1.24?
С переходом мапы на Swiss Tables структура бакетов поменялась. Теперь ключи и значения хранятся в специальных группах, но уже не в отдельных массивах, а в одной структуре:
type group struct {
ctrl uint64
slots [abi.SwissMapGroupSlots]struct {
key keyType
elem elemType
}
}
Что теперь произойдет, если использовать пустую структуру в качестве значения?
Дело в том, что, если
struct{} стоит последним полем в структуре, происходят неприятные последствия.Хотя сама пустая структура имеет нулевой размер, компилятор не может позволить указателю на последнее поле указывать за пределы объекта, поэтому он добавляет хвостовой байт (паддинг). А потом этот хвост еще может раздуться из-за выравнивания всей структуры (компилятор выравнивает структуры для большинства архитектур). То есть, теперь это поле с тем самым выравниваем будет занимать 8 байт, и наша оптимизация перестанет работать.
Как вы думаете, можно ли как-то решить эту проблему? Пишите свои варианты в комментариях.