3 个版本 (破坏性更新)
0.3.0 | 2024年4月27日 |
---|---|
0.2.2 | 2024年4月27日 |
0.2.1 |
|
0.2.0 |
|
0.1.0 | 2024年4月5日 |
在 异步 中排名第 493
每月下载量 124 次
19KB
152 行
关于
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