Стендап Сьогодні 📢 Канал в Telegram @stendap_sogodni

🤖🚫 AI-free content. This post is 100% written by a human, as is everything on my blog. Enjoy!

05.03.2023

Як Ruby, Node.js та Go витрачають памʼять на функції

Ні, не міг зупинитись без того, щоб розглянути також функції як значення. В Go та JavaScript це можливо напряму, а в Ruby є так звані лямбди (хоча це й не єдиний спосіб, як я вже писав, але найбільш поширений).

Власне, що значить — зберегти функцію? Код функції памʼяті не займає, принаймні, не в тому сенсі, як змінна. Та дійсно, у всіх мовах розмір функції ніяк не впливає на витрати памʼяті. В кожній мові посилання на функцію займає по-різному. В Go - чесні 8 байтів, в JavaScript - 70, в Ruby - цілих 241 байт. Знову відчувається різниця між системною мовою, мовою з функціональною парадигмою, та мовою, де лямбду нечасто побачиш.

Але на цьому витрати на функції не закінчуються. Можна подумати, що параметри функції щось займають, проте ні — параметри належать виклику функції, а не її визначенню. Але ж в усіх трьох мовах функції є замиканнями, та посилання на функцію зберігає доступні їй змінні. Чим більше змінних, тим більше памʼяті. Як завжди, Go окрім значень, нічого не зберігає, а в JS та Ruby є невеликі додаткові витрати.

Здивувало ось що. Які саме змінні включаються в замикання? Інтуїтивна відповідь — ті, що в ньому використовуються. На Go це абсолютно вірно — бо Go ретельно відстежує шлях використання змінних, та навіть не дасть скомпілювати програму, якщо деяка змінна залишається невикористаною. Зрозуміло, що Go чітко знає, що потрапить в замикання.

В Ruby все навпаки. В Ruby лямбда замикає всі доступні змінні — незалежно від того, використовуються вони в ній чи ні. Це важливо знати, бо всі ці змінні утримуватимуться в памʼяті, поки існує лямбда. Чому так? Я не впевнений, але здається, тому, що в Ruby є метод Binding#local_variable_get, яким можна за імʼям дізнатись значення будь-якої змінної. Та дійсно, цим методом можна підгледіти змінні поза межами лямбди:

def hidden_binding(i)
  j = i * i
  ->(name) { binding.local_variable_get(name) }
end
peek = hidden_binding(2)
puts peek.call(:j) # 4

Отакої! Гнучкість мови Ruby дається не безплатно.

В JavaScript такого не відбувається, якщо тільки в замиканні немає функції eval. Якщо є, то функція також захопить всі доступні змінні. Це показують і витрати памʼяті. Причому перевірка на eval відбувається статично, тобто якщо eval передати параметром, то замикання не буде повним:

function hiddenBinding(i) {
  let j = i * i;
  return (x) => eval(x);
}
console.log(hiddenBinding(2)("j")); // 4

function hiddenBinding2(i, f) {
  let j = i * i;
  return (x) => f(x);
}
console.log(hiddenBinding2(2, eval)("j")); // j is not defined

(Ясно, що порядні люди пишуть код без eval. Але он воно як цікаво працює!)