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

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# за последние версии подтянулся к лидерам и стал универсальным выбором для больших систем. Правильный ответ всегда зависит от того, что именно вы измеряете.

Полная версия статьи с таблицей результатов на uproger.com.