Rust vs Go vs C# в 2026: почему наивный Rust проиграл Go

Сравнение производительности Rust, Go и C# в 2026 году: наш тест на аллокациях, где наивный Rust уступил Go, а Rust с ареной оказался в 7 раз быстрее, плюс разбор GC, памяти, холодного старта, Green Tea и .NET 10.

Спор «что быстрее: Rust, Go или C#» обычно заканчивается ссылкой на чей-то бенчмарк, где один язык обгоняет другой в полтора раза. Мы решили проверить простую вещь и сразу получили результат, который ломает привычную картину: наивный код на Rust проиграл Go по времени выполнения. Ниже разбираем, почему так вышло, что на самом деле означает «производительность» в 2026 году и как выбирать язык под конкретную нагрузку.

Почему готовым бенчмаркам больше нельзя верить на слово

Долгое время главным ориентиром для веб-фреймворков были TechEmpower Framework Benchmarks. В марте 2026 года проект перевели в архив. Последний, 23-й раунд вышел в феврале 2025 года и охватывал больше 330 реализаций, но тестовая платформа годами почти не менялась, результаты упирались примерно в 30 млн запросов в секунду, а участники научились выигрывать за счет низкоуровневых трюков, которые в обычном приложении никто не пишет. Сами разработчики фреймворков признавали, что такие цифры вводят в заблуждение.

Синтетические тесты вроде Benchmarks Game полезны, но измеряют предельно оптимизированный код, написанный экспертами по каждому языку. В реальном проекте код пишут обычные разработчики под дедлайн, а теперь еще и ИИ-ассистенты. Поэтому полезнее смотреть, как язык ведет себя на простом, «честном» коде и сколько усилий нужно, чтобы выжать из него максимум.

Эксперимент: миллионы маленьких объектов

Для теста мы взяли классическую задачу на аллокации: 20 раз построить полное двоичное дерево глубины 20 (около двух миллионов узлов) и обойти его. Такой профиль нагрузки встречается везде, где программа создает много мелких объектов: парсеры, AST, деревья состояний, графы зависимостей, ответы API, разобранные в структуры. Код написан максимально прямолинейно, без оптимизаций.

Rust, каждый узел в отдельной Box:

struct Node {
    l: Option<Box<Node>>,
    r: Option<Box<Node>>,
}

fn make(d: u32) -> Box<Node> {
    if d == 0 {
        Box::new(Node { l: None, r: None })
    } else {
        Box::new(Node { l: Some(make(d - 1)), r: Some(make(d - 1)) })
    }
}

fn check(n: &Node) -> u64 {
    1 + n.l.as_deref().map_or(0, check) + n.r.as_deref().map_or(0, check)
}

fn main() {
    let mut total = 0u64;
    for _ in 0..20 {
        total += check(&make(20));
    }
    println!("{total}");
}

Go, обычные указатели и сборщик мусора:

package main

import "fmt"

type Node struct{ l, r *Node }

func makeTree(d int) *Node {
	if d == 0 {
		return &Node{}
	}
	return &Node{makeTree(d - 1), makeTree(d - 1)}
}

func check(n *Node) int {
	if n.l == nil {
		return 1
	}
	return 1 + check(n.l) + check(n.r)
}

func main() {
	total := 0
	for i := 0; i < 20; i++ {
		total += check(makeTree(20))
	}
	fmt.Println(total)
}

C#, тот же алгоритм на классах:

long total = 0;
for (int i = 0; i < 20; i++)
    total += Check(Make(20));
Console.WriteLine(total);

static Node Make(int d) =>
    d == 0 ? new Node() : new Node { L = Make(d - 1), R = Make(d - 1) };

static long Check(Node n) =>
    n.L is null ? 1 : 1 + Check(n.L) + Check(n.R!);

sealed class Node
{
    public Node? L, R;
}

Замеры проводились на виртуальной машине с двумя ядрами Intel Xeon 2,8 ГГц, Rust 1.97 и Go 1.24, каждый вариант запускался пять раз, в таблице медиана. C#-версию мы приводим для самостоятельной проверки: ее результат сильно зависит от режима (JIT или Native AOT) и настроек сборщика мусора, поэтому сравнивать ее стоит на своем железе с теми же настройками, что в продакшене.

ВариантВремя, медианаПроцессорное времяПик памяти
Rust, Box на каждый узел3,18 с3,12 с66 МБ
Go, настройки по умолчанию2,89 с4,04 с78 МБ
Go, GOMAXPROCS=13,45 с3,56 с119 МБ
Go, GOGC=4002,33 с2,75 с179 МБ
Go, GOGC=off2,23 с2,19 с667 МБ
Rust, арена (Vec и индексы)0,41 с0,40 с17 МБ

Что показал тест

