Менеджер пам'яті в рубі: чекай підступу

У рубі реалізовано автоматичне управління пам'яттю. У більшості випадків це добре. Але, на жаль, іноді це буває боляче.

Менеджер пам'яті рубі одночасно і елегантний, і не без дивацтв, в будь-яку хвилину готових обернутися неприємним сюрпризом. Рубі зберігає об'єкти (
RVALUE
), в так званій купі (це не heap в класичному розумінні c-програмістів, у рубі своя купа). На низькому рівні,
RVALUE
— це звичайна
c
struct, що складається з області «атрибутів» і власне області «даних». Докладна розповідь про те, як всередині зберігаються об'єкти виходить за рамки даної замітки; достатньо просто розуміти, що кожному об'єкту в рубі відповідає такий ось
RVALUE
. Збирач сміття вважає посилання на нього, і очищає слот, коли лічильник обнуляється. У суто академічних цілях скажу, що розмір кожного такого слоти залежить від архітектури:

/*
* sizeof(RVALUE) is
* 20 if 32-bit, double is 4-byte aligned
* 24 if 32-bit, double is 8-byte aligned
* 40 if 64-bit
*/


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



У більшості випадків, коли ми маємо справу із звичайним об'єктом (скажімо, екземпляром класу
User
) — все відбувається як у всіх інших мовах. Лічильник обнулився, пам'ять з системної купи повернулася (за допомогою стандартного виклику
free
). Але якщо розміщується об'єкт поміщається в слот, ніяка додаткова пам'ять виділена не буде. Рубі просто запише об'єкт в слот. Ось як ця різниця виглядає в синтаксисі псевдо-сі:

char *s = malloc(42); // об'єкт, не помістився в слот
char s[5]; // об'єкт, що помістився в слот


Іноді слоти закінчуються, і тоді рубай захоплює системну пам'ять під додаткові слоти. І ось тут-то нас і чекає підкрадається непомітно топінамбур. Пам'ять, виділена під слоти, системі більше не повертається. Зовсім. Такі справи.

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

def report
puts 'Memory ' + `ps ax-o pid,rss | grep-E "^[[:space:]]*#{$$}"`
.strip.split.map(&:to_i)[1].to_s + 'KB'
end

report
big_var = " " * 10_000_000
report
big_var = nil
report
ObjectSpace.garbage_collect
sleep 1
report

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 11788KB


Тут ми хапнули пам'яті під здоровенну рядок, покористувалися нею і повернули пам'яті операційній системі. Все відмінно. Давайте тепер спробуємо трохи модифікувати код:

big_var = " " * 10_000_000
+ big_var = 1_000_000.times.map(&:to_s)


Ерундовое зміна, адже правда? — Ні, неправда.

# ⇒ Memory 11788KB
# ⇒ Memory 65188KB
# ⇒ Memory 65188KB
# ⇒ Memory 57448KB


Якого біса? Пам'ять системі не повертається (так, цей процес тепер буде жити з майже шістдесятьма метрами оперативки до самої смерті, навіть якщо взагалі нічого не робити). Насправді, все логічно. Кожне з чисел від одного до мільйона поміщається в слот. А слотів за замовчуванням (приблизно, ось тут докладніше):

RUBY_HEAP_MIN_SLOTS=800000


Так і ми, все-таки, не єдині користувачі пам'яті. Насправді, це все не так страшно. Для переважної більшості завдань слотів вистачить, особливо враховуючи, що вони повертаються системі по мірі того, як перестають бути затребувані. Наприклад, якщо перезапустити код з прикладу вище два рази поспіль (в одному процесі, зрозуміло), то додатково до вже відібраної у системі пам'яті він нічого не зажадає: вже виділених на першому проході слотів вистачить. Значення
GC[:heap_used]
зменшиться одразу після обнулення лічильника використання big_var. Пам'ять повертається менеджеру пам'яті рубі без проблем. Менеджеру пам'яті рубі, а не операційній системі — ось про це не можна забувати.

Взагалі, потрібно бути акуратним, створюючи багато тимчасових змінних малого розміру:

big_var = " " * 10_000_000
big_var.gsub(/\s/) { |c| '-' }


Цей код також отожрет шматок пам'яті системи для слотів і закуклится:

# ⇒ Memory 10156KB
# ⇒ Memory 13788KB
# ⇒ Memory 13788KB
# ⇒ Memory 12808KB


Це не смертельно, але про це варто пам'ятати.

Ще з усього сказаного випливає цікаве наслідок: не потрібно створювати ruby symbols довше 23 символів (сюди відносяться імена змінних і методів), тому що пам'ять для них буде виділятися викликом
malloc
, а не в готовому слоті. Але це вже так, тарган на торті.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.