#actor #actor-framework #low-overhead #extremely #bare-bones #handle #supervisorless

无需 std slacktor

Slacktor 是一个极快、轻量级、无额外开销、无需监督器的无 std actor 库

3 个版本 (破坏性更新)

0.3.0 2024年4月27日
0.2.2 2024年4月27日
0.2.1 2024年4月27日
0.2.0 2024年4月26日
0.1.0 2024年4月5日

异步 中排名第 493

Download history 369/week @ 2024-04-23 48/week @ 2024-04-30 2/week @ 2024-05-28

每月下载量 124

MIT/Apache

19KB
152

Slacktor

crates.io crates.io docs.rs

用 Rust 编写的极快轻量级 actor 库。

关于

Slacktor 是一个性能极优的 actor 库。它支持无 std,只有一个依赖项,非常简单且易于移植。在最佳条件下,它可以达到每秒大约 7 亿条消息到一个 actor,如 simple 示例所示(该示例使用了 rand crate 来强制编译器不要优化代码)。因为 Slacktor 不处理同步,利用 rayon 可以在 i9-13900H 笔记本电脑 CPU 上实现大约 4.5 亿条消息/秒,如 parallel 示例所示。

Slacktor 的开销极低,仅仅将 parallel 中的 u64 转换为 u32 在 parallel_u32 中,速度就提升到大约 9 亿条消息/秒,将它们减少到 u8 在 parallel_u8 中,速度就提升到 21 亿条消息/秒。示例 no_slacktor 是不使用 Slacktor 的 parallel_u8,可以达到 22 亿条消息/秒。使用迭代器方法如 sum 而不是 collect 减少分配,我已经测量到高达 80 亿条消息/秒。

限制和免责声明

Slacktor actors 没有其他 actor 框架中所谓的“上下文”,这是通向外部世界的一个窗口,允许它们与现有 actors 交互。这需要用户自己提供,无论是作为 RwLock/Mutex 的一个 Arc 引用,还是通过消息传递。Slacktor 专注于提供简单的、性能优化的 actor 系统核心,具有最少的依赖。

Slacktor 实际上并不使用消息传递。相反,它基于原始函数调用模拟消息传递。这允许实现极高的性能,保持其他 actor 框架的优点,并为用户提供对其项目结构的完全控制。您可以根据需要使用 Slacktor 的任何部分或全部。

Slacktor 为什么这么快?

Slacktor 不尝试处理任何同步、并发或消息传递。相反,Slacktor 在演员块上提供了一个简单的抽象。消息传递通过在调用 send 后立即调用消息处理器来模拟。这允许编译器将 Slacktor 实质上优化到仅几个函数调用,同时仍然保持消息传递的抽象。

基准测试

在我的笔记本电脑上(i9-13900H,32GB RAM),以下代码每秒输出大约 70,000,000 条消息

use std::time::Instant;

use slacktor::{
    actor::{Actor, Handler, Message},
    Slacktor,
};

struct TestMessage(pub u64);

impl Message for TestMessage {
    type Result = u64;
}

struct TestActor(pub u64);

impl Actor for TestActor {
    fn destroy(&self) {
        println!("destroying");
    }
}

impl Handler<TestMessage> for TestActor {
    fn handle_message(&self, m: TestMessage) -> u64 {
        m.0 ^ self.0
    }
}

fn main() {
    // Create a slacktor instance
    let mut system = Slacktor::new();

    // Create a new actor
    let actor_id = system.spawn(TestActor(rand::random::<u64>()));

    // Get a reference to the actor
    let a = system.get::<TestActor>(actor_id).unwrap();

    // Time 1 billion messages, appending each to a vector and doing some math to prevent the
    // code being completely optimzied away.
    let num_messages = 1_000_000_000;
    let mut out = Vec::with_capacity(num_messages);
    let start = Instant::now();
    for i in 0..num_messages {
        // Send the message
        let v = a.send(TestMessage(i as u64));
        out.push(v);
    }
    let elapsed = start.elapsed();
    println!(
        "{:.2} messages/sec",
        num_messages as f64 / elapsed.as_secs_f64()
    );

    system.kill(actor_id);
}

system.get 调用移入循环后,它会降至大约 40,000,000 条消息/秒

// Create a slacktor instance
let mut system = Slacktor::new();

// Create a new actor
let actor_id = system.spawn(TestActor(rand::random::<u64>()));

