Rust 2026-10-15·Ievgenii Svyryd

Бенчмарк, раунд 2: Rust, Go і фреймворки Rust під профайлером

Go і сім застосунків на Rust (axum, actix-web, Rocket, Loco, Cot і Rustango на axum та actix) за однаковими правилами, коли працює лише застосунок під тестом, а також профілі CPU, системних викликів і мережевого обміну, які показують, на що кожен із них витрачає час.

Бенчмарк, раунд 2: Rust, Go і фреймворки Rust під профайлером

Це раунд 2 чесного бенчмарку. Раунд 2 охоплює лише компільовані застосунки: Go і сім застосунків на Rust. Він додає три фреймворки Rust (Rocket, Loco, Cot) і профілі CPU, системних викликів і мережевого обміну, які показують, на що кожен застосунок витрачає час.

Виміряно 14 жовтня 2026 року на Apple M3 Pro (Docker VM з 12 CPU і 16 GB).

  • 441/s збирань сміття Go на кешованому індексі
  • 3 vs 9 обмінів із Postgres на сторінку деталей: Go vs sqlx із пінгом під час отримання
  • +45% Rustango на actix, сторінка індексу, з окремою часткою пулу на кожен воркер
  • 6.3–22.8 MiB пам'ять під навантаженням, усі вісім застосунків
  • 0 помилок або відповідей не-200 за 144 прогони

Налаштування

Той самий блог, що й у раунді 1: 10 авторів, 30 тегів, 1 000 дописів, 2 600 зв'язків допис-тег і 10 000 коментарів. Шість маршрутів: індекс (20 найновіших дописів), сторінка допису з усіма коментарями, сторінка тегу, індекс і сторінка допису з кешу сторінок у Redis, а також /compute (сума простих чисел менших за 20 000, без бази даних).

Вісім застосунків:

  • Go: net/http зі стандартної бібліотеки, pgx v5, go-redis
  • axum + sqlx і actix + sqlx: сирий sqlx із SQL-текстом Go, шаблони Tera
  • Rocket: rocket_db_pools (sqlx) і rocket_dyn_templates (Tera)
  • Rustango на axum і на actix-web: ORM Rustango та його кеш сторінок
  • Loco: SeaORM, Tera
  • Cot: ORM Cot, шаблони askama

Правила:

  • Застосунок отримує 4 CPU (закріплені за ядрами 0–3) і 2 GB RAM, з 4 воркерами. PostgreSQL 16 і Redis 7 працюють на ядрах 4–7, генератор навантаження oha на ядрах 8–11.
  • Працює лише застосунок під тестом; решта зупинені. Між застосунками кількість з'єднань із Postgres повертається до базового рівня.
  • Пули: максимум 16 з'єднань, мінімум 4.
  • Перед будь-яким навантаженням кожна сторінка кожного застосунку порівнюється з HTML від Go. Розбіжність зупиняє прогін.
  • Без стиснення. 5 s розігріву, потім три прогони по 20 s на endpoint при 50 з'єднаннях; враховується медіана.

Тестовий стенд

DOCKER NETWORK · NO COMPRESSION oha load generator 8 9 10 11 CORES 8–11 App under test 4 CPUs · 2 GB · 4 workers 0 1 2 3 CORES 0–3 · PINNED Databases Postgres · MySQL · Redis 4 5 6 7 CORES 4–7 HTTP 50 conns SQL · cache 16 conns ✓ HTML parity vs Go, before any load PER ENDPOINT 5 s run 1 · 20 s run 2 · 20 s run 3 · 20 s WARM-UP → MEDIAN OF 3 · HTTP 200 ONLY DOCKER NETWORK oha load generator CORES 8–11 8 9 10 11 HTTP 50 conns ✓ HTML parity vs Go App under test 4 CPUs · 2 GB · 4 workers CORES 0–3 0 1 2 3 SQL · cache · 16 conns Databases PostgreSQL · MySQL · Redis CORES 4–7 4 5 6 7 PER ENDPOINT 20 s 20 s 20 s 5 S WARM-UP → MEDIAN OF 3

Той самий стенд, що й у раунді 1. Раунд 2 використовує лише PostgreSQL і Redis (без MySQL).

