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

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

Модель виконання: фрейми й продовження

Рішення плану 11, крок 17 (2026-10-05). Воно визначає, як згенеровані функції Erlang викликають, повертають керування, призупиняються й зазнають збою, коли з'являються рекурсія, хвостові виклики й процеси. Крок 19 реалізував виклики, повернення, хвостові виклики й фрейми (Реалізація перелічує, що ще відкрито); кроки 23, 24, 26 і 43 будуються на ньому.

Рішення

Кожен процес Erlang виконується на власному пласкому стеку явних фреймів, а згенерований код переходить між функціями лише через гарантовані хвостові передачі (LLVM musttail). Нехвостовий виклик зберігає продовження (continuation) того, хто викликає, у його фреймі й переходить до функції, яку викликають; повернення переходить назад до цього продовження. Тому нативний стек лишається завглибшки в один виклик над планувальником, хоч якою глибокою була б рекурсія Erlang, а процес може зупинитися на будь-якій передачі й пізніше відновитися на будь-якому потоці.

Стан процесу

ПолеЗначення
stack, capacityОдин масив слів, що може зростати; переміщується, коли зростає
frame, topЗсуви в словах до заголовка поточного фрейму й до першого вільного слова
x[], liveРегістри аргументів/результату; перші live слів є коренями під час передачі
reductionsСкільки викликів лишилося в кванті часу
resume_atВхід, з якого продовжує призупинений процес
канал збоївНинішній канал ревізії 2: причина, корисне навантаження, трасування стека, halted

Фрейми

Фрейм — це фіксований заголовок, за яким ідуть слоти функції (Y-регістри BEAM). Слоти обнулюються під час заштовхування, як нині кореневі фрейми.

Слово заголовкаЗначення
previousЗсув заголовка того, хто викликав (фрейми зв'язуються зсувами, ніколи вказівниками)
functionДескриптор функції
resumeІндекс продовження, за яким перемикається тіло, коли керування повертається сюди
handlerІндекс продовження найглибшого активного обробника, 0, коли такого немає

Дескриптор розширює нинішній abi::v1::FrameDescriptor (дескриптор модуля, слоти атомів модуля й функції, арність) вказівниками на код входу й тіла та кількістю слотів. Слова заголовка не є термами; обхідник іде за previous і читає кількість слотів із дескрипторів. Нижній фрейм (bottom frame), що належить runtime, лежить під першим викликом кожного процесу: продовження 1 — це нормальний вихід (результат у x[0]), продовження 2 — обробник неперехопленого винятку.

Операції

Функція, що не робить нехвостових викликів і не тримає слотів через точку безпеки (safepoint), може пропустити свій фрейм і повернутися прямо в тіло того, хто викликав. Це оптимізація, яку компілятор може додати пізніше, а не частина контракту.

Видимість коренів

На кожній передачі й кожній точці безпеки корені процесу такі: усі слоти всіх фреймів на його стеку, x[0..live) і корисне навантаження, список аргументів і терм стека каналу збоїв (уже корені процесу). Слова передачі результату нинішнього стеку коренів зникають; їхню роль бере x[0].

Значення ніколи не переживають передачу в нативних регістрах чи значеннях SSA. Кожне тіло перезавантажує адресу свого фрейму з stack + frame після входу й читає живі значення зі слотів. У межах одного продовження служба, що може заштовхнути фрейм чи перемістити купу, робить недійсними всі вказівники на слоти й на купу, що зберігаються в значеннях SSA; правило перезавантаження для збирань визначено в розділі збирання в згенерованому коді.

Наступник стеку коренів 8F

Сегментований стек існує лише тому, що згенерований код тримає абсолютні вказівники на фрейми через виклики. У цій моделі жоден вказівник на фрейм не переживає передачу, тож стек стає одним пласким блоком на процес:

Цільові платформи

musttail з уніфікованою сигнатурою void (Process *) приймається кожною потрібною цільовою платформою на O0 і O2 (прототип нижче, clang 23.1.2):

ЦільСловоРезультат
x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu64хвостові переходи
i686-pc-windows-msvc, i686-unknown-linux-gnu32хвостові переходи
aarch64-unknown-linux-gnu, arm64-apple-macosx14.064хвостові переходи
armv7-unknown-linux-gnueabihf32хвостові переходи

Бекенд повідомляє про помилку, коли не може виконати виклик musttail, тож успішна компіляція і є гарантією. Запасний варіант для майбутньої цілі, що його відхилить: трамплін. Кожен код повертає наступний вказівник на код до циклу планувальника замість переходу (null призупиняє); фрейми, корені й індекси продовжень не змінюються. Жодна потрібна ціль цього не потребує.

Реалізація

Крок 19 (2026-10-05) реалізує модель із такими рішеннями й прогалинами:

Порівняння альтернатив

Прототип у tests/prototypes/execution_model: ті самі функції Erlang (sum/1 — рекурсія в тілі, loop/2 — хвостова рекурсія, fail/1, що породжує boom на глибині N, catcher/1, що його перехоплює), знижені вручну трьома способами. Хост: Windows x64, clang 23.1.2; час — одиничні запуски на O2. Запуск: python tests/prototypes/execution_model/run.py.

Явні фрейми + musttail (обрано)Нативні виклики + кореневі фрейми (нині)Корутини LLVM (C++20)
Рекурсія в тілі завглибшки 1Mok, 17 мс; 5 слів на фрейм; 17 переміщень стека80 Б нативного стека на рівень: близько 13 000 рівнів у потоці з 1 МіБok, 60 мс; одне виділення в купі на 64–80 Б на виклик
10M хвостових викликівok, 5 мс; сталий стекO0 зростає на 80 Б за виклик; O2 — лише завдяки везінню з sibling callнемає хвостових викликів: 1M ітерацій тримають 1M фреймів
Поступання / відновленнякожен вхід; два процеси чергуютьсянеможливо без нативного стека на процессиметрична передача (сама є musttail)
Нативний стек на глибині 1M136–144 Бзростає з кожним рівнем144–520 Б
Корені, видимі для GCслоти у відомих фреймахслоти у відомих фреймахрозкладку фрейму корутини обирає LLVM; термам знадобилася б друга копія з коренями
Виняткирозмотування до фрейму обробникаперевірка каналу на кожному поверненніперевірка каналу на кожному поверненні

Варіант обраної моделі з трампліном проходить ті самі запуски (20 мс на рекурсію, 16 мс на 10M хвостових викликів на O2). Відхилено:

Прийнятні витрати: один непрямий перехід на повернення плюс диспетчеризація switch; значення, живі через виклики, перезавантажуються зі слотів (вони там уже зберігаються); зворотні трасування нативного налагоджувача показують лише поточну функцію, тоді як трасування стека Erlang беруться з ланцюга фреймів.

Свідчення прототипу

run.py збирає обрану модель (musttail і трамплін), нативну базову лінію й модель корутин для хоста на O0 і O2, запускає їх і компілює вручну знижені функції (generated.cpp, freestanding) для кожної цілі вище на O0 і O2, рахуючи виклики musttail в IR і хвостові переходи в асемблері. Результат 2026-10-05: PASS. Для обраної моделі на обох рівнях: повернення (42), рекурсія в тілі завглибшки 1 000 000, 10 000 000 хвостових викликів, помилка, породжена на глибині 100 000 і перехоплена фреймом обробника, та сама помилка без перехоплення (нижній фрейм, трасування з 8 фреймів fail) і два процеси, що чергуються квантами по 4 000 редукцій (1 002 кванти). Усі 15 місць передачі є musttail на O0 на кожній цілі (14 на O2 після вбудовування).

Тіло sum/1 для i686-pc-windows-msvc (IR на O2, імена скорочено). IR для x86_64-pc-windows-msvc такий самий, зі словами i64 і подвоєними зсувами.

%2 = load ptr, ptr %0, align 4                      ; stack base
%3 = getelementptr inbounds nuw i8, ptr %0, i32 8
%4 = load i32, ptr %3, align 4                      ; current frame offset
%5 = getelementptr inbounds nuw [4 x i8], ptr %2, i32 %4
%6 = getelementptr inbounds nuw i8, ptr %5, i32 16  ; slot 0 (N)
%7 = getelementptr inbounds nuw i8, ptr %5, i32 8   ; header: resume index
%8 = load i32, ptr %7, align 4
%9 = icmp eq i32 %8, 0                              ; switch on resume
...
16:                                                 ; N > 0: call sum(N - 1)
  store i32 1, ptr %7, align 4                      ; resume = 1
  ...                                               ; x0 = N - 1
  %19 = tail call ptr @call(ptr %0, ptr @SUM)       ; push frame or park
  musttail call void %19(ptr nonnull %0)
  ret void
20:                                                 ; resume 1: x0 += N
  ...
  %24 = tail call ptr @leave(ptr %0)                ; pop, caller body
  musttail call void %24(ptr nonnull %0)
  ret void