Всім привіт, шановні колеги! Якийсь час тому я став цікавитись Rust та його підходами. Ну знаєте, ці borrow checker’и, що виносять мізки, lifetimes і всяке таке. Цікаво, але складно для сприйняття. А чи є в Go щось схоже? І я таки знайшов. Мертвонароджене дитя 🥺

Вступ

Go відомий своєю ефективною автоматичною збіркою сміття (Garbage Collection, GC), яка значно спрощує керування пам'яттю для розробників. Однак, для певних сценаріїв високопродуктивних обчислень, де виділяється та звільняється велика кількість короткоживучих об'єктів, накладні витрати на роботу GC можуть стати ботлнеком. Саме з метою оптимізації таких випадків інженери та спільнота Go досліджували альтернативні підходи, одним з яких були так звані арени пам'яті (memory arenas), що з’явились в Go 1.20.

Що це таке?

Арена пам'яті (memory arenas) — це концепція керування пам'яттю, де замість виділення та звільнення кожного об'єкта індивідуально, виділяється великий, попередньо визначений блок пам'яті (сама арена). Об'єкти, які потрібні для виконання певного завдання або протягом обмеженого часу, розміщуються послідовно всередині цієї арени. Коли всі об'єкти в арені більше не потрібні, вся арена звільняється одним махом.

Власне, в цьому і полягає їх основна ідея - замість того, щоб GC відстежував і збирав кожен дрібний об'єкт окремо, ми групуємо пов'язані об'єкти в одному блоці і звільняємо цей блок цілком.

Переваги

Використання арен потенційно могло б надати кілька значних переваг:

  1. Зменшення навантаження на GC: замість того, щоб GC сканував та обробляв безліч дрібних об'єктів, йому потрібно лише керувати більшими блоками арен. Це могло б зменшити паузи, пов'язані зі збиранням сміття.
  2. Швидша алокація: розміщення об'єктів в арені зазвичай зводиться до простого зсуву вказівника в межах попередньо виділеного блоку, що є дуже швидкою операцією порівняно з пошуком вільного місця в загальній купі.
  3. Потенційна локальність кешу: оскільки пов'язані об'єкти розміщуються поруч в пам'яті (в межах однієї арени), це може покращити локальність кешу процесора, що призводить до прискорення доступу до даних.
  4. Швидке звільнення: звільнення всіх об'єктів в арені відбувається миттєво при звільненні самої арени, незалежно від кількості об'єктів всередині.

Недоліки

Однак, концепція арен не позбавлена своїх недоліків, особливо в контексті Go:

  1. Ручне керування: ви, як розробник, несете відповідальність за вибір правильного розміру арени та за звільнення арени, коли вона більше не потрібна. Це додає складності та є відступом від парадигми автоматичного GC.
  2. Ризик витоків пам'яті: якщо арена не буде звільнена належним чином, або якщо об'єкти, які повинні були жити лише в межах арени, "втечуть" з неї (тобто на них з'являться посилання з-за меж арени, які переживуть саму арену), це призведе до витоків пам'яті (memory leaks) або некоректної роботи програми.
  3. Складність інтеграції: інтеграція арен з існуючим збирачем сміття Go та його моделлю керування пам'яттю є нетривіальною задачею. GC повинен розуміти арени та не намагатися збирати об'єкти, які керуються ареною.

Приклади потенційного використання

Теоретично, арени могли б бути корисними у таких сценаріях:

<aside> ⚠️

Як ви вже, напевно, здогадались, це експериментальний пакет. Його використання в продакшені абсолютно НЕ РЕКОМЕНДУЄТЬСЯ!

</aside>

Для експериментів, використати арени можна, передавши змінну GOEXPERIMENT=arenas:

GOEXPERIMENT=arenas go run main.go

Давайте розглянемо декілька прикладів:

package main

import (
	"fmt"
	"arena"
)

// Визначимо просту структуру для прикладу
type Data struct {
	Value int
	Label string
}

func main() {
	// створення арени
	// arena.NewArena() створює новий блок пам'яті, який буде нашою ареною.
	a := arena.NewArena()

	// відкладене звільнення арени
	// використання defer a.Free() гарантує, що арена буде звільнена,
	// коли функція main завершить свою роботу. Це імітує звільнення
	// всіх об'єктів в арені одним махом після закінчення їхнього життєвого циклу.
	defer a.Free()

	// виділення об'єктів в арені
	// arena.New[T](a) виділяє пам'ять для одного об'єкта типу T в арені 'a'
	// arena.Make[T](a, len, cap) виділяє пам'ять для зрізу (slice) типу T
	// з заданими довжиною та ємністю в арені 'a'

	// виділяємо структуру Data в арені
	da := arena.New[Data](a)
	da.Value = 100
	da.Label = "first"

	// виділяємо зріз Data в арені
	// це буде зріз довжиною 5, ємністю 10, пам'ять для якого виділена в арені
	dataSlice := arena.MakeSlice[Data](a, 5, 10)

	// виконання "обчислень" з об'єктами з арени
	// заповнюємо зріз та працюємо з об'єктами
	sumOfValues := da.Value
	for i := range dataSlice {
		dataSlice[i].Value = i * 10
		dataSlice[i].Label = fmt.Sprintf("Data %d", i)
		sumOfValues += dataSlice[i].Value
		
		// виведемо адреси в межах арени
		fmt.Printf("об'єкт в зрізі [%d]: %+v (адреса: %p)\\n", i, &dataSlice[i], &dataSlice[i])
	}

	// завершення роботи з об'єктами з арени
	// тут логічно закінчується "життєвий цикл" об'єктів в арені.
	// далі спрацює defer a.Free()

	fmt.Println("завершення функції main, арена буде звільнена")

	// після того, як a.Free() спрацює, пам'ять, яку займали da та dataSlice,
	// буде повернута системі/runtime Go
	// доступ до цих об'єктів після звільнення арени є небезпечним і може призвести
	// до помилок "use after free" в мовах з ручним керуванням пам'яттю.
	// в Go runtime, що підтримує арени, це означає, що пам'ять звільнена
	// і не повинна використовуватись!
	// спроба доступу до значень (не вказівників) може тимчасово спрацювати,
	// але вказівники стають недійсними
}

<aside> ⚠️

Мапи та nil-ові арени не підтримуються.

</aside>

Схожість з Lifetimes в Rust?

Програмісти, що пишуть на Rust можуть помітити певну концептуальну схожість між ідеєю арен та життєвими циклами (lifetimes).