Контракт купи процесу
Купи процесів побудовано за класичним дизайном ERTS. Ця нотатка є контрактом; його реалізовано в plan 11 phase C (steps 8A–8I), а пізніші кроки, названі нижче, розширюють його. runtime.md підсумовує API.
Що замінила phase C
| До phase C | Проблема | Заміна |
|---|---|---|
| Список фрагментів пам'яті (chunks), які ніколи не переміщуються | Комірки не можна ущільнити чи скопіювати; місткість лише зростає | Один суцільний блок купи плюс фрагменти, що переміщуються копіювальним збирачем сміття (8G, 8H) |
Кожна комірка — вузол у індексі std::map для кожного процесу | Самі лише слова купи неможливо розібрати; одне хостове виділення пам'яті та пошук O(log n) на комірку | Самоописові комірки; допуск за власним діапазоном і заголовком (8C, 8D) |
Кожна комірка bitstring має фіксований 64-байтовий масив і shared_ptr, що звільняється через реєстр деструкторів | Великі комірки для малих даних; ніщо не може перемістити комірку чи знайти її мертві копії | Binaries у купі змінного розміру та комірки binary поза купою в списку поза купою для кожного процесу (8B) |
Хостовий Term закріплює купу через shared_ptr<HeapStorage>; значення, утримувані runtime, живуть лише в Term | Нічого, що збирач міг би знайти чи переписати | Модель ERTS: C++ утримує сирі слова лише між точками безпеки; значення, утримувані runtime, є кореневими словами процесу; хостові викликачі передають явні корені в collect() (8E) |
| Один буфер купи на кожен згенерований кореневий фрейм | Немає стека процесу для сканування | Один стек кореневих фреймів на процес (8F) |
| Немає області переповнення | Виділення пам'яті або вкладається в бюджет, або зазнає збою | Фрагменти купи, поки купа не повинна переміщуватися (8G) |
Згенерований код, його ABI та кожен спостережуваний результат програми залишаються незмінними.
Розміщення слів
Терм — одне слово цільової платформи (32 або 64 біти); кодування описано в abi.md. Кожна область купи — це послідовність об'єктів, яку обхідник розбирає, починаючи з її першого слова:
- Слово заголовка (основний тег
00): біти 2–6 містятьBoxedKind, біти від 7 і вище — кількість слів, що йдуть за заголовком. Упакований (boxed) терм вказує на свій заголовок. Кількість охоплює кожне слово префікса, корисного навантаження та вирівнювання, тож обхідник пропускає невідстежуване корисне навантаження, не інтерпретуючи його. - Cons-комірка: два слова термів (голова, хвіст) без заголовка. Терм
списку вказує на голову. Голова ніколи не є заголовком, бо жоден терм не має
тегу
00. - Заповнювач: повністю нульове слово (вид
tuple, кількість 0) — це однословний заповнювач; видfillerз кількістю n охоплює ще n слів. Резервування починаються обнуленими, тож зарезервовані, але невикористані слова розбираються як заповнювач. Непорожні кортежі завжди мають ненульову кількість, а{}є безпосереднім значенням.
Жодна комірка не потребує вирівнювання, сильнішого за слово.
memory/heap_walk розбирає область комірка за коміркою, а
ProcessHeap::verify перевіряє всю купу (8C). Комірки містять лише слова та
байти, за винятком std::shared_ptr binary поза купою (нижче), тож комірка
переміщується копіюванням її слів.
| Вид | Слова після заголовка | Відстежувані слова |
|---|---|---|
| cons (без заголовка) | загалом 2 слова | голова, хвіст |
tuple | n слотів елементів | усі |
map | 2n слотів: ключі в точному порядку термів, кожен із наступним значенням | усі |
native_record | адреса RecordDefinition runtime, потім n значень полів у порядку визначення | значення |
fun_closure | адреса FunDefinition runtime, потім n захоплених значень (funs) | значення |
bignum | слово знака, потім кінцівки (limbs) модуля, від молодшої | жодних |
floating | 8 байтів: 1 слово (64 біти) або 2 слова (32 біти) | жодних |
reference | 8 байтів: номер посилання (pid і посилання) | жодних |
heap_binary | довжина в бітах, потім байти даних, округлені до слів (щонайбільше 64 байти) | жодних |
refc_binary | зсув у бітах, довжина в бітах, std::shared_ptr (2 слова), зв'язок списку поза купою: 5 слів | жодних |
filler | n невикористаних слів | жодних |
Кількість для map вказано в словах (записи = кількість / 2). Pid є
безпосередніми значеннями й допускаються за номерами, виданими runtime. Види,
ще не допущені (зовнішні ідентичності), коли з'являться, дотримуватимуться
тих самих правил: ідентичності та дескриптори є ідентифікаторами реєстру в
невідстежуваних словах, ніколи не вказівниками C++, що володіють об'єктами.
Binaries поза купою
Binary, більший за 64 байти, — це незмінний буфер, що існує поза будь-якою
купою процесу і спільно використовується з підрахунком посилань (ProcBin і
Binary у BEAM).
- Його упакована комірка
refc_binaryміститьstd::shared_ptrна буфер, зсув у бітах і довжину в бітах; зрізи великого binary — це нові коміркиrefc_binary, що спільно використовують буфер (sub-binaries ERTS не використовуються). Копіювання комірки в інший процес копіюєshared_ptr(копіювання між купами), ніколи не байти. - Кожна комірка зв'язана зі списком поза купою свого процесу через своє слово зв'язку. Цей список — єдиний спосіб знайти C++-стан цих комірок.
- Переміщення комірки копіює інші її слова та створює
shared_ptrу новій комірці переміщенням, тож стара копія нічим не володіє. Після збирання прохід по списку перезв'язує переміщені комірки та знищуєshared_ptrмертвих; завершення процесу знищує всі. Буфер звільняється, коли помирає його остання комірка в будь-якому процесі. - Кожен процес підраховує свої комірки для кожного буфера
(
HeapStorage::buffers_); буфер враховується один раз для кожного процесу, що на нього посилається (його слова поза купою, віртуальна купа binary в ERTS), доки не помре остання комірка цього процесу для нього, і один раз на загальному рахунку runtime від створення до звільнення буфера. std::shared_ptr— це два вказівники в кожній підтримуваній STL;static_assertфіксує розмір комірки в п'ять слів після заголовка для обох ширин.
Області
- Купа. Один блок
[start, top, end)на процес із виділенням пам'яті зсувом вказівника (bump allocation) (8G). Він створюється першим виділенням пам'яті процесу, має розмірmax(min_heap_words, request), щоб запит завжди вміщався, і належить лише цьому процесу. - Фрагменти. Коли запит не вміщується, а купа не може переміщуватися, він
потрапляє до найновішого фрагмента, якщо вміщується там, інакше — до нового
фрагмента відповідного розміру (щонайменше мінімального розміру купи),
приєднаного до ланцюжка процесу. Наступне збирання об'єднує фрагменти в
новий блок купи. Резервування живе в одній області; відкат скидає
topцієї області та відкидає фрагмент (або блок купи), створений резервуванням. Виділення пам'яті ніколи не переміщує купу: переповнення залишається у фрагментах до наступної точки безпеки або хостового збирання (збирання в згенерованому коді). - Стек. Згенеровані фрейми (Y-регістри BEAM) живуть в одному плоскому
стеку на процес, окремо від купи (
ProcessStack, step 19). Кожен фрейм — це чотирислівний заголовок (зсув заголовка викликача, дескриптор, точка відновлення, обробник), за яким ідуть слоти термів (зокрема вивантажені терми) і сирі слоти вивантаження; фрейми зв'язані зсувами, тож блок зростає подвоєнням і переміщується. За замовчуванням він не має обмеження; необов'язкове обмеження на процесStackOptions::limit_wordsобмежує його окремо від купи (модель виконання). - Список поза купою. Як описано вище.
- Стара купа. Немає. Збирання за поколіннями відкладено; незмінні терми ніколи не вказують зі старіших даних на новіші, тож позначку верхнього рівня та стару купу можна додати пізніше без зміни комірок.
Розміри та бюджет
- Купа починається з
min_heap_words(233 слова, як в ERTS) і зростає за послідовністю розмірів ERTS: 12, 38, далі кожен розмір — сума двох попередніх плюс один до 833,026 слів, потім кроки по 20% (heap_size_at_least). - Новий блок збирання — найменший такий розмір, за якого слова, які він може
отримати, становлять менше 75% від нього: спершу всі використані слова,
оскільки живі дані до копіювання невідомі. Результат, у якому живих даних
менше 25%, копіюється ще раз у розмір, потрібний для його живих даних (8H);
якщо виділити цей блок не вдається, залишається більший. Жоден не менший за
min_heap_words. Обидва розміри враховують слова стека процесу як живі (step 26), оскільки ERTS тримає стек усередині блоку купи. - Якщо задано бюджет (нижче), новий блок містить щонайбільше свої живі слова
плюс половину бюджету, що залишається після них і буферів поза купою
(
block_limit, step 27), але ніколи не менше заmin_heap_words. Інша половина залишається вільною для фрагментів і нових буферів поза купою, тож сміття, виділене після збирання, досягає наступної точки безпеки як тригер, а не вичерпує бюджет. Розмір першої копії визначається з усіх використаних слів, тож блок, що перевищує межу для слів, які вижили, копіюється ще раз у розмір за політикою. - За замовчуванням немає обмеження пам'яті ні на процес, ні на runtime: купа
зростає, доки хост не відмовить у пам'яті (
out_of_memory), а описане вище визначення розмірів тримає її близько до розміру живих даних. Необов'язковий бюджет на процес,HeapOptions::limit_bytes(за замовчуваннямUNLIMITED_HEAP_BYTES), охоплює блок купи, фрагменти та байти буферів поза купою, на які посилається цей процес; його перевищення — цеlimit_exceeded. Під час збирання старий і новий блоки співіснують; лише новий блок перевіряється на відповідність бюджету, обмеженому залишком бюджету після буферів поза купою. Внесок буфера повертається, коли процес відкидає свою останню комірку для нього. Стек має власне необов'язкове обмеження,StackOptions::limit_words. Програми задають обидва обмеження через--max-heapі--max-stack(параметри runtime).
Обмеження пам'яті runtime
- Необов'язкове загальне обмеження runtime,
RuntimeOptions::memory_limit_bytes(за замовчуваннямUNLIMITED_HEAP_BYTES; програми задають його через--max-memory), обмежує пам'ять усіх процесів разом: блоки купи, фрагменти, буфери поза купою та місткість стеків (step 27A). В OTP такого обмеження немає; найближчий аналог — запуск VM під обмеженням пам'яті ОС, але тут збою зазнає один процес, а не вузол. - Один рахунок на runtime (
detail::RuntimeMemory, спільний для кожного сховища купи та стека) поповнюється, коли створюється блок, фрагмент, буфер поза купою чи місткість стека, і звільняється, коли їх відкидають; буфер, спільний для кількох процесів, враховується один раз і звільняється, коли помирає останнє посилання на нього (step 28). Завершення процесу повертає всі його внески.Runtime::memory_bytes()повідомляє загальну суму. - Для кожного процесу обмеження діє як бюджет зі сховища, яким він володіє,
плюс того, що залишає обмеження (
HeapStorage::budget,room), тож описане вище визначення розмірів залишає половину вільної пам'яті вільною після кожного збирання, а запит понад це —limit_exceeded(resource_limit) лише для процесу, що робить запит; інші процеси продовжують роботу. Сміття іншого процесу враховується, доки той процес не виконає збирання. - Цільовий простір (to-space) збирання враховується навіть понад обмеження, оскільки він замінює блоки, які звільняє наприкінці того самого збирання.
- Стек подвоюється, доки це дозволяє обмеження, а потім зростає лише на фрейм, що заштовхується.
Допуск
Вказівники в купу процесу створюються лише компілятором і runtime всередині цього процесу і завжди вказують на початок об'єкта; внутрішніх вказівників, які треба виявляти, немає. Допуск (8D) — це перевірка володіння для слів, що повертаються процесу:
- Адреса вирівняна за словом усередині однієї з областей процесу, нижче за
її
top: спершу перевіряється блок купи, потім фрагменти, відсортовані за адресою. Чужі та застарілі слова не проходять тут без жодного завантаження. - Упаковане слово вказує на заголовок допущеного виду (не заповнювач); слово списку вказує на cons-комірку (слово, яке не є заголовком).
Методи доступу декодують вид, кількість і корисне навантаження із самого
заголовка. verify() залишається повною перевіркою того, що кожен слот вказує
на початок об'єкта, для тестів.
Корені та точки безпеки
ProcessContext::visit_roots перелічує кожне кореневе слово для збирача
(step 23). У точці безпеки ніщо інше не утримує слова купи процесу:
| Власник | Кореневі слова | Примітки |
|---|---|---|
| Слоти термів фрейму | Перші roots слотів кожного фрейму в стеку (step 19) | Нижні фрейми не мають жодних; індекси відновлення та обробника — цілі числа |
| Сирі слоти фрейму | Жодних | Вивантажені нативні значення; згенерований код не тримає там слів купи в точці безпеки (правило повторного завантаження step 24) |
| Регістри | x[0..live) (ProcessStack::keep_registers) | Аргументи призупиненої точки входу (step 43); кожне заштовхування та виштовхування очищає live |
| Канал збоїв | Корисне навантаження помилки (BEAM fvalue), список аргументів erlang:error/2,3, терм трасування стека | Перезв'язуються на місці; захоплені фрейми трасування є вказівниками дескрипторів у код |
| Стан trap | Слова термів TrapState вбудованої функції, що виконала trap (step 43A) | Звільняються, коли вбудована функція завершується або зазнає збою |
| Поштова скринька | Кожне повідомлення у вхідній черзі сигналів і черзі повідомлень (step 45), зокрема повідомлення 'EXIT' і 'DOWN' | Доки receive його не забере; переписуються на місці, тож курсор receive (позиція в списку) і крайній термін тайм-ауту залишаються дійсними |
| Явні корені | Діапазон, який хост передає в collect(roots) (8E) | Зчитуються назад після виклику |
| Список поза купою | Жодних | Зв'язки проходяться та перезв'язуються, а не відстежуються |
Жодна комірка купи не містить закріплення. Атоми є безпосередніми
значеннями, а таблиця атомів ніколи не збирається. Комірки fun називають код
через свій невідстежуваний FunDefinition, який живе стільки ж, скільки
runtime; завантажені модулі ніколи не вивантажуються, тож ні funs, ні
дескриптори трасування не потребують закріплення. Малі безпосередні значення
не є коренями.
Як і в коді C в ERTS, хостовий Term — це сире теговане слово, дійсне до
наступної точки безпеки його купи. Він не закріплює сховище купи; він
утримує слабкий токен часу життя контексту та лічильник збирань купи, тож
використання після завершення процесу повідомляє expired_context, а
використання після пізнішого збирання повідомляє про помилку застарілого
терма. Term дійсний лише всередині власного процесу; інші процеси можуть
лише читати його.
Купа переміщується лише в точці безпеки і ніколи, поки резервування відкрите:
- явний хостовий
collect(), поки контекст не виконує згенерованого коду; collect()під час виконання згенерованого коду, що оголосив областьSafePoint, обіцяючи, що утримує слова купи лише у вказаних вище коренях. Runtime відкриває її лише в точках безпеки згенерованого коду з наступного розділу.
Будь-який інший запит повертає unsafe_point і нічого не змінює, навіть
канал збоїв згенерованого виклику, що виконується. Виділення пам'яті ніколи
не переміщує купу: запит, що не вміщується, створює фрагмент.
Збирання в згенерованому коді
Рішення plan 11 step 24 (2026-10-06), реалізоване в step 26 (реалізація). Згенерований код виконує збирання лише в кількох точках безпеки, де кожен живий терм уже перебуває в корені. Усе інше, зокрема кожен сервіс, що виділяє пам'ять, є критичною секцією, яка ніколи не переміщує купу.
Тригери
Точка безпеки виконує збирання, коли цього вимагає купа; інакше вона коштує одну перевірку.
| Тригер | Умова в точці безпеки | Відповідник в ERTS |
|---|---|---|
| Купа заповнена | Існує будь-який фрагмент: з часу останнього збирання виділення не вмістилося в блок купи | Вершина купи досягає кінця купи |
| Тиск binaries поза купою | Слова поза купою досягають межі віртуальної купи binary: спочатку 46,422 слова, після кожного збирання — удвічі більше за слова поза купою, що вижили, ніколи не менше за це, але щонайбільше ті, що вижили, плюс половина бюджету, що залишається вільною після блоку купи (step 27) | bin_vheap_sz / віртуальна купа binary |
erlang:garbage_collect/0 | Завжди; з'являється разом із сімействами вбудованих функцій (steps 36-37) як примусова точка безпеки | Явний повний прохід |
Розмір нового блоку визначається для живих слів плюс слів стека, що використовуються (ERTS тримає стек усередині блоку купи): глибокий стек отримує більшу купу, тож довга рекурсія виконує збирання пропорційно своєму виділенню пам'яті, а не пересканує весь стек кожні кілька сотень слів.
Точки безпеки
| Точка | Де | Живе поза слотами термів фрейму |
|---|---|---|
| Вхід у функцію | У CLAUSE_enter_v1 / CLAUSE_tail_v1 (а отже, і в хостовому виклику), перед заштовхуванням фрейму викликаної функції | Аргументи викликаної функції x[0..arity), утримувані як корені (keep_registers) |
| Початок циклу | Виклик CLAUSE_safepoint_v1(context) на початку кожного циклу генератора comprehension | Нічого |
Кожен цикл Erlang — це або рекурсія, яка на кожному кроці проходить через вхід у функцію, або comprehension, який проходить через початок свого циклу, тож сміття між двома точками безпеки обмежене лінійним кодом і окремими результатами сервісів.
Не є точками безпеки (критичні секції, які продовжують виділяти пам'ять у
фрагментах): кожен інший сервіс runtime, зокрема сервіси виділення пам'яті,
побудови та зіставлення; CLAUSE_return_v1; поширення винятків; і пізніше
доставлення повідомлень (step 45). Тому сервіси можуть утримувати сирі слова
купи в C++ протягом усього свого виконання, а їхні вхідні масиви та
результати не потребують повторного завантаження.
Процеси, що очікують і призупинені
Plan step 51. Процес, який не виконується, ніколи не збирається: він очікує в
receive, стоїть у черзі після yield чи trap або ще не запущений, і все, що він
утримує, вже є коренем (його фрейми, регістри точки входу чи продовження, з
якого він відновиться, стан trap і його повідомлення). Повідомлення, надіслані
йому, копіюються у фрагменти його купи. Кожне доставлення пробуджує процес,
що очікує, а його відновлення повторює вхід у його продовження (вбудовану
функцію очікування, продовження trap або функцію, в якій він виконав yield),
що є точкою безпеки входу у функцію: перше, що робить відновлений процес, —
виконує збирання, якщо цього вимагає його купа. Тому процес, що очікує у
вибірковому receive, який пропускає багато повідомлень, виконує збирання в
міру їх надходження, так само як ERTS збирає процес під час його наступного
планування. executables_mailbox_collection перевіряє накопичення,
очікування з тайм-аутом і глибоку рекурсію під навантаженням повідомленнями,
а також те, що споживач, який підтверджує 3,000 повідомлень, залишається в
межах --max-heap 65536.
Відхилено: виділення пам'яті як точка безпеки (BEAM test_heap). Це
вимагало б, щоб кожен вхід сервісу та кожен SSA-терм, живий під час будь-якого
виділення, був у корені, повторного завантаження після кожного сервісу, що
виділяє пам'ять, і протоколу повторних спроб у кожному сервісі, тоді як дві
вказані вище точки безпеки вже обмежують сміття.
Правило повторного завантаження
Жодне SSA-значення (значення в нативному регістрі) не утримує слово купи
через точку безпеки, і жоден нативний вказівник взагалі не перетинає її (це
вже помилка в lower_frames).
lower_framesобробляє виклик точки безпеки на початку циклу як точку відновлення виклику: розділяє блок після виклику та вивантажує кожне значення, що читається після нього, але обчислене до нього. Значення терма (завантаження зі слота терма чи регістра, значення, збережене в слот терма, або PHI таких значень) зберігається після свого визначення в наявний слот терма або в новий слот терма, врахований уrootsдескриптора, і повторно завантажується перед кожним використанням. Інші слова (малі безпосередні значення, атоми, сирі цілі числа, прапорці) зберігаються в сирих слотах: збирання ніколи їх не змінює.- Виклики вивантажують і повторно завантажують так само, терми — у слоти термів, тож точка безпеки входу бачить кожен живий терм кожного викликача.
- База фрейму залишається дійсною через точку безпеки на початку циклу, оскільки збирання переписує слова стека на місці й ніколи не переміщує стек; кожне передавання керування, як і раніше, перечитує її в пролозі тіла.
- Оптимізація виконується після
lower_framesі не може замінити повторне завантаження старішим SSA-значенням: адреса фрейму походить ізCLAUSE_frame_v1, тож виклик точки безпеки може записати будь-який слот (прототип нижче).
Поведінка при збоях
- Збирання в точці безпеки ніколи не записує збій. Коли його новий блок не
вдається виділити (
out_of_memory), купа залишається як є, а виконання продовжується з фрагментами. - Вичерпання пам'яті (step 27). Без обмеження пам'ять закінчується лише
тоді, коли хост відмовляє у блоці купи, фрагменті, буфері поза купою чи
зростанні стека:
out_of_memory. Якщо задано необов'язковий бюджет або загальне обмеження runtime, запит понад нього — цеlimit_exceeded, що повідомляється якresource_limit. Обидва є інфраструктурними збоями: жоден обробник не виконується, фрейми розгортаються до нижнього фрейму, а програма друкуєclau: runtime failure: entry call failed: <status>і завершується з кодом 70 після скидання stdout і знищення процесу (виконувані файли, відмінності). Оскільки кожне збирання залишає вільною половину бюджету, що залишається після тих, хто вижив (розміри, тригери), бюджет зазнає збою лише тоді, коли живий набір більше не вміщується або коли лінійний код між двома точками безпеки виділяє більше за цю половину; спершу збирається сміття.
Реалізація
Step 26 (2026-10-06):
ProcessStack::safepoint(live)запитуєProcessHeap::wants_collection()(існує фрагмент або слова поза купою досяглиbinary_limit_words_), утримуєx[0..live)як корені, відкриваєSafePointі виконує збирання; невдале збирання ігнорується.enter(а отже,tailіinvoke) викликає її з арністю викликаної функції перед заштовхуванням;CLAUSE_safepoint_v1викликає її з 0.- Пониження (lowering) comprehensions генерує
CLAUSE_safepoint_v1на початку кожного циклу генератора (lowering_comprehensions). lower_framesрозділяє кожне тіло після виклику точки безпеки та вивантажує значення, що її перетинають, як після виклику.home()зберігає слот аргументу або слот терма того самого блоку, якщо там є значення; інакше значення терма (term_value: завантажене зі слота терма чи регістра, збережене в слот терма або PHI таких) отримує новий слот терма, якийplace_slotsдодає до початкових слотів термів перед сирими слотами, а будь-яке інше значення — сирий слот.- Еталон
executables_garbage_collection(згенерований OTP) виділяє понад 64 MiB з малим живим набором: хвостовий цикл, що будує рядок із 400 слів на кожному кроці, 9,000 binaries поза купою по 8 KiB, comprehension, чий фільтр виділяє пам'ять на кожен елемент, рекурсію в тілі глибиною 20,000, що утримує вкладений терм (кортеж, список, binary) на кожен фрейм, і корисне навантаження помилки, перехоплене після розгортання фреймів, що виділяють пам'ять, і утримуване протягом довгого циклу з виділенням пам'яті.
Step 27 (2026-10-06):
collected_sizeобмежує блок значеннямblock_limit(live);shrinkтакож виконується, коли блок перевищує межу для слів, що вижили;collectобмежуєbinary_limit_words_тими, що вижили, плюс половиною бюджету, що залишається вільною після блоку.- Раніше блок міг забрати весь залишок бюджету (розмір визначався з використаних слів, включно зі сміттям), а віртуальна купа binary могла перевищити його, щойно ті, що вижили, перевищували половину бюджету, тож виділення пам'яті зазнавали збою, поки сміття ще не було зібране: за тодішнього типового бюджету 64 MiB 64-бітний запуск, що утримував 700 binaries по 64 KiB і відкидав чотири на кожному кроці, зазнавав збою при 68% живих даних; з обмеженнями вміщується 1,010 (99%).
- Типовий бюджет купи 64 MiB і бюджет стека 2^24 слів було видалено
(вказівка користувача): обидва вмикаються за бажанням для кожного процесу
(
Runtime::create_context(HeapOptions, StackOptions)), за замовчуванням без обмежень. - Еталон
executables_heap_growthутримує 1,100 binaries по 65,540 байтів (72 MB), відкидаючи чотири на кожному кроці, і друкує кількість так само, як OTP.runtime_collectionnear_budgetперевіряє обидва обмеження з бюджетом 10,000 слів (блок купи та буфери поза купою).
Step 27A (2026-10-06):
- Загальне обмеження runtime і
--max-heap,--max-stack,--max-memory(обмеження пам'яті runtime). Написані вручну еталонні запуски знову доводять обмеженість пам'яті:garbage_collectionchurnіbinariesпід загальним обмеженням runtime 1 MiB,comprehensionпід обмеженням купи 1 MiB іpayloadпід загальним обмеженням runtime 16 MiB (кожен виділяє понад 64 MiB); циклиtail_callsпід стеком 4 KiB і обмеженням купи 64 KiB;deepпід 1 MiB іdeep_recursionbuildпід стеком 64 KiB зазнають збою зresource_limit. Самdeepпотребує близько 100 MiB на O0 (живі фрейми утримують застарілі терми й займають близько 130 слів кожен), тож для нього немає успішного запуску з обмеженням.runtime_collectionshared_limit: два процеси під обмеженням 40,000 слів; блок утримувача зупиняється на 28,000 слів, інший збирає 20 раундів сміття поблизу обмеження, потім зазнає збою на списку з 16,000 слів зresource_limit, поки утримувач продовжує виділяти пам'ять, а завершення повертає всі внески.
Прототип
tests/prototypes/safepoint містить один цикл
у стилі comprehension у формі після lower_frames (loop.ll): терм Y,
обчислений до циклу, зберігається в слот терма й повторно завантажується
після точки безпеки на початку циклу.
python tests/prototypes/safepoint/run.py компілює його для
x86_64-pc-windows-msvc, aarch64-unknown-linux-gnu (64-бітні слова),
i686-pc-windows-msvc і armv7-unknown-linux-gnueabihf (32-бітні слова) на
O0 і O2 та перевіряє, що завантаження слова фрейму Y йде після виклику
точки безпеки. Усі вісім проходять із clang 23.1.2. На O2 (i686) цикл
зберігає повторне завантаження зі слота й ніколи не використовує повторно
регістр, що утримував Y:
LBB0_2: # loop head
pushl %edi
calll _clause_safepoint_v1
pushl 20(%esi) # cursor reloaded from its term slot
...
pushl 24(%esi) # accumulator
pushl 28(%esi) # Y reloaded from its term slot
calll _make
Збирання
Повнопрохідне копіювання за Чейні (Cheney) (8H, memory/heap_collect):
спершу виділяється новий блок (збій — це out_of_memory, і купа
залишається незмінною), потім хостові Term позначаються як застарілі,
копіюється об'єкт за кожним кореневим словом, і новий блок сканується зліва
направо з копіюванням дочірніх об'єктів кожної копії. Заголовок переміщеного
упакованого об'єкта замінюється упакованим вказівником на його копію;
переміщена cons-комірка отримує нульову голову та хвіст, що вказує на її
копію. Переадресація зберігає спільне використання. Корені переписуються на
місці, список поза купою проходиться (копії перезв'язуються в порядку списку,
мертві комірки знищуються), а старий блок і фрагменти звільняються. Купа,
яку ніколи не виділяли, не збирається. CollectionStats повідомляє кількість
слів до збирання, живі слова, новий блок купи, об'єднані фрагменти, місткість
слотів стека та слова поза купою.
Копіювання між купами
ProcessHeap::add(value), або рівнозначно value.copy_to(heap), повертає
терм купи призначення (step 28, BEAM size_object і copy_struct):
- Безпосередні значення та атоми того самого runtime не потребують сховища, а
терм купи призначення зберігає свою ідентичність. Граф іншого процесу того
самого runtime копіюється; граф іншого runtime — це
wrong_owner, джерело з минулим терміном —expired_context, дескриптор джерела, старіший за останнє збирання його купи, —stale_term. Фабрики й надалі відхиляють чужі вхідні дані (ProcessHeap::retain), тож граф переміщує лише явне копіювання. - Один обхід з явним стеком (без рекурсії) знаходить кожен окремий об'єкт,
досяжний зі значення, з ключем за адресою, тож внутрішнє спільне
використання зберігається:
{T, T}копіюєTодин раз, на відміну від типовогоcopy_structв ERTS, який сплощує спільне використання. Копія — це одне резервування на суму їхніх слів у блоці купи або у фрагменті, заповнене в порядку обходу з переписаними на копії вказівниками. - Копія binary поза купою — це нова комірка, що спільно використовує буфер; призначення утримує буфер (враховуючи власні слова поза купою, якщо досі не утримувало його) перед резервуванням і вносить комірку до списку лише після підтвердження резервування.
- Джерело лише читається. Збій (
resource_limitдля бюджету призначення чи обмеження runtime,out_of_memoryдля хоста) відпускає утримання буферів і відкочує резервування, тож обидві купи та кожен внесок залишаються такими, як були. Копія не володіє жодним сховищем джерела: вона переживає збирання та завершення джерела.
Вимірювання
runtime_heap_measurements (CTest у повному режимі; числа друкуються, а не
перевіряються) будує список зі 100,000 кортежів {Index, Float} через
TermFactory, обходить його назад через перевірені методи доступу та
створює 1,000 контекстів, кожен з яких утримує один малий кортеж. Починаючи з
8I, він також виконує збирання зі списком як єдиним коренем і обходить копію.
Додаткові байти — це хостові виділення пам'яті понад резерв купи (індекс
об'єктів до 8D, ланцюжок фрагментів починаючи з 8G).
| Ревізія | Збірка | Побудова / обхід ядра | Використано / місткість купи, слів | Додаткові байти | Байтів на контекст | Слів купи на контекст |
|---|---|---|---|---|---|---|
bb09359 (список chunks, індекс об'єктів) | Windows x64 Debug, clang-cl | 264 / 81 мс | 700,000 / 704,512 | 24,002,256 (близько 80 на комірку) | 66,217 | 8,192 |
| 8D (список chunks, власний діапазон) | Windows x64 Debug, clang-cl | 185 / 147 мс | 700,000 / 704,512 | 3,440 | 66,057 | 8,192 |
| 8G (купа з 233 слів, близько 3,000 фрагментів) | Windows x64 Debug, clang-cl | 219 / 174 мс | 700,000 / 706,223 | 163,878 | 2,377 | 233 |
| 8I, до збирання | Windows x64 Debug, clang-cl | 216 / 173 мс | 700,000 / 706,223 | 163,878 | 2,377 | 233 |
| 8I, після одного збирання | Windows x64 Debug, clang-cl | збирання 56 мс / обхід 72 мс | 700,000 / 999,631 | 0 | — | — |
Порівняно з базовим рівнем bb09359: додаткові метадані на комірку зникли
(з 24 MB до нуля після збирання), контексту потрібно 2.4 KB і 233 слова купи
замість 66 KB і 8,192 слів, побудова приблизно на 20% швидша, а обхід
повільніший, доки збирання не об'єднає фрагменти (допуск фрагментів — це
двійковий пошук серед близько 3,000 діапазонів); після одного збирання обхід
триває 72 мс. Живий набір із 700,000 слів збирається приблизно за 56 мс у
блок із 999,631 слова — розмір ERTS, що тримає його нижче 75%.
Clause