Rust の非同期ランタイムの実行速度を比較してみる


環境

stable-x86_64-unknown-linux-gnu
rustc 1.41.0 (5e1a79984 2020-01-27)

動機

This Week in Rustこんな記事を見つけたので Rust の非同期ランタイムの実行速度が気になって比較してみました。
tokioactix-rt(内部的には tokio)、futuresasync-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 非同期ランタイムに詳しくなれたような気がした。