Всім привіт, шановні колеги! Якийсь час тому я став цікавитись Rust та його підходами. Ну знаєте, ці borrow checker’и, що виносять мізки, lifetimes і всяке таке. Цікаво, але складно для сприйняття. А чи є в Go щось схоже? І я таки знайшов. Мертвонароджене дитя 🥺
Go відомий своєю ефективною автоматичною збіркою сміття (Garbage Collection, GC), яка значно спрощує керування пам'яттю для розробників. Однак, для певних сценаріїв високопродуктивних обчислень, де виділяється та звільняється велика кількість короткоживучих об'єктів, накладні витрати на роботу GC можуть стати ботлнеком. Саме з метою оптимізації таких випадків інженери та спільнота Go досліджували альтернативні підходи, одним з яких були так звані арени пам'яті (memory arenas), що з’явились в Go 1.20.
Арена пам'яті (memory arenas) — це концепція керування пам'яттю, де замість виділення та звільнення кожного об'єкта індивідуально, виділяється великий, попередньо визначений блок пам'яті (сама арена). Об'єкти, які потрібні для виконання певного завдання або протягом обмеженого часу, розміщуються послідовно всередині цієї арени. Коли всі об'єкти в арені більше не потрібні, вся арена звільняється одним махом.
Власне, в цьому і полягає їх основна ідея - замість того, щоб GC відстежував і збирав кожен дрібний об'єкт окремо, ми групуємо пов'язані об'єкти в одному блоці і звільняємо цей блок цілком.
Використання арен потенційно могло б надати кілька значних переваг:
Однак, концепція арен не позбавлена своїх недоліків, особливо в контексті Go:
Теоретично, арени могли б бути корисними у таких сценаріях:
<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>
Програмісти, що пишуть на Rust можуть помітити певну концептуальну схожість між ідеєю арен та життєвими циклами (lifetimes).