The Jerusalem PostJordan and Syria tout close ties, but border smuggling and strikes tell another story – analysisPunchNigerians must work towards building one prosperous country — DogaraDaily MaverickPOLITICIANS PUMMELLED: SA’s path to ‘no future’ — former statistician-general Lehohla slams ‘performative’ politicsZDF heuteEntdecken Sie das ZDF-NachrichtenstudioCBS NewsStevens endorses Rodgers amid rift with El-SayedQuemVirginia Fonseca diz que não tira música do ex da cabeça: 'Do nado me pego cantando'The South AfricanDa Cruz has a plan for Kaizer Chiefs’ biggest problemIl Fatto QuotidianoL’Anm sulla norma inserita in Manovra: “Tutela stipendi delle toghe è condizione per la salvaguardia dell’indipendenza”Futura SciencesUn morceau de viande oublié dans un frigo : comment Pilar Granado en a fait une invention contre le gaspillageHindustan Times Sport'Is Ashton Jeanty injured?' Fantasy managers concerned by Raiders RB's slow start against Jetsالشرقترمب يشيد بتأييد الديمقراطية ستيفنز لروجرز في مواجهة عبدول السيدХабрНашел золотую середину LLM в приватность/цена/качество? Прогнал два Qwen на российском (!) API: сколько стоит моя задача
The Daily Newsstand · Free, Always
Sunday, November 1, 2026

Ты не бойся, это Goose

Translate

Введение

Язык и компилятор Goose спроектировал Wouter van Oortmerssen. Согласно странице проекта, реализация компилятора, бенчмарки, примеры и документация созданы с помощью ИИ-модели Claude Fable. В чём автор был замечен ранее: ЯП Lobster, разработка которого началась в 2010 году, и библиотека сериализации FlatBuffers, опубликованная Google в 2014-м.

Бэкграунд у автора вполне солидный, посмотрим же на “гуся” в деталях.

Основная фишка Goose связана с управлением памятью. К системе управления памятью можно предъявить три нефункциональных требования на букву “Б”:

  1. Безопасно

  2. Без затей: управление памятью не требует сложных действий типа явных аннотаций времени жизни

  3. Быстро: выделение и освобождение памяти требуют минимальной и предсказуемой работы во время выполнения

В грубом приближении некоторые языки, с которыми я знаком, расставляют акценты так:

Язык

Безопасно

Без затей

Быстро

Go

◐

⬤

◔

JavaScript

◐

⬤

◔

Dart

⬤

⬤

◔

Rust

⬤

◯

⬤

⬤ – да; ◐ – с оговорками; ◔ – лишь частично; ◯ – нет.

Пояснения к таблице:

  • Go и JavaScript: без затей и с базовыми гарантиями безопасности памяти. В обычном Go-коде без unsafe и в управляемой памяти JavaScript исключены проблемы типа dangling pointers и use-after-free, но в обоих языках возможны гонки данных: в Go – между goroutines, работающими с общей памятью, в JavaScript – между agents (например, Web Workers), обращающимися к одному SharedArrayBuffer. Выделение памяти в системах с GC обычно дёшево, но поиск и освобождение недостижимых объектов расходуют процессорное время и могут создавать latency spikes. То есть требование 1 выполняется частично, 2 – да, 3 – с проблемами.

  • Dart: без затей и с гарантиями. Гонки данных над управляемой памятью в чистом Dart-коде исключены моделью isolates. Понятно, эта гарантия не распространяется на FFI и JavaScript-интероп. Есть GC, со всеми плюсами и минусами. То есть требования 1 и 2 выполняются, а 3 – частично.

  • Rust: safe Rust обеспечивает безопасность памяти и исключает гонки данных, при этом работает без GC. За статические гарантии приходится платить “магическими рунами” в тексте: fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() >= b.len() { a } else { b } }. Ещё больше “затей” возникает с замыканиями; в своё время я разбирал эту тему и предлагал упрощение. Выходит, что требования 1 и 3 выполняются, а 2 – нет.