Наивный Rust оказался медленнее Go по реальному времени: 3,18 секунды против 2,89. Причина в модели памяти. Каждый Box в Rust означает отдельный вызов системного аллокатора на создание и отдельное освобождение при удалении дерева, то есть миллионы вызовов malloc и free. Аллокатор Go устроен иначе: мелкие объекты одного размера выделяются из заранее подготовленных блоков очень дешево, а освобождает их сборщик мусора пачками.

Но у победы Go есть цена, которую видно во втором столбце. Процессорного времени Go потратил 4,04 секунды против 3,12 у Rust. Сборщик мусора работал параллельно на втором ядре, и выигрыш по часам получен за счет дополнительной нагрузки на процессор. На сервере, где все ядра заняты полезной работой, этот запас исчезнет. Если ограничить Go одним ядром через GOMAXPROCS=1, время вырастает до 3,45 секунды, и Rust снова впереди.

Строки с GOGC показывают, как в Go обменивается память на скорость. При GOGC=400 сборщик запускается реже: время падает до 2,33 секунды, а пик памяти растет до 179 МБ. С отключенным сборщиком программа работает быстрее всего среди вариантов Go, но съедает 667 МБ. В продакшене для этого обмена есть более безопасный инструмент GOMEMLIMIT, который задает мягкий потолок памяти.

Самая интересная строка последняя. Rust с ареной, где все узлы лежат в одном векторе, а ссылки заменены индексами, справился за 0,41 секунды и 17 МБ памяти. Это почти в семь раз быстрее Go и наивного Rust. Именно здесь проявляется настоящее преимущество Rust: он дает полный контроль над размещением данных в памяти. Только этот контроль нужно использовать осознанно, сам по себе язык быструю программу не гарантирует.

// Узлы лежат в одном Vec, ссылки заменены индексами
struct Node {
    l: u32,
    r: u32,
}

const NIL: u32 = u32::MAX;

fn make(arena: &mut Vec<Node>, d: u32) -> u32 {
    let (l, r) = if d == 0 {
        (NIL, NIL)
    } else {
        (make(arena, d - 1), make(arena, d - 1))
    };
    arena.push(Node { l, r });
    (arena.len() - 1) as u32
}

fn check(arena: &[Node], i: u32) -> u64 {
    let n = &arena[i as usize];
    if n.l == NIL { 1 } else { 1 + check(arena, n.l) + check(arena, n.r) }
}

fn main() {
    let mut total = 0u64;
    let mut arena = Vec::with_capacity(1 << 21);
    for _ in 0..20 {
        arena.clear();
        let root = make(&mut arena, 20);
        total += check(&arena, root);
    }
    println!("{total}");
}

Ось первая: чистая вычислительная скорость

В вычислительных циклах без аллокаций Rust стабильно держится на уровне C и C++: компилятор на базе LLVM агрессивно встраивает функции, разворачивает циклы и автоматически векторизует код. C# в последние годы заметно сократил отставание. Многоуровневая JIT-компиляция с динамической оптимизацией по профилю (Dynamic PGO) перекомпилирует горячие методы с учетом реальных типов и ветвлений, а .NET 10, вышедший 11 ноября 2025 года, улучшил встраивание, девиртуализацию методов, работу со структурами и добавил поддержку AVX10.2 и Arm64 SVE.

Компилятор Go сознательно проще: он оптимизирует менее агрессивно, зато собирает проекты очень быстро. Для числодробилок Go обычно отстает от Rust и современного C#, и именно поэтому в Go 1.26 появился экспериментальный пакет simd/archsimd для прямого доступа к векторным инструкциям. Если ваш сервис в основном ждет базу данных и сеть, эта разница почти не будет заметна.

Ось вторая: задержки и сборка мусора

Для сетевых сервисов средняя скорость важна меньше, чем хвостовые задержки: насколько медленно отвечают худшие 1% или 0,1% запросов. Здесь главную роль играет сборщик мусора.

Самая известная история на эту тему произошла в Discord в 2020 году. Сервис, отвечавший за статусы прочтения сообщений, был написан на Go и регулярно страдал от всплесков задержек, связанных со сборкой мусора. После переписывания на Rust всплески исчезли, а задержки и потребление ресурсов снизились. С тех пор сборщик Go сильно изменился. В Go 1.26, вышедшем 10 февраля 2026 года, по умолчанию включен новый сборщик Green Tea. Он сканирует память целыми страницами вместо отдельных объектов, что лучше работает с кэшем процессора. По данным команды Go, многие программы тратят на сборку мусора примерно на 10% меньше времени, а отдельные нагрузки до 40% меньше.

