Speed of concurrency in Rust
In the last post, I have investigated the performance of Erlang and Go to create a large number of processes, or goroutines in the case of Go. The test program would spawn a large number of processes, and then send a termination message to all of them to terminate. The purpose is to see how long it takes to do this work in Erlang and Go.
Today, I’m going to do the same thing in Rust, a new programming language by the Mozilla Foundation. Rust is a safe, concurrent language with some very interesting features. In Rust, the basic concurrency primitive is called task, which is similar to goroutine in Go or process in Erlang. Rust also has built-in feature for cross-task message passing. Again, just like Erlang and Go.
It’s probably not fair to compare Rust to Erlang and Go, as Rust is still in very early stage of development. Its features are still moving targets, therefore, it would be too much to ask that the core of the language, or libraries, be highly optimized.
Whatever, let’s do a comparison anyway. Here is the Rust code of the same program:
/*
* Copyright (C) 2012, xp@renzhi.ca
* All rights reserved.
*/
use std;
import task;
fn main(args: [str]) {
let numThreads = if vec::len(args) == 2u {
option::get(uint::from_str(args[1]))
} else {
10u
};
io::println(#fmt("Number of threads: %u", numThreads));
let port = comm::port::<int>();
let chan = comm::chan::<int>(port);
let mut task_chs = [];
let mut i = 0u;
let t1 = std::time::precise_time_ns();
while i < numThreads {
let task_ch = task::spawn_listener::<int> {|port|
wait_4_cmd(i, port, chan);
};
task_chs += [task_ch];
i += 1u;
}
vec::iter(task_chs) { |task_ch|
comm::send(task_ch, -1);
}
let t2 = std::time::precise_time_ns();
let elapsed = t2 - t1;
let a = (elapsed as f64) / (numThreads as f64) / 1000.0;
io::println(#fmt("Total time: %f ms", ((elapsed as f64)/1000.0/1000.0)));
io::println(#fmt("Average : %f microseconds", a));
ret;
}
fn wait_4_cmd(id: uint, p: comm::port<int>, c: comm::chan<int>) {
let mut res: int;
do {
res = comm::recv(p);
//io::println(#fmt("Task %u received: %d\n", id, res));
} while res != -1;
//io::println(#fmt("Task %u done\n", id));
}
If you look carefully, the code above looks more like the Erlang code. The Go code that we had previously has cheated a little bit. I say that the Go code cheated a bit because it just sent message to a channel, which is listened by all goroutines, while the Erlang code had to maintain the list of process IDs, and the Rust code had to maintain the list of task channels. This takes up more memory and more maintenance, hence, more time.
Here are the result output, running on the same laptop:
xp@shanghai:~/workspace-xp/test-rust$ ./test_concurrency 20000 Number of threads: 20000 Total time: 1254.860085 ms Average : 62.743004 microseconds xp@shanghai:~/workspace-xp/test-rust$ ./test_concurrency 100000 Number of threads: 100000 Total time: 5812.849664 ms Average : 58.128496 microseconds xp@shanghai:~/workspace-xp/test-rust$ ./test_concurrency 200000 Number of threads: 200000 Total time: 10568.965048 ms Average : 52.844825 microseconds
As you can see, Rust takes significantly longer to perform the work. Comparing to the Go result, this is many times slower. But again, this measurement is, by no means, scientific.
Anyways, we’ll keep an eye on this language as it matures.
I ported your code to Rust 0.4. If you’re interested, I posted it at https://gist.github.com/3915251