Тут напрашивается очередная неформальная трилемма: “одновременно можно получить максимум два качества из трёх”.

Goose пытается сломать этот шаблон, предлагая все три качества и даже добавляя четвёртое: “Бережно”, то есть с минимальными расходами памяти на служебные данные.

Как это работает

Проще всего представить управление памятью Goose как арены. Кто писал, к примеру, на Go или Rust, тот знает про такую концепцию. Данные с общим временем жизни здесь освобождаются вместе. Каждая такая логическая арена может использовать несколько data stacks – независимых линейных аллокаторов. У каждого стека есть курсор выделения top, то есть bump-указатель на следующий свободный байт. Компилятор заранее вычисляет, сколько data stacks одновременно понадобится программе.

Концептуально передача этого числа в runtime выглядит так:

// Компилятор рассчитал, что программе нужны три data stacks.
const int64_t N = 3;

// Runtime создаёт для них память.
gs_stack_block(N);

Runtime получает готовое число и резервирует по одной области памяти для каждого data stack (код сильно упрощён):

static void gs_stack_block(int64_t n) {
    // Создаём N описателей. Сама память стеков здесь ещё не резервируется.
    gs_stks = calloc(n > 0 ? n : 1, sizeof(gs_stack));

    // Для каждого data stack резервируем отдельную область памяти.
    for (int64_t i = 0; i < n; i++) {
        gs_stks[i].top = gs_reserve_region(&gs_stks[i]);
    }

    gs_nstks = n;
}

Итого: компилятор считает N, а runtime N раз вызывает gs_reserve_region. Эта функция резервирует большой диапазон виртуальных адресов: в Linux через mmap, в Windows через VirtualAlloc. По умолчанию на каждый data stack резервируется 2 ГБ адресного пространства. Реальная память подключается по мере использования. Guard-область в конце останавливает переполнение. Исходный код runtime.

Выделение памяти внутри data stack – это запись по адресу top и сдвиг top вперёд. Например, a сразу создаётся литералом, b строится циклом, а затем растёт c:

fn make_b() -> i32[] {
    var result: i32[>..] = [];
    for i: i32 in 5 {
        result.push(i);
    }
    return result;
}

fn main() {
    var a: i32[] = [1, 2, 3];
    var b: i32[] = make_b();
    var c: i32[>..] = [];

    for i: i32 in 1000 {
        c.push(i);
    }

    print(a.len, b.len, c.len);
}

Внутри make_b массив result растёт обычным циклом. Компилятор строит возвращаемый i32[] прямо на месте b, без промежуточной копии. Готовый b больше не resizable, поэтому поверх него можно разместить растущий c:

data stack 0: [ a ][ b ][ c растёт............. ] -> top

Массив c находится на вершине стека, поэтому может расти простым сдвигом top и никуда не переезжает. Правило Goose: на одном стеке может находиться только один “живой” resizable-массив. Если одновременно живут три таких массива, компилятор использует три стека:

var a: i32[>..] = [];
var b: i32[>..] = [];
var c: i32[>..] = [];

for i: i32 in 1000 {
    a.push(i);
    b.push(i);
    c.push(i);
}
data stack 0: [ a растёт................ ] -> top
data stack 1: [ b растёт................ ] -> top
data stack 2: [ c растёт................ ] -> top

Итого: Goose заранее рассчитывает необходимое число data stacks, быстро наращивает их bump-указатели и массово освобождает данные вместе с областью видимости. Формальные правила описаны в спецификации Goose.

Где компилятор бьёт по рукам

За простую модель памяти приходится платить ограничениями. Ниже – несколько характерных примеров.

Растущий массив должен быть последним полем обычной структуры. Иначе его рост затрёт следующее поле:

struct S {
    tail: i64[>..],
    after: i64,
}