В .NET используется поколенческий сборщик мусора с режимами Workstation и Server, а для контейнеров есть адаптивный режим DATAS, который подстраивает размер кучи под нагрузку. В .NET 10 улучшен escape-анализ, поэтому больше короткоживущих объектов размещается на стеке и вообще не попадает в кучу, а на Arm64 доработанные барьеры записи сократили паузы сборщика на 8-20%. Rust сборщика мусора не имеет вовсе, память освобождается детерминированно, и всплесков из-за GC просто не бывает. Для систем с жесткими требованиями к задержкам, вроде торговых движков, игровых серверов или сетевых прокси, это решающий аргумент.

Ось третья: потребление памяти

Наш тест хорошо показывает закономерность. Языку со сборщиком мусора для скорости нужен запас памяти: чем реже он убирает, тем быстрее работает и тем больше занимает. Go позволяет управлять этим через GOGC и GOMEMLIMIT, .NET через настройки GC и режим DATAS. Rust занимает ровно столько, сколько нужно данным, плюс накладные расходы аллокатора.

В облаке это превращается в деньги. Если сервис на Go или C# требует 512 МБ на под, а аналог на Rust укладывается в 128 МБ, при сотнях реплик разница в счете за Kubernetes становится заметной. Правда, сэкономить так получится только на сервисах, которые реально упираются в память.

Ось четвертая: холодный старт и размер

Для serverless-функций, CLI-утилит и сервисов с частым масштабированием важно, как быстро процесс начинает отвечать. Go компилируется в один статический бинарник и стартует за миллисекунды, поэтому так популярен в инфраструктурных инструментах. Rust тоже дает компактный нативный бинарник с мгновенным стартом.

Классическое приложение на .NET стартует медленнее из-за загрузки рантайма и JIT-компиляции, и под нагрузкой ему нужно время на «прогрев». Эту проблему решает Native AOT: приложение заранее компилируется в машинный код, стартует быстро и занимает меньше памяти. В .NET 10 AOT-сборки стали меньше и быстрее, но у режима есть ограничения: часть кода с рефлексией и динамической генерацией не работает, а библиотеки должны поддерживать тримминг. Кроме того, без JIT пропадает динамическая оптимизация по профилю, и в долгоживущих сервисах пиковая производительность AOT-версии бывает ниже, чем у прогретой JIT-версии.

Ось пятая: скорость разработки

Производительность команды тоже производительность. Go собирается быстрее всех и читается почти без подготовки, а Go 1.27, вышедший в августе 2026 года, ускорил выделение мелких объектов до 30%, добавил generic-методы и новый пакет encoding/json/v2 с заметно более быстрым разбором JSON. C# дает богатую экосистему, отличные инструменты и производительность, которой хватает подавляющему большинству бизнес-систем. Rust требует больше времени на обучение и сборку, а borrow checker заставляет продумывать владение данными заранее. Зато многие ошибки, которые в других языках всплывают в продакшене, в Rust не проходят компиляцию.

Так что выбрать

Если вы пишете сетевой сервис, API или инфраструктурный инструмент и вам важны простота, быстрая сборка и предсказуемый результат без тонкой настройки, Go остается самым практичным выбором, особенно после появления Green Tea. Если у вас корпоративная система, богатая бизнес-логика, команда с опытом в .NET или нужна экосистема Microsoft, C# в 2026 году дает производительность, которой с запасом хватает, а Native AOT закрывает вопрос холодного старта.

Rust стоит выбирать там, где каждая миллисекунда хвостовой задержки и каждый мегабайт памяти превращаются в деньги или в требования к надежности: прокси и балансировщики, движки баз данных, обработка потоков данных, встраиваемые системы, горячие участки других сервисов. Но, как показал наш тест, Rust становится быстрым тогда, когда вы проектируете размещение данных, а не просто переписываете код с другого языка один в один.

Хорошее правило на практике: напишите прототип горячего участка на двух языках, прогоните его на своих данных и своем железе и смотрите на четыре числа сразу: время, процессорное время, пик памяти и p99 задержки. Команды для такого сравнения:

rustc -O trees.rs -o trees_rs
go build -o trees_go trees.go
dotnet publish -c Release            # JIT
dotnet publish -c Release -p:PublishAot=true   # Native AOT

/usr/bin/time -v ./trees_rs   # смотрите Elapsed и Maximum resident set size
GOGC=400 ./trees_go
DOTNET_gcServer=1 ./trees_cs

Итог

Вопрос «какой язык быстрее» в 2026 году почти потерял смысл. Rust дает максимальный потолок производительности и предсказуемость без сборщика мусора, но этот потолок нужно уметь достать. Go выигрывает простотой и обменивает процессор и память на скорость разработки, а Green Tea заметно снизил цену сборки мусора. C# за последние версии подтянулся к лидерам и стал универсальным выбором для больших систем. Правильный ответ всегда зависит от того, что именно вы измеряете.

Поделиться:

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *