І горутини, і BEAM процеси не мають в своєму дизайні бути максимально легкими для короткоживучих операцій.
Окрім цього, і Go і BEAM мають т.зв. userspace goroutine/process preemption.
На відміну від них, .NET спирається на hill-climbing алогоритм та blocked worker detection які спавнять додаткові потоки тредпулу, які в свою чергу спираються на physical thread preemption та work-stealing для того щоб більша кількість work items (Task continuations) могла прогресувати у виконанні.
Імплементація Rust теж відрізняється тим що обробляє весь 1M фьючерів в скоупі однієї Task (тому що Task це coarse-grained примітив який в себе вміщає статично відомий розмір фьючерів. Cаме над Tasks оперують окремі worker threads в Tokio).
Це впливає на сценарії де той чи інший підхід матиме перевагу, в особливості стосовно scheduling fairness.
Цей експеримент сфокусований на кейс де ворклоад потребує масової логічної concurrency і вивчає особливості базового оверхеду для таких сценаріїв. Проте, даний підхід є наївним, ідіоматичні паттерни для досягнення мультиплексування такої кількості виконання логічних тредів між мовами будуть відрізнятись.
Фідбек стосовно імплементації та альтернативних кейсів заохочується
- Заспавнити 1 мільйон процесів/горутин/тасок
- Кожен з примітивів повинен дочекатись (одне з двох)
- окремого таймера на 5с
- спільного джерела яке очікує таймер на 5с
- Дочекатись виконання процесів/горутин/тасок
macOS 15.0 24A5331b arm64, M1 Pro 6P+2E
.NET SDK 9.0.100-rc.1.24414.7
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit] [dtrace]
Elixir 1.17.2 (compiled with Erlang/OTP 26)
go version go1.23.0 darwin/arm64
cargo 1.80.1 (376290515 2024-07-16)
rustc 1.80.1 (3f5fd8dd4 2024-08-06)
- Скомпілювати наявним тулчейном в нативний додаток (за наявністю)
- Обрати найкращий результат
time ...
з пʼяти послідовних спроб - Виміряти memory RSS під час очікування на таймер
Memory RSS at wait - розмір памʼяті яку споживає процес під час очікування закінчення 5с паузи.
Executed in - загальний час на виконання від старту процесу до його виходу з кодом 0.
Usr time - ефективний час активної роботи CPU для виконання userspace коду. На multi-core системах максимальний usr time на секунду часу це 1s * CPU cores.
Sys time - ефективний час активної роботи CPU для виконання kernelspace коду (наприклад час проведений у кернел коді системних таймерів, виводу в консоль, тощо). Так само як usr time максимальне значення на секунду це 1s * CPU cores.
Таким чином, основні показники ефективності асинхронних примітивів будуть MEM + usr time, і потім executed in за умови що останній не відхиляється суттєво від 5c відмітки.
Go:
Зібрано go build tasks.go
Memory RSS at wait: 2.50 GB
❯ time ./tasks 1000000
spawning 1000000 tasks
done
________________________________________________________
Executed in 5.81 secs fish external
usr time 3.18 secs 52.00 micros 3.18 secs
sys time 0.74 secs 669.00 micros 0.74 secs
Elixir:
Зібрано elixirc tasks.ex
Memory RSS at wait: 2.75/3.31/4.00 GB (does not have stable value)
❯ time elixir --erl "+P 1000001" -e Tasks.main 1000000
spawning 1000000 processes
done
________________________________________________________
Executed in 10.14 secs fish external
usr time 16.59 secs 0.07 millis 16.59 secs
sys time 13.79 secs 1.03 millis 13.79 secs
C#:
Зібрано dotnet publish -o .
(проект з темплейту dotnet new console --aot
)
Memory RSS at wait: 272.8 MB
❯ time ./Tasks 1000000
spawning 1000000 tasks
done
________________________________________________________
Executed in 5.83 secs fish external
usr time 3.99 secs 0.08 millis 3.99 secs
sys time 0.24 secs 1.20 millis 0.24 secs
Rust:
Зібрано cargo build --release
Memory RSS at wait: 203.2 MB
❯ time ./target/release/tasks 1000000
spawning 1000000 tasks
done
________________________________________________________
Executed in 5.48 secs fish external
usr time 836.91 millis 0.09 millis 836.82 millis
sys time 51.53 millis 1.49 millis 50.04 millis
Go:
Зібрано go build tasks-singlesource.go
Memory RSS at wait: 2.48 GB
❯ time ./tasks-singlesource 1000000
spawning 1000000 tasks
done
________________________________________________________
Executed in 5.29 secs fish external
usr time 3.85 secs 0.07 millis 3.85 secs
sys time 1.90 secs 1.14 millis 1.90 secs
Elixir:
Зібрано elixirc tasks-singlesource.ex
Memory RSS at wait: 3.41/3.72 GB (does not have stable value)
❯ time elixir --erl "+P 1000001" -e TasksSingleSource.main 1000000
spawning 1000000 processes
done
________________________________________________________
Executed in 11.26 secs fish external
usr time 18.58 secs 39.00 micros 18.58 secs
sys time 13.69 secs 476.00 micros 13.69 secs
C#:
Зібрано dotnet publish -o .
(проект з темплейту dotnet new console --aot
)
Memory RSS at wait: 136.1 MB
❯ time ./TasksSingleSource 1000000
spawning 1000000 tasks
done
________________________________________________________
Executed in 5.26 secs fish external
usr time 1.25 secs 0.10 millis 1.25 secs
sys time 0.42 secs 1.19 millis 0.42 secs
Rust:
asynchronous sleep не є shareable в імплементації Tokio. Ідіоматичний Раст схильний до зменшеного шерінгу даних і більшої кількості комунікації через dedicated контейнери даних для цього. На відміну від Го ченел не є кор примітивом синхронізації в Расті тому ця entry лишається пустою.
(автор програв borrow checker'у)
Параметр | Go | Elixir | C# | Rust |
---|---|---|---|---|
Окремий таймер | ||||
Execution time | 5.81 сек | 10.14 сек | 5.83 сек | 5.48 сек |
Memory RSS at wait | 2.50 GB | 2.75-4.00 GB (нестабільно) | 272.8 MB | 203.2 MB |
User time | 3.18 сек | 16.59 сек | 3.99 сек | 836.91 мсек |
System time | 0.74 сек | 13.79 сек | 0.24 сек | 51.53 мсек |
Спільний таймер/канал/процес | ||||
Execution time | 5.29 сек | 11.26 сек | 5.26 сек | - |
Memory RSS at wait | 2.48 GB | 3.41-3.72 GB (нестабільно) | 136.1 MB | - |
User time | 3.85 сек | 18.58 сек | 1.25 сек | - |
System time | 1.90 сек | 13.69 сек | 0.42 сек | - |
За ефективністю CPU:
- Rust
- C# shared delay
- Go per-goroutine sleep
- C# per-task delay
- Go shared wait by channel
- Elixir per-process sleep
- Elixir process wait
За ефективністю MEM:
- (Rust custom shared delay primitive???)
- C# shared delay
- Rust
- C# per-task delay
- Go (обидва варіанти)
- Elixir (обидва варіанти мають подібну варіацію споживання памʼяті)