Clause
← Уся документація

Перекладено з англійського оригіналу · 06042fa · 2026-10-09 · Читати англійською

Контракт купи процесу

Купи процесів побудовано за класичним дизайном 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. Кожна область купи — це послідовність об'єктів, яку обхідник розбирає, починаючи з її першого слова:

Жодна комірка не потребує вирівнювання, сильнішого за слово. memory/heap_walk розбирає область комірка за коміркою, а ProcessHeap::verify перевіряє всю купу (8C). Комірки містять лише слова та байти, за винятком std::shared_ptr binary поза купою (нижче), тож комірка переміщується копіюванням її слів.

ВидСлова після заголовкаВідстежувані слова
cons (без заголовка)загалом 2 словаголова, хвіст
tuplen слотів елементівусі
map2n слотів: ключі в точному порядку термів, кожен із наступним значеннямусі
native_recordадреса RecordDefinition runtime, потім n значень полів у порядку визначеннязначення
fun_closureадреса FunDefinition runtime, потім n захоплених значень (funs)значення
bignumслово знака, потім кінцівки (limbs) модуля, від молодшоїжодних
floating8 байтів: 1 слово (64 біти) або 2 слова (32 біти)жодних
reference8 байтів: номер посилання (pid і посилання)жодних
heap_binaryдовжина в бітах, потім байти даних, округлені до слів (щонайбільше 64 байти)жодних
refc_binaryзсув у бітах, довжина в бітах, std::shared_ptr (2 слова), зв'язок списку поза купою: 5 слівжодних
fillern невикористаних слівжодних

Кількість для map вказано в словах (записи = кількість / 2). Pid є безпосередніми значеннями й допускаються за номерами, виданими runtime. Види, ще не допущені (зовнішні ідентичності), коли з'являться, дотримуватимуться тих самих правил: ідентичності та дескриптори є ідентифікаторами реєстру в невідстежуваних словах, ніколи не вказівниками C++, що володіють об'єктами.

Binaries поза купою

Binary, більший за 64 байти, — це незмінний буфер, що існує поза будь-якою купою процесу і спільно використовується з підрахунком посилань (ProcBin і Binary у BEAM).

Області

Розміри та бюджет

Обмеження пам'яті runtime

Допуск

Вказівники в купу процесу створюються лише компілятором і runtime всередині цього процесу і завжди вказують на початок об'єкта; внутрішніх вказівників, які треба виявляти, немає. Допуск (8D) — це перевірка володіння для слів, що повертаються процесу:

  1. Адреса вирівняна за словом усередині однієї з областей процесу, нижче за її top: спершу перевіряється блок купи, потім фрагменти, відсортовані за адресою. Чужі та застарілі слова не проходять тут без жодного завантаження.
  2. Упаковане слово вказує на заголовок допущеного виду (не заповнювач); слово списку вказує на 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 дійсний лише всередині власного процесу; інші процеси можуть лише читати його.

Купа переміщується лише в точці безпеки і ніколи, поки резервування відкрите:

Будь-який інший запит повертає 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).

Поведінка при збоях

Реалізація

Step 26 (2026-10-06):

Step 27 (2026-10-06):

Step 27A (2026-10-06):

Прототип

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_heap_measurements (CTest у повному режимі; числа друкуються, а не перевіряються) будує список зі 100,000 кортежів {Index, Float} через TermFactory, обходить його назад через перевірені методи доступу та створює 1,000 контекстів, кожен з яких утримує один малий кортеж. Починаючи з 8I, він також виконує збирання зі списком як єдиним коренем і обходить копію. Додаткові байти — це хостові виділення пам'яті понад резерв купи (індекс об'єктів до 8D, ланцюжок фрагментів починаючи з 8G).

РевізіяЗбіркаПобудова / обхід ядраВикористано / місткість купи, слівДодаткові байтиБайтів на контекстСлів купи на контекст
bb09359 (список chunks, індекс об'єктів)Windows x64 Debug, clang-cl264 / 81 мс700,000 / 704,51224,002,256 (близько 80 на комірку)66,2178,192
8D (список chunks, власний діапазон)Windows x64 Debug, clang-cl185 / 147 мс700,000 / 704,5123,44066,0578,192
8G (купа з 233 слів, близько 3,000 фрагментів)Windows x64 Debug, clang-cl219 / 174 мс700,000 / 706,223163,8782,377233
8I, до збиранняWindows x64 Debug, clang-cl216 / 173 мс700,000 / 706,223163,8782,377233
8I, після одного збиранняWindows x64 Debug, clang-clзбирання 56 мс / обхід 72 мс700,000 / 999,6310——

Порівняно з базовим рівнем bb09359: додаткові метадані на комірку зникли (з 24 MB до нуля після збирання), контексту потрібно 2.4 KB і 233 слова купи замість 66 KB і 8,192 слів, побудова приблизно на 20% швидша, а обхід повільніший, доки збирання не об'єднає фрагменти (допуск фрагментів — це двійковий пошук серед близько 3,000 діапазонів); після одного збирання обхід триває 72 мс. Живий набір із 700,000 слів збирається приблизно за 56 мс у блок із 999,631 слова — розмір ERTS, що тримає його нижче 75%.