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

Спор «что быстрее: 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=1 | 3,45 с | 3,56 с | 119 МБ |
| Go, GOGC=400 | 2,33 с | 2,75 с | 179 МБ |
| Go, GOGC=off | 2,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# за последние версии подтянулся к лидерам и стал универсальным выбором для больших систем. Правильный ответ всегда зависит от того, что именно вы измеряете.





