Сколько процессов тянет планировщик flang
Миллион работающих процессов планировщик держит. Прогон заводит их за 11,6 секунды и 1,7 ГиБ, доставка при этом стоит 0,492 мкс, переключение при 960 000 одновременно готовых — около 11 мкс. Четыре миллиона тоже проверены: 39,87 секунды и 6 744 МиБ.
Что мерили
Модель конкурентности лежит в flang/conc/SPEC.md (210 КБ), flang/conc/RESILIENCE.md, flang/conc/DISTRIBUTED.md. Есть свидетельский планировщик на JavaScript и три напечатанных:
| Цель | Файл | Что это |
|---|---|---|
| C | flang/src/emit/c/flang_conc.c (3508 строк) | свой планировщик; рабочий режим — потоки берут процессы, у каждого поток свой склад готовых |
| Elixir | flang/src/emit/elixir/flang_conc.ex | процесс flang = процесс BEAM (use GenServer) |
| JavaScript | flang/src/emit/js/flang_conc.js (611 строк) | свой планировщик, один поток |
Остальные пять целей из восьми (Go, Rust, Python, Java, C#) программу с процессами печатать отказываются — код FLANG_CONC_UNSUPPORTED.
Процессы заводятся на ходу: слово породить есть в модели и в цели C (flang/src/emit/c/flang_conc.c:1390), у целей JavaScript и Elixir его нет. Поэтому «сколько процессов тянет планировщик» и «сколько процессов можно завести» — один вопрос, а не два: исходник стенда «рой» постоянного размера, 2 628 байт и два объявления, а миллион процессов он заводит прогоном.
Мерилось шесть вещей: память молчащего и работающего процесса, цена заведения, цена доставки сообщения, цена переключения, потолок печати и ядра. Всё, что здесь названо числом, — прогон, а не оценка по коду.
По умолчанию планировщик работает в один поток, и это нужно для сверки: единственный источник случайности — выбор процесса из очереди готовых, и он берётся из генератора, заведённого семенем. Так напечатанный C даёт побайтово тот же журнал доставок, что свидетель, и это проверяется тестами. Пул потоков включается полем workers в запросе — это второй режим, а не замена.
Как мерили
Замеры сняты 15–17 августа 2026 на машине с 256 процессорами и 499 ГБ памяти: gcc 15.2.0, Erlang/OTP 29 (erts-17.0.5, JIT), Elixir на нём же.
Машина всё время была занята чужой работой. Средняя нагрузка за минуту держалась между 125 и 734, на замерах пула гуляла от 60 до 1250 — то есть местами машина была перегружена втрое. Поэтому:
- время меряется процессорное (
/usr/bin/time), а не настенное; исключение — замеры пула, где сравнивается настенное, потому что меряется именно распараллеливание; - из повторов берётся минимум (помеха может замер только удлинить);
- цена одного пробега считается наклоном: один и тот же двоичник зовётся с двумя разными пределами пробегов, берётся разность. Всё, что не зависит от числа пробегов — загрузка программы, заведение процессов, печать итога, — из разности уходит целиком;
- два режима меряются чередуясь внутри повтора, а не блоками: измерив сперва все повторы одного, а потом все повторы другого, мы сравнили бы разные минуты, а не два режима;
- память считается по мелким промахам страниц (целое число, страница 4 КиБ) и точными байтами из valgrind, а не по одному лишь пиковому RSS: в этой среде RSS округляется до 2 МиБ и на мелких программах показывает ноль;
- у каждого прогона проверяется контрольная сумма: сумма поля «всего» по всем процессам равна числу пробегов, потому что каждый пробег прибавляет ровно единицу ровно одному процессу. Прогон, у которого она не сошлась, замером не считается;
- прежде чем сравнивать скорость двух сборок, сверяется вывод: на семенах 1, 7 и 4172, с журналом и без, ответ обязан совпасть побайтово (md5 всего ответа, включая журнал доставок). Иначе «быстрее» означало бы «считает другое».
Все числа времени — верхние оценки. На свободной машине они меньше.
Стенды для замера лежат в flang/conc/bench/, а работать надо в пустом рабочем каталоге: на миллионе объявленных процессов стенд — это 166 МБ исходника и 129 МБ двоичника.
Числа
Молчащий процесс: около 440 байт
Стенд «тихо»: N процессов объявлено, сообщение приходит одному, прогон кончается за один пробег. Пять повторов на каждое N; разброс по повторам — единицы страниц из тысяч, то есть меньше процента везде, кроме самой мелкой строки.
| Процессов | Промахов страниц | Байт на процесс | Пик RSS |
|---|---|---|---|
| 2 | 100–104 | — | ниже порога измерения |
| 1 000 | 202–208 | 427 | ниже порога |
| 10 000 | 1 079–1 081 | 401 | 2 МиБ |
| 100 000 | 9 847–9 850 | 399 | 42 МиБ |
| 1 000 000 | 97 551–97 552 | 399 | 450 МиБ |
Цена постоянна — 399 байт на слот и записи планировщика, от тысячи до миллиона. Из чего она складывается (размеры спрошены у компилятора, zamer/sizes.c): слот процесса 168 байт, плюс место в очереди готовых, признаках, дереве надзора и итоговых состояниях — 234 байта в памяти прогона; плюс 129 байт в самом двоичнике (строка имени и запись в таблице процессов).
Сверху лежит указатель имён, которым доставка ищет адресата, и он стоит 41 байт на молчащий процесс: на ста тысячах — 10 689–10 692 промаха страниц против 9 848–9 850 и пик RSS 47 140 КиБ против 43 020 КиБ. Из этих 41 восемь-десять байт сам указатель, шестнадцать — поле «каким куском покупать» в арене, остальное округление страниц. Итого около 440 байт на молчащий процесс.
Для сравнения, Erlang/OTP 29 на этой же машине: 2 648 байт на процесс (process_info(Pid, :memory), повторяется до байта), 2 651 байт по разности erlang:memory(processes) на миллионе процессов.
Работающий процесс: 1,5–1,8 КиБ
У каждого процесса своя куча, и куча из двух половин: после каждого пробега живое переезжает в свободную половину, занятая сбрасывается целиком. Сборщика мусора не нужно вовсе. Половина покупается куском в 512 байт (черновик пробега и почта остаются на 64 КиБ), и покупается в тот момент, когда процесс впервые что-то посчитал.
Стенд «ждут», 10 000 процессов, готовы всегда все, 100 000 пробегов, три повтора: пик RSS 14 336–16 384 КиБ, мелких промахов страниц 4 399–4 401, процессорное время 0,58–0,88 с. Это 1,5 КиБ на работающий процесс.
Точный прибор — счётчик malloc из valgrind, числа повторяются до байта:
| Стенд | Байт всего |
|---|---|
| два работающих процесса | 207 312 |
| прогон надзора на 20 семенах | 1 494 448 |
Адресное пространство — двоичным поиском по ulimit -v, стенд «рой», 100 000 процессов: 260 000 КиБ не хватает, 280 000 КиБ хватает — 2,87 КиБ на процесс. Тем же вторым прибором, по пиковому RSS на ста тысячах порождённых: 178 176 КиБ, то есть 1 825 байт на процесс.
Миллион работающих процессов — около трёх ГБ адресного пространства.
Точное сообщение об отказе при нехватке памяти — то самое, которое планировщик собирался сказать; на стенде «рой» под пределом ulimit -v выходит именно оно:
{"ok":false,"code":"FLANG_MEMORY","message":"кончилась память в планировщике конкурентности"}
Заведение: миллион процессов за 11,6 секунды
Стенд «рой»: один объявленный сервер порождает работников на ходу, исходник постоянного размера — 2 628 байт и два объявления, — а сколько процессов заведётся, решает предел пробегов запроса.
| Пробегов | Заведено процессов | Процессорное время | Пиковый RSS | Байт на процесс |
|---|---|---|---|---|
| 2 000 | 1 001 | 0,00 с | ниже порога | — |
| 20 000 | 10 002 | 0,08 с | 18 МиБ | 1 887 |
| 200 000 | 100 000 | 0,95 с | 176 МиБ | 1 804 |
| 600 000 | 300 001 | 3,44 с | 571 МиБ | 1 950 |
| 2 000 000 | 1 000 000 | 11,61 с | 1 722 МиБ | 1 764 |
| 8 000 000 | 4 000 000 | 39,87 с | 6 744 МиБ | 1 768 |
Цена на процесс постоянна от ста тысяч до четырёх миллионов: 1 804 / 1 950 / 1 764 / 1 768 байта. Значит потолка здесь нет вовсе — есть память машины и предел FL_CONC_MAX_PROCESSES, который задаётся запросом.
1 764 байта на работающий процесс против 2 648 у Erlang/OTP 29.
Поправка от 20 августа: 1 948 байт, а не 1 764, и код тут ни при чём. Перепроверка тем же стендом дала 1 948 байт на процесс при миллионе и 1 948 при четырёх миллионах — три прогона подряд, разброс ноль (1 902 592 КиБ при миллионе). Число процессов и время воспроизвелись точно: 4 000 000 процессов за 39,51 с против записанных 39,87 с.
Чтобы понять, регрессия это или запись, стенд собран и померен НА ТОМ САМОМ коммите, где число 1 764 было написано (32475889, отдельная рабочая копия): 1 946 / 1 948 / 1 946 байта, то есть ровно столько же, сколько на сегодняшнем дереве. Значит код не менялся, а число не воспроизводится. Проверены и отброшены две догадки: прозрачные большие страницы (режим madvise, отключение glibc.malloc.hugetlb не меняет ничего) и предел числа процессов в запросе (2 / 4 / 8 миллионов дают 1 902 592, 1 900 544 и 1 894 400 КиБ — разброс в обратную сторону).
Вывод, который стоит записать честно: 1 764 было снято при условиях, которых сегодня не воспроизвести, и цена на процесс — около 1 950 байт. Против 2 648 байт у Erlang/OTP 29 это по-прежнему в полтора раза дешевле, и вывод таблицы не меняется; меняется третий знак.
Порождение стоит около 11,6 мкс на процесс (два пробега: сервер порождает, работник просыпается). Заведение при старте прогона, когда процессы объявлены в исходнике, дешевле — на него приходятся не три обращения к malloc (имя и две половины кучи), а ни одного лишнего:
| Процессов | Время |
|---|---|
| 100 000 | 0,10–0,11 с |
| 1 000 000 | 1,05–1,11 с |
Наклон: 1,04 мкс на процесс → около 960 000 процессов в секунду. Erlang на этой же машине: 200 000 spawn подряд, минимум из пяти повторов 415 938 мкс → 480 841 процесса в секунду.
Доставка: 0,492 мкс при миллионе процессов
Стенд «пингконец»: два процесса разговаривают, а объявлены они последними, поэтому адресат ищется в самом неудобном месте таблицы. Наклон между 20 000 и 120 000 пробегами, минимум из пяти повторов; строка «1 000 000» снята наклоном между 100 000 и 4 100 000.
| Объявлено процессов | мкс на доставку |
|---|---|
| 2 (стенд «пинг») | 0,477 |
| 1 000 | 0,600 |
| 10 000 | 0,600 |
| 100 000 | 0,600 |
| 1 000 000 | 0,492 |
Линия ровная. Миллион объявленных процессов не стоит ничего: 0,492 мкс против 0,477 мкс на двух процессах — разница внутри разброса прибора.
2,03 миллиона сообщений в секунду при миллионе процессов против 1,53 миллиона у Erlang/OTP 29 на этой же машине (миллион кругов пинг-понга, минимум 1 311 185 мкс → 762 669 кругов в секунду).
Адресат ищется указателем имён (открытая адресация, FNV-1a, номер uint32_t), который планировщик строит один раз при старте прогона и дополняет на каждом порождении. Перебор таблицы вместо указателя делает прогон квадратичным по числу процессов: двойник стенда «рой», у которого поиск заменён на цикл по таблице и всё остальное байт в байт то же самое, отстаёт в 37 раз уже на 80 000 заведённых процессах — 30,51 с против 0,82 с.
Переключение: около 11 мкс при миллионе одновременно готовых
Очередь готовых — дерево частичных сумм (Фенвика) над признаком «готов» (fl_conc_rank_add и fl_conc_select в flang/src/emit/c/flang_conc.c). «Стал готов / перестал» и «кто k-й» стоят O(log P), «сколько готовых» — O(1), памяти ровно по одному size_t на процесс.
Наружу видно не хранилище, а две функции, и обе видны в свидетеле буквально:
- СКОЛЬКО процессов готово — на это умножается число из семени;
- КТО k-й ПО ВОЗРАСТАНИЮ НОМЕРА среди готовых — его и берут.
Ни порядок в памяти, ни способ его держать наружу не видны ничем, поэтому дерево даёт побайтово тот же журнал: сверено на семенах 1, 7 и 4172, с журналом и без, и на прогоне в 1 280 000 пробегов.
Стенд «ройждут»: работник откладывает своё сообщение и потому готов всегда, а на первом пробеге заводит двоих таких же — число готовых растёт линейно по пробегам. Процессорное время, минимум из повторов:
| Пробегов | Готовых, примерно | Секунд |
|---|---|---|
| 20 000 | 15 000 | 0,11 |
| 40 000 | 30 000 | 0,25 |
| 80 000 | 60 000 | 0,54 |
| 160 000 | 120 000 | 1,16 |
| 320 000 | 240 000 | 2,60 |
| 640 000 | 480 000 | 5,22 |
| 1 280 000 | 960 000 | 13,83 |
При удвоении пробегов столбец множится на 2,3 / 2,2 / 2,1 / 2,2 / 2,0 / 2,7 — то есть линейно, как ему и положено. Наклон по соседним точкам — около 0,009 нс на готовый процесс. Нулём он не стал и стать не мог: дерево на миллионе узлов — восемь мегабайт, верхние его уровни в кэш не помещаются, так что O(log P) обращений — это O(log P) промахов.
Миллион одновременно готовых процессов — около 11 мкс на переключение (13,83 секунды на 1 280 000 пробегов).
Ядра: столько, сколько попросят
Число потоков задаётся полем workers в запросе. Пул не будит НИКОГО: положивший процесс на склад никого не будит, спящий поток просыпается сам и обходит склады заново.
Стенд «пачка»: N/2 пар, и внутри пары письмо уходит соседу не на каждом пробеге, а на каждом K-м; между передачами процесс пишет САМ СЕБЕ, то есть остаётся у того же потока. Один двоичник на каждое K, всё остальное — то же самое. Числа — во сколько раз рабочий режим быстрее проверочного, настенное время, минимум из пяти повторов.
Дешёвый обработчик (0,73 мкс на пробег), 4 000 000 пробегов, 1024 процесса:
| K | 1 поток (сторож) | 8 потоков | 64 потока |
|---|---|---|---|
| 1 | 0,99× | 2,91× | 1,85× |
| 2 | 1,01× | 4,40× | 3,22× |
| 4 | 0,86× | 5,75× | 5,20× |
| 8 | 0,97× | 6,61× | 8,82× |
| 16 | 1,05× | 7,50× | 14,95× |
| 32 | 1,01× | 8,07× | 23,23× |
| 64 | 1,00× | 8,47× | 33,91× |
Первый столбец — сторож прибора: workers: 1 это тот же проверочный режим, тот же двоичник, одно лишнее поле в запросе. Он обязан дать единицу и даёт её с разбросом ±15 % — вот шум этой машины, названный числом.
Дорогой обработчик (1,80 мкс на пробег, свёртка по списку из 32 чисел), 400 000 пробегов:
| K | 8 потоков | 64 потока |
|---|---|---|
| 1 | 4,80× | 4,25× |
| 2 | 5,07× | 7,78× |
| 4 | 5,92× | 11,83× |
| 8 | 6,64× | 18,00× |
| 16 | 6,55× | 17,75× |
| 32 | 7,00× | 24,33× |
| 64 | 8,00× | 18,25× |
Последняя клетка (18,25× там, где рядом 24,33×) — не находка, а предел прибора: рабочий прогон там занимает 0,03–0,04 секунды.
Порог посчитан был K > 6, измерен K = 1. Уже при одном пробеге на передачу пул быстрее в 1,85–4,80 раза, смотря по обработчику, и дальше растёт с K почти линейно.
Считанный порог брался из цены передачи работы потоку через mutex с условной переменной — 9,43 мкс на 256 ядрах против 1,76 мкс на пробег. В этом пуле такого числа просто нет: он никого не будит, поэтому мерить пришлось, а не верить.
Настоящий порог — не K, а сколько процессов готовы ОДНОВРЕМЕННО. Стенд «пинг»: два процесса перекидывают одно письмо, готов в каждый момент РОВНО ОДИН. Два миллиона пробегов, настенное время, минимум из пяти повторов:
| потоков | настенное, с | раз | процессорного |
|---|---|---|---|
| 1 | 1,02 | 1,00× | 100 % |
| 2 | 1,46 | 0,70× | 183 % |
| 4 | 2,18 | 0,47× | 391 % |
| 8 | 4,52 | 0,23× | 749 % |
| 16 | 6,98 | 0,15× | 1503 % |
На программе без параллелизма пул медленнее в 6,7 раза и жжёт при этом пятнадцать ядер. Потоку, которому нечего взять, положено покрутиться и уснуть, и шестнадцать таких потоков вокруг одного готового процесса — это шестнадцать обходов складов на каждый пробег.
Порог между крайностями измерен прямо (K = 8, восемь потоков, 2 000 000 пробегов, меняется только число процессов в стенде):
| Процессов объявлено | готовы одновременно | раз |
|---|---|---|
| 4 | 2 | 1,28× |
| 16 | 8 | 5,50× |
| 64 | 32 | 5,58× |
| 256 | 128 | 6,38× |
| 1024 | 512 | 6,96× |
Пул окупается начиная примерно с одного готового процесса на поток и дальше почти не растёт. Правило для заказчика: просить потоков не больше, чем у программы бывает одновременно готовых процессов.
Где потолок: две стенки печати и цепочка компиляции
Стенка 1 — печать сообщений прогона. Около 31 350 сообщений в одном дано. Найдено двоичным поиском, каждый шаг — настоящая печать. Массив из четырёх строк на каждое сообщение раскладывается в аргументы вызова, и на тридцати одной тысяче сообщений кончается стек печатника. Граница гуляет на пару единиц от прогона к прогону: первый поиск дал «31 349 держит, 31 350 падает», повторный — «31 348 держит, 31 349 падает», а проверочный запуск того же 31 349 прошёл. Сколько именно аргументов влезет, зависит от того, сколько стека осталось к моменту вызова, а это меняется от состояния кучи. Считать надо около 31 350, и не строить планов на «31 349 точно можно».
Стенка 2 — компилятор C на той же функции. Входные сообщения прогона печатаются одной функцией C с локальной переменной на каждое сообщение. gcc -O2 на ней растёт быстрее линейного:
Сообщений в дано | Время cc -O2 |
|---|---|
| 1 000 | 7,8 с |
| 10 000 | 112,6 с |
| 31 000 | 1 302,9 с (21,7 минуты) |
Обе стенки стоят на двери снаружи: столько сообщений можно передать в прогон строками дано. Изнутри прогона порождённым процессам сообщения раздаются без всякого дано, поэтому до этих стенок доходит не всякая программа.
Стенка 3 — цепочка компиляции, если процессы всё же объявлять в исходнике. Миллион объявлений:
| Шаг | Время |
|---|---|
| исходник (166 байт на процесс) | 166 МБ на диске |
| разбор | 28,1–29,5 с |
| проверка типов | 3,1 с (нуль диагностик) |
| печать в C | 6,2–7,2 с |
cc -O2 (54 МБ кода C) | 24,0–30,9 с |
| двоичник | 129 МБ |
Итого около 66 секунд, то есть примерно 15 000 процессов в секунду через всю цепочку. На ста тысячах те же шаги дают 3,4 с + 0,38 с + 0,68 с + 4,4 с — всё растёт линейно. Стенд «рой» этой цепочки не касается вовсе: 2 628 байт исходника независимо от числа процессов.
Предел пробегов — умолчание, а не потолок: FL_CONC_MAX_TURNS (flang/src/emit/c/flang_conc.h:178) останавливает прогон после десяти тысяч пробегов с исходом "предел пробегов", и задаётся полем turns в запросе.
Другие цели
Elixir — единственная цель, где процесс flang становится настоящим процессом операционной системы языка: use GenServer, дерево Supervisor. Значит там работают все свойства BEAM — вытеснение, настоящая параллельность, миллион процессов по 2,7 КБ. Платится за это скоростью: тот же пинг-понг даёт около 16 000 сообщений в секунду при 222–242 % процессора, потому что его исполняет BEAM. У напечатанного Elixir каждый пробег ведёт учёт в общей таблице ETS, а прогонщик опрашивает состояние покоя раз в две миллисекунды.
JavaScript — планировщик есть (flang/src/emit/js/flang_conc.js), здесь не мерен.
Go, Rust, Python, Java, C# — процессов нет, печать отказывает кодом FLANG_CONC_UNSUPPORTED.
Что из этого складывается
| Что | Сколько | Чем ограничено |
|---|---|---|
| молчащих процессов | 1 000 000 проверено | цепочкой компиляции, если объявлять в исходнике |
| работающих процессов, заведённых прогоном | 4 000 000 проверено | памятью машины и FL_CONC_MAX_PROCESSES |
| одновременно готовых процессов | 960 000 проверено | — |
| процессов, которым можно послать письмо снаружи | ~31 350 | падением печати |
| сообщений в секунду при миллионе процессов | 2,03 млн | — |
| байт на работающий процесс | 1 764 | — |
| ядер | сколько попросят (workers) | числом одновременно готовых процессов |
Чем это ограничено
- Свободной машины не было ни разу. Нагрузка 125–734 при 256 ядрах, на замерах пула 60–1250. Все числа времени — верхние оценки, и на пустой машине они будут меньше.
- Цель JavaScript не мерена вовсе. Планировщик там есть, руки не дошли.
- Цель Elixir мерена только по пропускной способности. Память на процесс там меряется через
erlang:memory, а не через RSS, и это отдельная работа; голая BEAM на этой машине даёт 2 648 байт на процесс, и цель Elixir от этого числа далеко уйти не может — процесс flang там и есть процесс BEAM. - Порождение есть только у свидетеля и у цели C. Планировщики JavaScript и Elixir отвечают на неизвестное действие названной ошибкой, а не молча делают не то; печать программы с
породитьв эти цели не запрещена, и это записанный долг, а не недосмотр. - Надзор, перезапуск, отказы и таймеры не мерены. Мерилось то, что спрошено: память, заведение, доставка, потолок, переключение, ядра.
- Четыре прежние стенки сняты и в счёт больше не идут: перебор таблицы имён при доставке, кусок кучи в 64 КиБ на процесс, отсортированный массив готовых и одно-единственное ядро.
Что менять, чтобы вышли миллионы
Осталось два пункта, и оба — в печати.
1. Убрать спред при печати сообщений (~10 строк)
В печатнике цели C массив строк на каждое сообщение раскладывается в аргументы вызова: lines.push(...body) и соседний ...inbox.map(...). Заменить на цикл. Снимает стенку в ~31 350 сообщений полностью.
2. Разбить входные сообщения на несколько функций (~30 строк)
Там же: сейчас все сообщения прогона печатаются одной функцией C, и gcc на ней захлёбывается. Резать по 500 сообщений в функцию. Снимает двадцатиминутную сборку.
Как повторить
Этот раздел — для тех, кто развивает язык. Команды ниже — команды репозитория, а не команды языка: стенды собираются и меряются скриптами из дерева flang, и путь к ним надо знать.
Стенды и скрипты — flang/conc/bench/. Локаль в среде сломана, перед node нужен LC_ALL=C.UTF-8.
Работать надо в пустом каталоге, а не в дереве: на миллионе объявленных процессов стенд это 166 МБ исходника и 129 МБ двоичника. Скрипты кладут и ищут сборки в текущем каталоге (можно переопределить ZAMER=), а само дерево находят сами.
Z=/tmp/zamer && mkdir -p $Z && cd $Z
F=/путь/к/flang/conc/bench
# 1. Собрать стенд: вид (тихо|ждут|цепь|пинг|пингконец|рой|ройждут|пары|пачка) и размеры N
bash $F/assemble.sh тихо 2 1000 10000 100000 1000000
# 2. Память покоя и время: пять повторов на стенд
bash $F/memory.sh b-тихо-2 b-тихо-1000 b-тихо-10000 b-тихо-100000 b-тихо-1000000
# 3. Цена одного пробега: наклон между двумя пределами пробегов
bash $F/assemble.sh пинг 0
bash $F/slope.sh b-пинг-0 1000000 16000000 7 0
# 4. Цена доставки против числа объявленных процессов
bash $F/assemble.sh пингконец 1000 10000 100000
bash $F/delivery.sh 20000 120000 b-пингконец-1000 b-пингконец-10000 b-пингконец-100000
# 5. Порождение: сколько процессов заводит ПРОГОН
bash $F/assemble.sh рой 1
bash $F/how-many.sh b-рой-1 2000 20000 200000 2000000
# 6. Переключение против числа ОДНОВРЕМЕННО ГОТОВЫХ
bash $F/assemble.sh ройждут 1
REP=3 bash $F/swarm.sh b-ройждут-1 20000 40000 80000 160000 320000 640000 1280000
# 7. Двойник: тот же стенд с одной изменённой строкой рантайма
bash $F/twin.sh перебор b-рой-1 b-ройперебор-1
REP=5 bash $F/swarm.sh b-ройперебор-1 20000 40000 60000 80000 160000
# 8. Точные байты malloc на процесс
bash $F/valgrind.sh b-ждут-1000 5000
# 9. Во что упирается при нехватке памяти (двоичный поиск по ulimit -v, КиБ)
bash $F/assemble.sh цепь 10000
bash $F/limit.sh b-цепь-10000 20000 1450000 1460000
# 10. Течёт ли со временем
bash $F/leak.sh b-ждут-10000 100000 400000 1600000 6400000
# 11. Ядра: пул против проверочного режима
for k in 1 2 4 8 16 32 64; do K=$k bash $F/assemble.sh пачка 1024; done
for k in 1 2 4 8 16 32 64; do K=$k TYAZH=32 bash $F/assemble.sh пачка 1024; done
REP=5 bash $F/threshold.sh 4000000 8 1 2 4 8 16 32 64
REP=5 TYAZH=32 bash $F/threshold.sh 400000 8 1 2 4 8 16 32 64
# 12. Где пул не окупается
bash $F/assemble.sh пинг 16
REP=5 bash $F/by-cores.sh b-пинг-16 2000000 1 2 4 8 16
# 13. Где падает печать (двоичный поиск, каждый шаг — настоящая печать)
LC_ALL=C.UTF-8 node $F/stenka.mjs 1000 100000
# 14. Что стоит проверка типов
LC_ALL=C.UTF-8 node $F/tipy.mjs тихо-1000000.flang
# 15. Размеры записей планировщика — у компилятора, а не на глаз
cp $F/sizes.c b-тихо-2/ && cc -O2 -std=c99 -Ib-тихо-2 b-тихо-2/sizes.c -o /tmp/razmery -lm && /tmp/razmery
# 16. Точка отсчёта на настоящей BEAM
erlc -o $Z $F/beam.erl
erl +fnu -noshell -pa $Z +P 20000000 -s beam start -s init stop