error: resizable field tail of struct S must be the final field

Тест компилятора.

Ссылка или срез не может пережить данные, на которые указывает:

fn bad() -> i64[:] {
    var local: i64[1] = [42];
    return local[..];
}

error: returning a reference rooted in local, which dies with this function

Тест компилятора.

Нельзя уменьшать массив, пока ещё используется срез его данных:

var buf: u8[>..] = [65];
let s: const u8[:] = buf;
buf.pop();
print(s);

error: cannot pop buf while s is still used

Тест компилятора.

Наконец, нельзя повторно растить массив, пока на его вершине ещё строится новый элемент:

fn make(words: u8[][>..]&) -> u8[] {
    words.push("side effect");
    return "result";
}

var words: u8[][>..] = [];
words.push(make(words));

error: cannot call make, which grows words: the element pushed into words is still under construction, and the growth would land inside it

Тест компилятора.

За что боролись (магия)

Ниже – бенчмарки, в которых самый быстрый вариант Goose под Clang опередил и лучший safe Rust, и оптимизированный вручную C++ больше чем на 10%. Время измерено на размере large; память – пиковый рабочий набор (peak working set) той же реализации.

В таблице скорости gap показывает, во сколько раз Goose быстрее.

Бенчмарк

Goose, мс

Expert C++, мс

Gap

Safe Rust, мс

Gap

records

441,0

625,5

1,42×

891,8

2,02×

interp

67,8

93,8

1,38×

87,5

1,29×

words

1199,5

1987,8

1,66×

2084,0

1,74×

bintrees

203,8

406,9

2,00×

235,4

1,16×

В таблице памяти gap показывает, во сколько раз рабочий набор Goose меньше.

Бенчмарк

Goose, МБ

Expert C++, МБ

Gap

Safe Rust, МБ

Gap

records

149,0

556,9

3,74×

593,4

3,98×

interp

28,0

63,4

2,26×

57,0

2,04×

words

535,6

638,4

1,19×

675,9

1,26×

bintrees

31,8

35,8

1,13×

31,5

на 1% больше

Под expert C++ автор понимает не особые флаги компилятора, а вручную оптимизированное представление данных: арены с индексами вместо отдельных объектов, tagged union вместо иерархии с виртуальными методами, собственную open-addressing таблицу вместо unordered_map. Для Rust отдельной категории expert нет: все реализации остаются safe, но при наличии нескольких вариантов для сравнения выбирается самый быстрый. Поэтому быстрый Rust здесь тоже не всегда “наивный”: для деревьев это рекомендуемая Vec-арена с индексами u32, а для words – написанная вручную open-addressing таблица. Rust собран с -O -C codegen-units=1. Описание вариантов Rust.

  • records строит журнал из 16 миллионов variant-записей и четыре раза его обходит. Goose использует встроенный variable-size enum: каждый элемент занимает место только своего варианта, а короткий текст хранится inline. Expert-вариант C++ вручную заменяет std::string встроенным буфером, а Rust использует обычные enum и String; при этом каждый элемент Rust всё равно имеет размер крупнейшего варианта. Здесь выигрыш Goose получается из возможностей языка, без собственного аллокатора или ручной упаковки записей.

  • interp строит дерево выражений и восемь раз его вычисляет. Goose хранит листья и внутренние узлы разного размера в одном пуле и связывает их типизированными относительными ссылками. Быстрые C++ и Rust тоже переходят на арену, но хранят узлы фиксированного максимального размера и связывают их числовыми индексами. Пул в Goose указан явно, зато код не реализует индексацию и проверку принадлежности ссылок вручную.

  • words считает частоты в тексте из 16 миллионов слов. Здесь важно не преувеличивать: самый быстрый вариант Goose, как и варианты C++ и Rust, содержит написанную вручную open-addressing таблицу. Однако вариант Goose со стандартным dictionary всё равно показал 1473,4 мс и 423,6 МБ – быстрее приведённых в таблице C++ и Rust и экономнее по памяти. То есть ручная оптимизация увеличила преимущество, но не создала его с нуля.

  • bintrees многократно строит и выбрасывает бинарные деревья. Во всех трёх быстрых вариантах применяется арена. В Rust это Vec с индексами u32 и ручным clear(), в C++ – vector-арена, а в Goose – локальный массив с типизированными ссылками, который освобождается сбросом top при выходе из функции. Goose всё ещё требует выбрать структуру в виде пула, но не требует писать отдельную схему владения и освобождения каждого узла.