Версії

  • Go 1.25.14, pgx v5.7.2, go-redis v9.7.0
  • rustc 1.99.0; axum 0.8.9, actix-web 4.16.0, tokio 1.53, sqlx 0.8.6, Tera 1.20.1
  • Rocket 0.5.1 (sqlx 0.7.4, hyper 0.14)
  • Loco 1.2.0 (SeaORM 2.0.4, sqlx 0.9.0)
  • Cot 0.7.0 (sqlx 0.9.0, askama 0.16.1)
  • Rustango без пінгу під час отримання з'єднання, з кешем сторінок, що зберігає сирі байти, і одним з'єднанням із пулу на запит (висновок 5)

Налаштування, які не вдалося вирівняти

НалаштуванняЗастосункиЧому
Пул пінгує Postgres при кожному отриманні з'єднання (sqlx test_before_acquire)Rocket, Loco, CotНемає налаштування, щоб його вимкнути. В axum + sqlx, actix + sqlx і Rustango його вимкнено.
10 з'єднань у пулі замість 16CotCot не має налаштувань пулу.
Окрема частка пулу на кожен воркер (4 з 16 з'єднань) і одне з'єднання Redis на воркерactix + sqlx, Rustango · actixСпільний пул повільніший на actix-web (див. висновок 6).
sqlx 0.7.4 і hyper 0.14RocketЗафіксовані Rocket 0.5.1.
5, 5 і 7 запитів на сторінку індексу, деталей і тегу замість 3, 3 і 4CotЙого API запитів не може виразити запити Go (див. висновок 9).
askama замість TeraCotШаблонізатор Cot.

Результати

Запитів на секунду, більше — краще.

Сторінки без кешу

Той самий HTML, 50 з'єднань, PostgreSQL на кожен запит

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Індекс
Go 8 741 req/s
axum + sqlx 14 140 req/s
actix + sqlx 14 408 req/s
Rocket 13 316 req/s
Rustango · axum 14 686 req/s
Rustango · actix 17 401 req/sfastest
Loco 10 290 req/s
Cot 2 018 req/s
Деталі
Go 22 607 req/s
axum + sqlx 22 842 req/s
actix + sqlx 21 630 req/s
Rocket 24 561 req/s
Rustango · axum 25 788 req/s
Rustango · actix 27 292 req/sfastest
Loco 15 635 req/s
Cot 9 282 req/s
Тег
Go 6 386 req/s
axum + sqlx 6 467 req/s
actix + sqlx 6 628 req/s
Rocket 6 836 req/s
Rustango · axum 7 090 req/sfastest
Rustango · actix 7 056 req/s
Loco 5 604 req/s
Cot 3 065 req/s

Медіана трьох прогонів по 20 s при 50 з'єднаннях. Працював лише застосунок під тестом. 0 відповідей не-200, 0 помилок.

Кешовані сторінки

Віддаються з кешу сторінок у Redis, без SQL

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Індекс, кешований
Go 91 839 req/s
axum + sqlx 198 669 req/s
actix + sqlx 226 887 req/s
Rocket 138 224 req/s
Rustango · axum 174 501 req/s
Rustango · actix 231 975 req/sfastest
Loco 153 243 req/s
Cot 190 245 req/s
Деталі, кешовані
Go 141 574 req/s
axum + sqlx 214 871 req/s
actix + sqlx 240 161 req/s
Rocket 155 691 req/s
Rustango · axum 192 897 req/s
Rustango · actix 242 609 req/sfastest
Loco 170 158 req/s
Cot 203 119 req/s

Медіана трьох прогонів по 20 s при 50 з'єднаннях. Працював лише застосунок під тестом. 0 відповідей не-200, 0 помилок.

Обчислення

Прості числа менші за 20 000, без бази даних

req/s · higher is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 16 580 req/sfastest
axum + sqlx 14 963 req/s
actix + sqlx 14 578 req/s
Rocket 14 696 req/s
Rustango · axum 14 799 req/s
Rustango · actix 14 710 req/s
Loco 14 184 req/s
Cot 15 330 req/s

Медіана трьох прогонів по 20 s при 50 з'єднаннях. Працював лише застосунок під тестом. 0 відповідей не-200, 0 помилок.

Затримка, p50 / p99 у ms:

ЗастосунокІндексДеталіТегІндекс, кешованийДеталі, кешованіОбчислення
Go5.50 / 9.961.91 / 5.027.61 / 11.720.35 / 2.730.24 / 2.052.37 / 10.75
axum + sqlx3.49 / 4.712.14 / 3.607.42 / 13.890.24 / 0.530.22 / 0.513.35 / 6.07
actix + sqlx3.32 / 6.382.21 / 4.307.23 / 13.230.20 / 0.500.19 / 0.453.41 / 4.97
Rocket3.66 / 5.831.97 / 3.587.14 / 11.550.34 / 0.700.30 / 0.673.40 / 6.14
Rustango · axum3.28 / 5.651.83 / 3.556.90 / 11.180.27 / 0.590.25 / 0.523.38 / 6.09
Rustango · actix2.80 / 4.511.77 / 3.376.91 / 11.530.20 / 0.480.19 / 0.453.41 / 3.96
Loco4.62 / 10.842.99 / 6.348.67 / 14.520.30 / 0.790.28 / 0.623.48 / 6.90
Cot23.35 / 50.365.28 / 8.0215.57 / 27.060.25 / 0.500.23 / 0.523.27 / 5.85

Пам'ять

Пам'ять під навантаженням

Резидентна пам'ять контейнера застосунку на некешованому індексі

MiB · lower is better
  • Go
  • Rust
  • Rustango
  • Loco
  • Cot
Go 22.8 MiB
axum + sqlx 7.6 MiB
actix + sqlx 7.2 MiB
Rocket 8.7 MiB
Rustango · axum 6.9 MiB
Rustango · actix 6.3 MiBlowest
Loco 12.0 MiB
Cot 22.6 MiB

docker stats, вибірка з найвищим CPU з шести, знятих під час першого прогону індексу.

Найбільше використовують Go і Cot. Індекс Cot на кожен запит завантажує всі 1 000 дописів разом із текстом (висновок 9).

Висновки

Кожен висновок каже, що виміряно, чому так відбувається і як це доведено. Профілі — це одиночні прогони по 25 s на тому самому стенді, по одному застосунку, тож їхні req/s трохи відрізняються від чесного прогону вище. Якщо причину не доведено, про це сказано.

1. Обчислення: Go швидший на 8–17%

Що. Go обслуговує 16 580 req/s на /compute; застосунки на Rust — 14 184–15 330.

Чому. Обидва скомпільовані цикли мають 8 інструкцій, 64-бітний sdiv і одну перевірку ділення на нуль. Різниця — у порядку інструкцій, який обирає LLVM. Яка мікроархітектурна деталь CPU робить цей порядок повільнішим, не доведено: у Docker VM немає апаратних лічильників.

Як доведено. Сам цикл, на одному ядрі, поза HTTP:

Цикл простих чисел, один виклик

Одне ядро, без HTTP

мікросекунди · lower is better
  • Go
  • Rust
Go 194 мікросекундиlowest
Rust 224 мікросекунди
Rust, порядок інструкцій Go 194 мікросекундиlowest

Rust із точно таким самим порядком інструкцій, як у Go, виконується за час Go. Решта кожного запиту в Rust дешевша: 8 µs проти 14 µs у Go.

Затримка. Go має нижчий p50, але вищий p99. Середня затримка дорівнює 50 / req/s для кожного застосунку, тож середні значення йдуть за пропускною здатністю; різниця — у розкиді. Go обслуговує запити не по черзі: p10 0.56 ms, p99 10.75 ms. Застосунки на Rust обслуговують майже в порядку надходження: axum + sqlx p10 1.85 ms, p99 6.07 ms. Чому p99 у Go тримається біля 10 ms, не доведено.

2. Кешовані сторінки: збирач сміття Go

Що. На кешованому індексі Go обслуговує 91 839 req/s; axum + sqlx — 198 669.

Чому. Go виконує на цьому маршруті 441 збирання сміття на секунду, і паузи stop-the-world у сумі займають близько 10% часу. Решта — CPU на запит: 27.2 µs у Go проти 14.6 µs в axum + sqlx.

Як доведено. Підвищення GOGC прибирає більшу частину розриву:

Go, кешований індекс, налаштований GC

До: типове GOGC=100. Профілювальні прогони.

beforeafter
GOGC=400, req/s 97 289 req/s 144 083 req/s +48%
GC майже вимкнено, req/s 97 289 req/s 159 734 req/s +64%
GC майже вимкнено, p99 2.48 ms 0.90 ms −64%

Куди йде решта CPU, на запит на кешованому індексі:

Goaxum + sqlx
CPU застосунку27.2 µs14.6 µs
Шлях запису в ядрі10.6 µs6.9 µs
Системні виклики10.7 (6.6 read, 2.9 write)4.8 (2.0 recvfrom, 1.4 writev)
CPU Redis6.8 µs2.5 µs
Трафік Redisgo-redis: один GET на запитодне з'єднання, GET-и багатьох запитів об'єднані в один pipeline

Go: CPU на кешований запит

27.2 µs загалом

  • 39%Шлях запису в ядрі, 10.6 µs
  • 61%Усе інше, 16.6 µs

axum + sqlx: CPU на кешований запит

14.6 µs загалом

  • 47%Шлях запису в ядрі, 6.9 µs
  • 53%Усе інше, 7.7 µs

3. Сторінка деталей: обміни з Postgres

Що. Сторінка деталей: Go 22 607, axum + sqlx 22 842, Rocket 24 561, Rustango · axum 25 788 req/s. Усі надсилають ті самі три запити.

Чому. Відрізняється кількість обмінів (round trips) із Postgres. pgx надсилає один на запит. sqlx додає один, коли з'єднання береться з пулу (пінг під час отримання), і ще один, коли воно повертається. axum + sqlx бере з'єднання на кожен запит, тож платить за кожен запит; Rocket і Rustango беруть одне з'єднання на сторінку, тож платять один раз. Кожен зайвий обмін коштує близько 10 µs CPU Postgres.

Як доведено. Перехоплення протоколу Postgres під навантаженням із підрахунком повідомлень ReadyForQuery на сторінку:

Обміни з Postgres на сторінку деталей

Перехоплення трафіку, ті самі три запити; ping = пінг sqlx під час отримання

обміни · lower is better
  • Go
  • Rustango
  • Rust
Go · pgx 3 обміниlowest
Rustango 4 обміни
Rocket 5 обміни
axum + sqlx, пінг вимкнено 6 обміни
axum + sqlx, пінг увімкнено 9 обміни

Сторінка індексу показує ті самі значення.

axum + sqlx: пінг під час отримання вимкнено

Профілювальні прогони

beforeafter
Деталі, req/s 18 222 req/s 23 472 req/s +29%
Деталі, CPU Postgres 166.6 µs/req 134.3 µs/req −19%
Індекс, req/s 12 140 req/s 14 419 req/s +19%
Індекс, CPU Postgres 237.9 µs/req 208.5 µs/req −12%

Три обміни менше заощаджують 29–32 µs CPU Postgres на сторінку: близько 10 µs кожен.

Go має найменше обмінів, але тут не найшвидший: його збирач сміття зупиняє його приблизно на 9% часу на цьому маршруті.

4. Сторінка індексу: GC і рендеринг шаблонів

Що. Сторінка індексу: Go 8 741 req/s, axum + sqlx 14 140.

Чому. Дві виміряні причини. Знову збирач сміття Go (паузи займають близько 8% часу на цьому маршруті) і рендеринг шаблонів: html/template у Go використовує рефлексію і коштує 167 µs на запит; serde + Tera у Rust коштують 72 µs.

Як доведено.

Go, некешований індекс, налаштований GC

До: типове GOGC=100. Профілювальні прогони.

beforeafter
GOGC=400, req/s 9 359 req/s 12 848 req/s +37%
GC майже вимкнено, req/s 9 359 req/s 13 967 req/s +49%

Рендеринг шаблонів, сторінка індексу

CPU на запит, із профілю CPU

мікросекунди · lower is better
  • Go
  • Rust
Go · html/template 167 мікросекунди
Rust · serde + Tera 72 мікросекундиlowest

На що axum + sqlx витрачає CPU на індексі, за його профілем (частини перетинаються: алокації відбуваються всередині sqlx і Tera):

  • sqlx: близько 112 µs на запит, з яких близько 25 µs — пінг, коли з'єднання повертається в пул
  • malloc з glibc: близько 56 µs
  • sendto у ядрі: 38 µs

5. Rustango: що змінили виправлення

Що. Профілі Rustango до цих виправлень показали чотири витрати. Три виправлено:

Витрата доЧастка CPUВиправлення
Отримання з'єднання з пулу на кожен запит до БД19–35%Одне з'єднання з пулу на HTTP-запит: деталі +26%
Пінг до Postgres при кожному отриманні8–16%Пінг типово вимкнено
CachePageLayer зберігає сторінки як JSON + base64~12–20% на влучання в кешСторінки зберігаються як сирі байти
Querysets перебудовуються і компілюються на кожен запит~2%Ще не виправлено

Rustango · axum, до → після виправлень

Профілювальні прогони, по одному застосунку

beforeafter
Індекс, req/s 11 215 req/s 15 849 req/s +41%
Деталі, req/s 17 267 req/s 28 097 req/s +63%
Індекс, CPU застосунку 286.6 µs/req 220.8 µs/req −23%
Деталі, CPU застосунку 164.7 µs/req 110.4 µs/req −33%

У чесному прогоні Rustango на actix-web і на axum — два найшвидші застосунки на всіх трьох некешованих сторінках.

Відкрите питання. На кешованому індексі Rustango · axum обслуговує 174 501 req/s проти 198 669 в axum + sqlx. Цей розрив ще не профілювали.

6. actix-web: спільний пул повільний

Що. У раунді 1 застосунки на actix-web відставали від axum на некешованих сторінках і використовували менше 300% зі свого бюджету CPU в 400%.

Чому. З одним спільним пулом кожен запит до БД будить потік іншого рантайму. Якщо дати кожному воркеру actix власну частку пулу (4 з 16 з'єднань) і власне з'єднання Redis, проблема зникає. Механізм — що сокет обслуговує рантайм, який його відкрив, — це припущення; ефект виміряно.

Як доведено. Ті самі застосунки, спільний пул проти часток на воркер:

Спільний пул → окрема частка пулу на воркер

Профілювальні прогони

beforeafter
actix + sqlx
Індекс 11 094 req/s 15 108 req/s +36%
Деталі 18 080 req/s 23 330 req/s +29%
Індекс, кешований 192 604 req/s 238 884 req/s +24%
Rustango · actix
Індекс 12 052 req/s 17 454 req/s +45%
Деталі 23 032 req/s 28 693 req/s +25%
Індекс, кешований 190 889 req/s 237 962 req/s +25%
Перемикань контексту на запитСпільний пулПул на воркер
actix + sqlx, індекс4.041.45
actix + sqlx, деталі3.571.64
Rustango · actix, індекс2.700.81
Rustango · actix, деталі2.341.03

Приблизно у 2.5× менше перемикань контексту на запит. Більше воркерів не допомагає: 8 воркерів зі спільним пулом досягають 11 904 req/s на індексі actix + sqlx (4 воркери, спільний: 11 094).

7. Rocket: сильний на сторінках із БД, найповільніший із кешем

Що. Rocket: деталі 24 561 req/s, попереду Go і застосунків на сирому sqlx; кешований індекс 138 224, найповільніший результат серед Rust.

Чому. На сторінках із БД Rocket бере одне з'єднання на сторінку і робить 5 обмінів (висновок 3). На кешованих сторінках він витрачає 24.1 µs CPU на запит проти 14.6 µs в axum + sqlx, з 8.1 системного виклику на запит проти 4.8. Профіль показує зайвий writev (2.24 на запит проти 1.36) і конкуренцію в malloc; викликів futex 2.88 на запит проти 0.91.

Як доведено. Облік CPU, підрахунок системних викликів і профілі CPU на кешованому індексі.

8. Loco

Що. Loco: індекс 10 290, деталі 15 635, кешований індекс 153 243 req/s.

Чому. SeaORM надсилає стільки ж запитів, скільки Go. На кешованих сторінках Loco витрачає 21.0 µs на запит: на 2.4 µs більше в malloc і на 1.2 µs більше в middleware, ніж axum + sqlx. На сторінках із БД пінг під час отримання вимкнути неможливо (висновок 3 показує його вартість у sqlx).

Як доведено. Облік CPU і профілі CPU на кешованому індексі. Сторінки Loco з БД не профілювали, тож яку частину цього розриву пояснює пінг, не доведено.

9. Cot: межу задає API запитів

Що. Cot: індекс 2 018, деталі 9 282, тег 3 065 req/s, але кешований індекс 190 245.

Чому. API запитів Cot має лише filter, limit і offset: немає ORDER BY, JOIN, IN чи COUNT. Щоб показати 20 найновіших дописів, індекс завантажує всі 1 000 дописів разом із текстом. Сторінкам потрібно 5, 5 і 7 запитів замість 3, 3 і 4. Пам'ять під навантаженням — 22.6 MiB. Кешовані сторінки не торкаються ORM і швидкі: 16.4 µs CPU на запит.

Як доведено. Кількість запитів із коду, пам'ять із чесного прогону, облік CPU на кешованому індексі.

10. Пам'ять

Див. графік пам'яті вище. Шість застосунків на Rust використовують 6.3–12.0 MiB під навантаженням. Go використовує 22.8 MiB. Cot використовує 22.6 MiB; його індекс завантажує кожен допис на кожен запит (висновок 9).

11. Спільні витрати

Дві витрати є в кожному застосунку на Rust і не належать жодному фреймворку:

  • Рендеринг Tera: близько 18–20% CPU на некешованих сторінках
  • malloc з glibc: близько 12%

Швидший алокатор, як-от mimalloc чи jemalloc, міг би зменшити другу. Це не перевірено.

Обмеження

  • Одна машина: Apple M3 Pro. Інші процесори можуть упорядкувати результати інакше, особливо в обчисленнях (висновок 1).
  • Навантаження переважно на читання: без записів, без транзакцій.
  • Застосунки працюють у Docker VM на macOS, а не на bare-metal Linux. У VM немає апаратних лічильників продуктивності.

Підсумок: чому Rustango на actix-web став набагато швидшим

Rustango · actix, сторінка індексу

Три прогони: раунд 1, профілювальний прогін і чесний прогін цього раунду

req/s · higher is better
Раунд 1 (0.60.6) 6 341 req/s
Виправлення, спільний пул 12 052 req/s
Виправлення, пул на воркер 17 401 req/sfastest

Дві зміни, у двох місцях.

1. Виправлення в Rustango. Вони допомагають і на axum, і на actix-web:

  • Кожна колонка декодується один раз, за своїм типом. Раніше декодування забирало 46% CPU.
  • Немає пінгу перед кожним отриманням із пулу: на один обмін із Postgres менше на кожен запит до БД.
  • Одне з'єднання з пулу на HTTP-запит: запити однієї сторінки ділять одне з'єднання, а не отримують його й пінгують під час повернення кожен окремо.

2. Налаштування actix-web. axum працює на одному спільному багатопотоковому рантаймі Tokio. actix-web запускає однопотоковий рантайм на кожен воркер. Коли один пул спільний для всіх воркерів, сокет з'єднання лишається за рантаймом, який його відкрив, тож коли інший воркер використовує це з'єднання, кожне читання і запис будять інший потік. Тоді застосунок робить у 2.5× більше перемикань контексту і використовує близько 280% своїх 4 CPU. Окрема частка пулу (4 з 16 з'єднань) і одне з'єднання Redis на воркер прибирають ці переходи між потоками:

  • Rustango · actix: індекс 12 052 → 17 454, деталі 23 032 → 28 693, кешований індекс 190 889 → 237 962 req/s
  • actix + sqlx: індекс 11 094 → 15 108 req/s
  • 8 воркерів зі спільним пулом не допомогли: 11 904 req/s на індексі actix + sqlx

Прискорення і падіння кількості перемикань контексту виміряно. Прив'язка сокетів до рантайму — найкраще пояснення, але прямо воно не доведене. В axum такої проблеми немає, бо всі його задачі працюють на одному спільному рантаймі.

На практиці: в actix-web створюйте пул з'єднань із БД усередині кожного воркера, а не один раз для всіх воркерів.

Зв'язатися →