// Time 1 billion messages, appending each to a vector and doing some math to prevent the
// code being completely optimzied away.
let num_messages = 1_000_000_000;
let mut out = Vec::with_capacity(num_messages);
let start = Instant::now();

for i in 0..num_messages {
    // Retrieve the actor from the system and send a message
    let v = system.get::<TestActor>(actor_id).unwrap().send(TestMessage(i as u64));
    out.push(v);
}

let elapsed = start.elapsed();
println!(
    "{:.2} messages/sec",
    num_messages as f64 / elapsed.as_secs_f64()
);

system.kill(actor_id);

如果我们删除将值推送到向量(并在循环外检索演员引用),则 Rust 编译器能够完全优化循环,并且代码在 100ns 内完成执行

// Create a slacktor instance
let mut system = Slacktor::new();

// Create a new actor
let actor_id = system.spawn(TestActor(rand::random::<u64>()));

// Get a reference to the actor
let a = system.get::<TestActor>(actor_id).unwrap();

// Time 1 billion messages, appending each to a vector and doing some math to prevent the
// code being completely optimzied away.
let num_messages = 1_000_000_000;
let start = Instant::now();

for i in 0..num_messages {
    // Send the message
    a.send(TestMessage(i as u64));
}

let elapsed = start.elapsed();
println!(
    "{:?}",
    elapsed
);

system.kill(actor_id);

在这种情况下,在循环内检索演员引用给我们带来大约 60,000,000 条消息/秒的速度。

Actix 框架的以下等效代码每秒可以处理大约 400,000 条消息,并且不允许 Rust 编译器优化第二个循环。我已经将消息数量减少到 1000 万,因为 10 亿对于 Actix 在合理时间内处理来说太多。

use std::time::Instant;

use actix::prelude::*;

#[derive(Message)]
#[rtype(u64)]
struct TestMessage(pub u64);

// Actor definition
struct TestActor(pub u64);

impl Actor for TestActor {
    type Context = Context<Self>;
}

// now we need to implement `Handler` on `Calculator` for the `Sum` message.
impl Handler<TestMessage> for TestActor {
    type Result = u64; // <- Message response type

    fn handle(&mut self, msg: TestMessage, _ctx: &mut Context<Self>) -> Self::Result {
        msg.0 ^ self.0
    }
}

#[actix::main]
async fn main() {
    let actor = TestActor(rand::random::<u64>()).start();

    let num_messages = 1_000_000;
    let mut out = Vec::with_capacity(num_messages);
    let start = Instant::now();
    
    for i in 0..num_messages {
        let a = actor.send(TestMessage(i as u64)).await.unwrap();
        out.push(a);
    }

    let elapsed = start.elapsed();
    println!("{:.2} messages/sec", num_messages as f64/elapsed.as_secs_f64());

    // Actix won't optimize away
    let num_messages = 1_000_000;
    let start = Instant::now();

    for i in 0..num_messages {
        let _a = actor.send(TestMessage(i as u64)).await.unwrap();
    }

    let elapsed = start.elapsed();
    println!("{:.2} messages/sec", num_messages as f64/elapsed.as_secs_f64());
}

所有这些测试都是使用 cargo --release、Cargo 版本 1.75.0 和 rustc 版本 1.75.0 以及启用 lto(效果最小)运行的。

可以说 Slacktor 为使用它的任何项目几乎不会引入任何开销。

此外,Slacktor 完全可并行化,因此以下使用 Rayon 的代码能够达到大约 45 亿条消息/秒

// Create a slacktor instance
let mut system = Slacktor::new();

// Create a new actor
let actor_id = system.spawn(TestActor(rand::random::<u64>()));

// Get a reference to the actor
let a = system.get::<TestActor>(actor_id).unwrap();

// Time 1 billion messages, appending each to a vector and doing some math to prevent the
// code being completely optimzied away.
let num_messages = 1_000_000_000;
let start = Instant::now();

let _v = (0..num_messages).into_par_iter().map(|i| {
    // Send the message
    a.send(TestMessage(i as u64))
}).collect::<Vec<_>>();

let elapsed = start.elapsed();

println!(
    "{:.2} messages/sec",
    num_messages as f64 / elapsed.as_secs_f64()
);

system.kill(actor_id);

在循环内检索演员引用导致速度降低到大约 30 亿条消息/秒。 parallel_u8 示例能够达到大约 210 亿条消息/秒。

依赖项

~45KB