Goose часто выражает плотную компоновку, арены и ссылки между их элементами непосредственно средствами языка. Там, где в C++ и safe Rust приходится вручную переходить к буферам, индексам и специальным контейнерам, Goose требует заметно меньше вспомогательного кода. Бенчмарки отобраны только по скорости, а таблица памяти показывает даже и проигрыш: в bintrees Goose занял 31,8 МБ против 31,5 МБ у Rust.

Саморазоблачение (бенчмарки)

Тут надо сразу отметить, что автор Goose добросовестно указывает на проигрыши Goose, шум и ограничения методики. Поэтому “разоблачение” здесь относится скорее к текущему “информационному шуму” вокруг Goose.

  • В таблицы выше сознательно вошли только четыре убедительных выигрыша Goose, а не весь набор из 16 бенчмарков. Они показывают задачи, на которых модель памяти Goose раскрывается лучше всего, но не среднюю производительность языка.

  • Обобщённая таблица автора охватывает все 16 бенчмарков. В ней итоговые показатели скорости и памяти рассчитаны независимо и могут относиться к разным реализациям Goose, поэтому их нельзя воспринимать как характеристики одной программы.

  • Все измерения выполнены на одном компьютере без закрепления процесса за ядрами. Сам автор считает шумом различия менее чем в 10%, а для graph и lru – менее чем в 15%. Поэтому небольшие преимущества в отдельных строках нельзя считать доказанным превосходством языка.

  • Goose выигрывает не везде: calc быстрее на Rust, lru проигрывает Rust при компиляции через MSVC и выигрывает через Clang, а scene проигрывает под MSVC и примерно равен Rust под Clang. Плоские численные вычисления в particles практически одинаковы во всех трёх языках.

Цена вопроса: замыкания

В Goose можно передать именованную функцию:

fn each_pair<T, F>(xs: T[:]) {
    for i in 0..xs.len - 1 {
        F(xs[i], xs[i + 1]);
    }
}

fn print_pair(a: i64, b: i64) {
    print(a, " ", b);
}

fn main() {
    var xs: i64[] = [4, 8, 15, 16, 23, 42];
    each_pair(xs, print_pair);
}

F связывается с print_pair на этапе компиляции. Компилятор создаёт специализацию each_pair для этой функции, а вызов F(...) превращает в прямой вызов или встраивает в цикл. Function values существуют только во время компиляции: это не closure-объекты и не указатели на функции.

Вложенные функции и блоки могут захватывать локальные переменные, но не могут покинуть функцию, в которой объявлены. Function value нельзя сохранить в переменной или структуре, положить в массив, выбрать через if или вернуть из функции. Поэтому обычную фабрику замыканий написать не получится:

fn make_counter() {
    var n = 0;

    fn next() -> i64 {
        n++;
        return n;
    }

    return next;
}

error: function values cannot be returned

Цена такой модели – нельзя создавать долгоживущие callbacks, хранить обработчики событий или выбирать реализацию во время выполнения. Зато для map, filter, sort и похожих функций не нужны closure-объекты, косвенные вызовы и дополнительные аллокации. Подробнее это описано в спецификации Goose; ошибка проверяется отдельным тестом компилятора.

Цена вопроса: передача данных в потоки

