Зачастую в памяти программы хранятся структуры данных, которые изменяют свой размер динамически, по ходу работы программы. Примером такой структуры может быть кэш данных или журнал работы программы или данные, получаемые от внешних систем. При этом может возникнуть ситуация, когда потребление памяти растёт, возможностей оборудования не хватает, а конкретный механизм утечки не ясен.
Основным способом профилирования Go-приложений является подключение инструмента pprof из пакета «net/http/pprof». В результате можно получить таблицу или граф с распределением памяти в работающей программе. Но использование этого инструмента требует очень больших накладных расходов и может быть неприменимо, особенно если вы не можете запустить несколько экземпляров программы с реальными данными.
В таком случае возникает желание измерить потребление памяти объектами программы по запросу, чтобы, например, отобразить статистику системы или передать метрики в систему мониторинга. Однако средствами языка это в общем случае невозможно. В Go нет инструментов для определения размера переменных во время работы программы.
Поэтому я решил написать небольшой пакет, который предоставляет такую возможность. Основным инструментом является рефлексия (пакет «reflection»). Всех интересующихся вопросом такого профилирования приложения приглашаю к дальнейшему чтению.
Сначала нужно сказать пару слов по поводу встроенных функций
unsafe.Sizeof(value)
и
reflect.TypeOf(value).Size()
Эти функции эквивалентны и зачастую в Интернете именно их рекомендуют для определения размера переменных. Но эти функции возвращают не размер фактической переменной, а размер в байтах для контейнера переменной (грубо — размер указателя). К примеру, для переменной типа int64 эти функции вернут корректный результат, поскольку переменная данного типа содержит фактическое значение, а не ссылку на него. Но для типов данных, содержащих в себе указатель на фактическое значение, вроде слайса или строки, эти функции вернут одинаковое для всех переменных данного типа значение. Это значение соответствует размеру контейнера, содержащего ссылку на данные переменной. Проиллюстрирую примером:
func main() {
s1 := "ABC"
s2 := "ABCDEF"
arr1 := []int{1, 2}
arr2 := []int{1, 2, 3, 4, 5, 6}
fmt.Printf("Var: %s, Size: %v\n", s1, unsafe.Sizeof(s1))
fmt.Printf("Var: %s, Size: %v\n", s2, unsafe.Sizeof(s2))
fmt.Printf("Var: %v, Size: %v\n", arr1, reflect.TypeOf(arr1).Size())
fmt.Printf("Var: %v, Size: %v\n", arr2, reflect.TypeOf(arr2).Size())
}
В результате получим:
Var: ABC, Size: 16
Var: ABCDEF, Size: 16
Var: [1 2], Size: 24
Var: [1 2 3 4 5 6], Size: 24
Как видите, фактический размер переменной не вычисляется.
В стандартной библиотеке есть функция binary.Size() которая возвращает размер переменной в байтах, но только для типов фиксированного размера. То есть если в полях вашей структуры встретится строка, слайс, ассоциативный массив или просто int, то функция не применима. Однако именно эту функция я взял за основу пакета size, в котором попытался расширить возможности приведённого выше механизма на типы данных без фиксированного размера.
Для определения размера объекта во время работы программы необходимо понять его тип, вместе с типами всех вложенных объектов, если это структура. Итоговая структура, которую необходимо анализировать, в общем случае представляется в виде дерева. Поэтому для определения размера сложных типов данных нужно использовать рекурсию.
Таким образом вычисление объёма потребляемой памяти для произвольного объекта представляется следующим образом:
- алгоритм определение размера переменной простого (не составного) типа;
- рекурсивный вызов алгоритма для элементов массивов, полей структур, ключей и значений ассоциативных массивов;
- определение бесконечных циклов;
Чтобы определить фактический размер переменной простого типа (не массива или структуры), можно использовать приведённую выше функцию Size() из пакета «reflection». Эта функция корректно работает для переменных, содержащих фактическое значение. Для переменных, являющихся массивами, строками, т.е. содержащих ссылки на значение нужно пройтись по элементам или полям и вычислить значение каждого элемента.
Для анализа типа и значения переменной пакет «reflection» упаковывает переменную в пустой интерфейс (interface{}). В Go пустой интерфейс может содержать любой объект. Кроме того, интерфейс в Go представлен контейнером, содержащим два поля: тип фактического значения и ссылку на фактическое значение.
Именно отображение анализируемого значения в пустой интерфейс и обратно послужило основанием для названия самого приёма — reflection.
Для лучшего понимания работы рефлексии в Go рекомендую статью Роба Пайка в официальном блоге Go. Перевод этой статьи был на Хабре.
В конечном итоге был разработан пакет size, который можно использовать в своих программах следующим образом:
package main
import (
"fmt"
"github.com/DmitriyVTitov/size"
)
func main() {
a := struct {
a int
b string
c bool
d int32
e []byte
f [3]int64
}{
a: 10, // 8 bytes
b: "Text", // 4 bytes
c: true, // 1 byte
d: 25, // 4 bytes
e: []byte{'c', 'd', 'e'}, // 3 bytes
f: [3]int64{1, 2, 3}, // 24 bytes
}
fmt.Println(size.Of(a))
}
// Output: 44
Замечания:
- На практике вычисление размера структур объёма около 10 ГБайт с большой вложенностью занимает 10-20 минут. Это результат того, что рефлексия — довольно дорогая операция, требующая упаковки каждой переменной в пустой интерфейс и последующий анализ (см. статью по ссылке выше).
- В результате сравнительно невысокой скорости, пакет следует использовать для примерного определения размера переменных, поскольку в реальной системе за время анализа большой структуры фактические данные наверняка успеют измениться. Либо обеспечивайте исключительный доступ к данным на время расчёта с помощью мьютекса, если это допустимо.
- Программа не учитывает размер «контейнеров» для массивов, интефейсов и ассоциативных массивов (это 24 байта для массива и слайса, 8 байт для map и interface). Поэтому, если у вас большое количество таких элементов небольшого размера, то потери будут существенными.
grSereger
Кажется, слайс байт из 3х элементов должен занимать 27 байт — 24 (под 64 битную ос) под структуру слайса и 3 под данные. Кстати, считается емкость или занятые данные?
И, кажется, вы не учитываете кейс с циклическими ссылками.
DmitriyTitov Автор
1. Циклические ссылки учитываю: веду список посещённых ссылок:
2. Вы правы. Однако я в примечании писал, что размеры контейнеров отбрасываются. Иначе не представляю как обрабатывать вложенность в рекурсии. Поэтому вычисления приблизительные, предназначенные для определения порядка потребляемой памяти для больших объектов.
grSereger
Простите, не обратил на кэш внимание.
И, имхо, правильнее считать именно по капасити, а не по длинне слайсы и массивы
DmitriyTitov Автор
Полностью согласен, для слайсов и массивов так вернее. Надо только проверить что будет с обработкой элементов на «пустых» позициях. Ведь это может быть как массив из простых типов, где нужно умножить size.Of(element) * array.Cap() так и массив каких-нибудь больших структур, где len() и cap() дадут примерно похожий результат, поскольку отсутствующие элементы будут пустыми указателями.
Обязательно поправлю, как только придумаю хорошие тестовые примеры.