The benchmarks in my last post had one thing in common: all communication was one sender to one receiver. It’s surprising how often this is sufficient, but sooner or later we are going to need a way to have multiple tasks sending to the same receiver. I’ve been experimenting with two ways of doing many senders to different receivers, and I now have some results to show.
The pipes library includes a
select operation. This lets you listen on several receive endpoints simultaneously. Unfortunately, the single-use nature of endpoints makes
select a little clunky to use. To help alleviate this, I added a
port_set to the library. Port sets allow you to easily treat several receive endpoints as a unit. This allows send to still be very fast, but receive is a little bit slower due to the overhead setting up and tearing down the select operation. The current implementation for
select is O(n) in the number of endpoints, so this works well for small numbers of tasks, but breaks down as things get bigger.
The other option is to slow down the sending end, using something I call a
shared_chan. This is a send endpoint wrapped in an exclusive ARC. Now all the senders have to contend with each other, but the receive side is exactly as cheap as before. For cases where you have a lot of senders that send messages relatively infrequently, this will likely outperform the
port_set approach, at least until
select is faster.
Both of these are sufficient to run the
msgsend benchmark that I talked about at the beginning of all of this. Here are the results, combined with the previous numbers.
|Language||Messages per second||Comparison|
The most obvious thing is that the
port_set version is over twice as fast as Scala, the previous winner. I also re-ran the
chan version for comparison, and it got a little bit faster. There has been quite a bit of churn in Rust recently, so it’s quite possible that these showed up here as better performance.
port_set version proved the most interesting to me. Relying on
select ended up relaxing some of the ordering guarantees. Previously if we had Task A send a message to Task C and then send a message to Task B, and then have Task B wait to receive message to from Task A and then send a message to Task C, we could count on Task C seeing Task A’s message before seeing Task B’s message. With the
port_set, this is no longer true, although we still preserve the order in messages sent by a single task. An easy way to work around this, however, was to rely on pipe’s closure reporting ability. The server could tell when a worker would no longer send any more messages because it would detect when the worker closed its end of the pipe.