Goose исключает data races в чистом коде весьма нехитрым способом: у потоков нет общей изменяемой памяти. Функция, объявленная как thread_fn, вместе со всем вызываемым ею кодом компилируется как отдельная программа со своими data stacks и собственными копиями глобальных переменных.

Например, worker увеличит только свою копию counter, поэтому main по-прежнему напечатает 0:

var counter = 0;

thread_fn increment() {
    counter++;
}

fn main() {
    thread_wait(thread_spawn(increment));
    print(counter); // 0
}

Задания и результаты передаются через типизированные очереди qput<T> и qget<T>. Через очередь могут проходить только плоские значения без ссылок и срезов: они лежат в памяти непрерывно и копируются одним memcpy. Поэтому языку не нужны locks, atomics и memory ordering. Подробнее это показано в документации Goose.

struct Job { n: i32 }
struct Result { n: i32 }

thread_fn worker() {
    let job = qget<Job>();
    qput(Result { n: job.n * 2 });
}

fn main() {
    let id = thread_spawn(worker);
    qput(Job { n: 21 });
    let result = qget<Result>();
    thread_wait(id);
    print(result.n); // 42
}

Цена такой гарантии – нельзя разделить между потоками изменяемый кеш или граф и нельзя передать ссылку на уже построенную структуру. Например, такой код не скомпилируется:

thread_fn worker(value: i64&) {
    print(value);
}

fn main() {
    var value = 42;
    thread_spawn(worker, &value);
}

error: thread_fn parameters must be flat, not i64&

Тест компилятора.

Цена вопроса: индексирование массивов

Если элементы имеют разный размер, у массива нет фиксированного шага. Чтобы найти entries[i], нужно прочитать размеры всех предыдущих элементов:

struct Entry {
    text: u8[],
}

fn main() {
    var entries: Entry[>..] = [];
    entries.push(Entry { text: "a" });
    entries.push(Entry { text: "much longer" });

    for entry in entries { print(entry.text); } // работает
    print(entries[1].text);                      // ошибка
}

error: arrays of variable-size elements cannot be indexed, only iterated; for random access, keep your own array of references to them

Тест компилятора.

Goose не создаёт скрытую таблицу смещений: массив с элементами разного размера можно последовательно обходить, но нельзя индексировать. Если нужен произвольный доступ, индекс нужно поддерживать явно в виде второго массива ссылок:

struct Entry {
    text: u8[],
}

fn main() {
    var entries: Entry[>..] = [];
    var entry_at: (Entry&)[>..] = [];

    entry_at.push(entries.push(Entry { text: "a" }));
    entry_at.push(entries.push(Entry { text: "much longer" }));

    print(entry_at[1].text); // "much longer"
}

Таким образом, данные остаются плотно упакованными, а entry_at[i] обеспечивает произвольный доступ.

Официальный пример с четырёхбайтовым индексом.

Итак, переходим на Goose?

Вынесем за скобки зрелость проекта. Стоит ли переходить, да или нет? Нет. Или да. Зависит от контекста.

Преимущества Goose лучше всего раскрываются в программах, где данные создаются группами, некоторое время обрабатываются, а затем освобождаются целиком, как то:

  • Парсеры, компиляторы

  • Пакетная обработка

Скорее всего, Goose будет неудобен там, где нужны:

  • Общая изменяемая память, atomics или lock-free структуры

  • Передача больших структур между потоками без копирования

  • Долгоживущие графы, в которых требуется автоматически обнаруживать обращения к удалённым или повторно использованным узлам: slot_pool<T> позволяет переиспользовать слоты, но контроль логического use-after-free и double free остаётся на приложении

Ну то есть Goose – не замена C++ или Rust, а язык для отдельных задач, где максимум можно выжать за счёт компоновки данных и управления памятью.

Источники и дополнительные материалы

Goose

Контекст

Модели памяти и конкурентности

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.

Ты не бойся, это Goose — KioskNews