Как работает программа: где лежат переменные, что происходит при вызове функции, почему рекурсия падает, как устроен malloc и почему в Go, Java и Python одно и то же значение может стоить ноль наносекунд или сотню.
Листайте стрелками ← →, свайпом или кнопками внизу. В схемах нажимайте кнопки и двигайте ползунки.
Каждый процесс видит одну и ту же картину: внизу код и глобальные данные из исполняемого файла, над ними растёт куча, наверху — стек главного потока, между ними — библиотеки и большие блоки через mmap.
Нажмите на строку программы или на область карты. Адреса настоящие по форме, но с ASLR меняются при каждом запуске. Свою карту видно в cat /proc/<pid>/maps. Почему адреса виртуальные — в презентации про виртуальную память.
Что сделает эта программа на Linux, собранная обычным gcc?
int main(void) {
char *s = "hello";
s[0] = 'H';
puts(s);
}
"hello" лежит в .rodata, а эта область отображена только на чтение (r--p в /proc/pid/maps). Запись в неё — аппаратное исключение и сигнал SIGSEGV. Компилятор пропускает код, потому что тип литерала в C — char[], а не const char[]. Нужна изменяемая копия — пишите char s[] = "hello";: тогда массив окажется на стеке.rsp уменьшается при вызове и возвращается при выходе. Освободить кадр — значит просто сдвинуть указатель.Нажимайте «Шаг»: вызовы sum_sq и рекурсивный fact. Обратите внимание, что второй вызов sq занимает те же адреса, что и первый.
rdi, rsi, rdx, rcx, r8, r9, вещественные — в xmm0–7, результат — в rax. В стек идут только остальные. На ARM64 — x0–x7.call кладёт адрес возврата, ret снимает его и прыгает туда.sub rsp, N. Вот почему локальные переменные «бесплатны»: их цена не зависит от количества.Пройдите по инструкциям и следите за rsp, rbp и ячейками стека. Свой код так же смотрят в objdump -d или на godbolt.org.
StackOverflowError, RangeError. Python считает не байты, а кадры: sys.getrecursionlimit() = 1000.panic, её не поймать через recover.Выберите рантайм и двигайте глубину рекурсии. Размер кадра зависит от числа локальных переменных — увеличьте его.
ulimit -s), в JVM — 1 МБ (-Xss). Это резерв адресов: физическая память выделяется только под затронутые страницы.Java-сервис держит 2000 потоков с -Xss1m. Большинство из них ждут на сокете, глубина стека — пара десятков кадров. Сколько физической памяти занимают их стеки?
-Xss резервирует адреса, а физическая страница появляется только при первом обращении. Неглубокий стек затрагивает несколько страниц по 4 КБ — 2000 потоков дадут порядка 20–60 МБ RSS. Виртуальная память (VSZ в top) при этом покажет 2 ГБ, и это не повод для паники. Смотрите RSS или -XX:NativeMemoryTracking=summary.После выхода из функции её кадр никто не стирает: байты остаются на месте, пока следующий вызов не займёт те же адреса. Указатель на локальную переменную продолжает «работать» — до поры.
Пройдите программу по шагам в C, а потом в Go. Современный GCC с -O2 вообще подменяет такой адрес на NULL и выдаёт предупреждение. Rust подобный код не скомпилирует.
ret уходит по мусорному адресу.-fstack-protector-strong) — случайное значение перед адресом возврата, проверяется при выходе; NX — стек нельзя исполнять; ASLR — адреса случайны при каждом запуске; теневой стек (Intel CET) хранит копию адресов возврата.panic, а не порча памяти.rsp, одна инструкция на всю функциюничего не ищет, не блокирует, не фрагментируетсяmalloc ищет подходящий блок: 20–100 нс в быстром пути, микросекунды, если нужен системный вызовfree), подсчёт ссылок или сборщик мусораmallocbrk или mmap. Аллокатор режет его на блоки, у каждого — заголовок с размером и флагом «занят».free возвращает блок в список и склеивает его со свободными соседями. Без склейки память дробится на крошки.Выделяйте и освобождайте блоки. Потом нажмите «Сценарий»: много мелких блоков, каждый второй освобождён — свободно половина кучи, а 200 байт уже не выделить.
mcache на каждом P, в glibc — tcache.mmap — это системный вызов и обнуление страниц.Двигайте размер запроса и смотрите, куда он попадёт и сколько байт пропадёт на округлении.
Массив значений — один непрерывный кусок памяти. Массив ссылок — массив указателей плюс отдельный объект в куче на каждый элемент: больше выделений, заголовки объектов, прыжки по памяти при обходе.
Сравните миллион точек (x, y) в разных языках. В Java объекты с заголовком 12 байт, в Python каждое число — отдельный объект. Для чисел берите array/NumPy, в JS — Float64Array.
new или &T{}.go build -gcflags=-m.Угадайте для каждого фрагмента, где окажется значение.
make([]T, 0, n), new ArrayList<>(n), strings.Builder.Grow.Двигайте число элементов и включите предварительное выделение.
Программа на C выделила 2 ГБ объектов по 100 байт, а потом освободила 95% из них вперемешку. Что покажет RSS процесса в top?
malloc_trim, jemalloc и mimalloc, которые активнее возвращают память, и арены или пулы для объектов с общим временем жизни. Языки со сборкой мусора решают это уплотнением — об этом в презентации про управление памятью.buf = buf[:0], sync.Pool, пулы объектов.[]T вместо []*T, массивы примитивов вместо коллекций обёрток (int[], а не List<Integer>).interface{} (и fmt.Sprintf) часто означает выделение, в Java — автоупаковка int → Integer.__slots__ убирает словарь у каждого объекта, генераторы не создают промежуточных списков, числа держите в NumPy.// было: новый буфер на каждый запрос
func handle(w io.Writer, r *Req) {
buf := make([]byte, 0, 4096) // уходит в кучу
buf = r.AppendTo(buf)
w.Write(buf)
}
// стало: буферы переиспользуются
var bufPool = sync.Pool{New: func() any {
b := make([]byte, 0, 4096)
return &b
}}
func handle(w io.Writer, r *Req) {
bp := bufPool.Get().(*[]byte)
buf := r.AppendTo((*bp)[:0])
w.Write(buf)
*bp = buf
bufPool.Put(bp)
}
/proc/<pid>/maps, pmap -x; размер стека — ulimit -s.-gcflags=-m, go test -benchmem (allocs/op), профиль alloc_space в pprof.tracemalloc — какие строки кода выделяют больше всего.node --inspect).jdk.ObjectAllocationSample), async-profiler -e alloc.-fsanitize=address ловит выход за границы и висячие указатели, heaptrack и valgrind показывают выделения.$ go build -gcflags=-m ./...
./user.go:12:9: &User{...} escapes to heap
./user.go:20:13: ... argument does not escape
$ go test -bench=Parse -benchmem
BenchmarkParse-8 1854213 642 ns/op
320 B/op 6 allocs/op
$ go test -bench=Parse -memprofile=mem.out
$ go tool pprof -sample_index=alloc_space mem.out
import tracemalloc
tracemalloc.start()
run()
snap = tracemalloc.take_snapshot()
for s in snap.statistics("lineno")[:5]:
print(s) # файл:строка, байты, число блоков
Самое быстрое выделение памяти — то, которого не было.