環境
stable-x86_64-unknown-linux-gnu
rustc 1.41.0 (5e1a79984 2020-01-27)
動機
This Week in Rust でこんな記事を見つけたので Rust の非同期ランタイムの実行速度が気になって比較してみました。
tokio、actix-rt(内部的には tokio)、futures、async-std の4つを比較します。
計測
Cargo.toml
[dependencies]
tokio = "0.2"
actix-rt = "1.0"
futures = "0.3"
async-std = "1.4"
main.rs
use tokio;
use actix_rt;
use futures;
use async_std;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;
fn main() {
let mut rt = tokio::runtime::Runtime::new().unwrap();
let now = Instant::now();
rt.block_on(Yields(100));
println!("tokio: {:?}", now.elapsed());
let mut rt = actix_rt::Runtime::new().unwrap();
let now = Instant::now();
rt.block_on(Yields(100));
println!("actix_rt: {:?}", now.elapsed());
let now = Instant::now();
futures::executor::block_on(Yields(100));
println!("futures: {:?}", now.elapsed());
let now = Instant::now();
async_std::task::block_on(Yields(100));
println!("async_std: {:?}", now.elapsed());
}
struct Yields(i32);
impl Future for Yields {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
if self.0 == 0 {
Poll::Ready(())
} else {
self.0 -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
結果
crate | time |
---|---|
tokio | 126.455µs |
actix-rt | 138.074µs |
futures | 7.504µs |
async-std | 84.609µs |
futures が圧倒的に早い。 |
おまけ
async-std のスケジューラが新しくなるようなので、試してみます。
Cargo.toml
[dependencies]
async-std = { git = 'https://github.com/stjepang/async-std', branch = 'new-scheduler' }
crate | time |
---|---|
async-std (new-scheduler) | 7.541µs |
futures と同じぐらいですね。以上。 |
追記
tokio とその他 crates のパフォーマンスにあまりに差があるので調べました。
tokio のランライムは Thread Pool を使用するので、そのオーバーヘッドが大きいようです (下記参照)。Single Thread の executor を使用するには下記のようになります。(詳細は Runtime Configurations を参照)
let mut basic_rt = tokio::runtime::Builder::new()
.basic_scheduler().build().unwrap();
let now = Instant::now();
basic_rt.block_on(Yields(100));
println!("tokio (single thread): {:?}", now.elapsed());
crate | time |
---|---|
tokio (single thread) | 10.5µs |
用途に合わせて適切にランタイムを使い分ける必要がありますね。 |
もうちょっとだけ続く
ちゃんと Feature Flag を立てないと Thread Pool は有効にならないのでした…。
Cargo.toml
[dependencies]
tokio = { version = "0.2", features = ["rt-threaded"] }
futures = { version = "0.3", features = ["thread-pool"] }
let mut rt = tokio::runtime::Runtime::new().unwrap();
let now = Instant::now();
rt.block_on(Yields(100));
println!("tokio (thread pool): {:?}", now.elapsed());
let mut basic_rt = tokio::runtime::Builder::new()
.basic_scheduler().build().unwrap();
let now = Instant::now();
basic_rt.block_on(Yields(100));
println!("tokio (single thread): {:?}", now.elapsed());
use futures::task::SpawnExt;
let pool = futures::executor::ThreadPool::new().unwrap();
let handle = pool.spawn_with_handle(Yields(100)).unwrap();
let now = Instant::now();
futures::executor::block_on(handle);
println!("futures (thread pool): {:?}", now.elapsed());
let now = Instant::now();
futures::executor::block_on(Yields(100));
println!("futures (single thread): {:?}", now.elapsed());
crate | time |
---|---|
tokio (thread pool) | 6.633µs |
tokio (single thread) | 10.551µs |
futures (thread pool) | 46.969µs |
futures (single thread) | 5.824µs |
う、う〜ん.。oO(この比較意味あるの?) |
思いつきで書き始めた記事だけど、ちょっとだけ Rust 非同期ランタイムに詳しくなれたような気がした。