В Go-сервисе после добавления большого глобального массива указателей время каждого цикла GC выросло, хотя число живых объектов почти не изменилось. Какой механизм объясняет этот рост?
Большой глобальный массив указателей становится частью корней сборщика мусора. Во время каждого цикла GC рантайм должен просматривать его указательные слоты, поэтому растёт стоимость сканирования корней даже при почти неизменном объёме живых объектов.
Если элементы массива указывают на объекты, они также удерживают эти объекты достижимыми. Поэтому глобальная структура может одновременно увеличивать и время сканирования, и объём памяти, который GC обязан сохранять.
Tracing-сборщики мусора определяют достижимость объектов, начиная с набора корней: стеков горутин, глобальных переменных и других известных рантайму ссылок. Такой подход позволяет освобождать циклические структуры без ручного управления памятью.
В Go сборка мусора выполняет большую часть работы конкурентно с приложением. Однако поиск корней и их сканирование всё равно требуют обработки всех областей памяти, содержащих указатели, поэтому размер корневого набора непосредственно влияет на стоимость GC.
Глобальный массив часто выбирают как быстрый индекс, кэш или таблицу соответствий. Ошибка возникает, когда оценивают только количество реально используемых элементов и не учитывают, что тип массива содержит большое число указательных слотов.
Даже преимущественно пустой массив требует проверки указательных позиций. Если же в нём остаются ссылки на давно не нужные объекты, эти объекты не считаются недостижимыми и не могут быть освобождены.
Например, глобальное хранилище может выглядеть так:
Сам массив является глобальной областью памяти, а его тип сообщает рантайму, что элементы могут содержать указатели. Во время маркировки GC сканирует эту область, проверяет элементы и при ненулевых ссылках добавляет соответствующие объекты в граф достижимости.
Стоимость зависит не только от числа ненулевых ссылок. Пустые слоты обычно быстро проверяются, но их всё равно нужно охватить сканированием. Поэтому большой массив указателей может увеличивать CPU-затраты каждого GC-цикла даже при небольшом числе живых записей.
Это отличается от простого размера данных: глобальный массив байтов без указателей не требует поиска ссылок внутри каждого байта. Для GC важна карта указателей типа, а не только общий объём памяти.
Практические варианты оптимизации:
Замена указателя на целочисленный идентификатор требует отдельного хранилища объектов и дополнительных операций поиска. Простое преобразование указателя в uintptr не является безопасной заменой ссылки: такое значение не удерживает объект от сборки мусора.
В сервисе был глобальный индекс на миллион указателей, из которых обычно использовалось около десяти тысяч. После роста частоты GC профили показали увеличение времени маркировки, хотя объём живых объектов почти не изменился.
Рассматривались три варианта. Уменьшение массива снижало стоимость сканирования, но ограничивало диапазон ключей. Разбиение на несколько массивов помогало только при возможности не держать все части активными одновременно. Хранение записей в отдельном пуле по идентификаторам уменьшало число указательных слотов, но добавляло косвенный доступ.
Выбрали индекс по компактным идентификаторам и отдельное хранилище записей, потому что задержка поиска оставалась приемлемой, а большая часть пустых указательных слотов исчезла. Важно измерять результат профилировщиками GC и задержек: уменьшение времени сканирования может не компенсировать стоимость дополнительного поиска.
nil?Ответ: Да, потенциально увеличится стоимость сканирования корневой области, поскольку рантайму нужно обработать указательные слоты, хотя nil-значения не приводят к маркировке объектов. При этом такой массив не удерживает дополнительные объекты живыми, поэтому влияние на объём живой кучи будет отличаться от массива с ненулевыми ссылками.
Ответ: Удаление ссылки лишь делает объект потенциально недостижимым. Объект будет освобождён только во время последующего цикла GC и только если других путей достижимости нет: например, ссылок из стеков горутин, других глобальных структур или объектов, уже отмеченных как живые.
Ответ: Нет. Большое число живых элементов усиливает две проблемы: сканирование ссылок и удержание объектов. Но даже пустые или почти пустые указательные слоты могут увеличивать работу по сканированию корней, поэтому при оценке нужно учитывать размер всей структуры и частоту циклов GC, а не только заполненность массива.