How Linux::Event::Async Became Faster
The largest improvement did not come from changing the public async/await API. It came from changing how native Stream delivery interacts with Future::AsyncAwait.
The original bottleneck
Previously, every framed message followed this path:
native framer -> wake coroutine -> consume one message -> suspend again
For 100,000 messages, that caused approximately:
100,000 coroutine suspensions
100,000 continuation callbacks
200,000 AWAIT_IS_READY checks
The underlying native consumer was already fast. Repeatedly crossing the native-to-Perl await boundary for every individual message was the expensive part.
The optimizations
Added an end-of-native-drain flush hook
The generic Linux::Event consumer ABI gained an optional flush operation. A consumer can now receive all messages produced during one native socket drain and request a single wakeup when that drain finishes.
The ABI remains compatible with older consumers through its existing struct_size negotiation.
Added bounded native prefetching
Linux::Event::Async::Stream now keeps a small native receive ring containing up to:
64 additional messages
approximately 256 KiB of payload
The boundary may overshoot by one complete frame so a message is never split merely to satisfy the limit.
Batched coroutine wakeups
Messages produced during one native drain are placed into the prefetch ring. The coroutine is resumed once at the drain boundary instead of once for every message.
After resuming, repeated calls to:
await $stream->recv usually complete immediately from the native queue without suspending the coroutine again.
Preserved backpressure and correctness
The queue is deliberately bounded. When the consumer stops draining it, normal Stream pausing and kernel backpressure still apply.
The implementation also preserves:
- message ordering
- cancellation behavior
- receive-generation safety
- queued messages when a consumer temporarily stops
- correct EOF delivery after prefetched messages
- Effect on the await path
For 100,000 messages at the 2.5 KB payload size, coroutine suspensions fell from approximately: 100,000 -> 1,910
That means about 98.1% of receives completed immediately instead of paying a full suspend/resume cycle.
Why it matters
The public programming model remains straightforward:
while (my $message = await $stream->recv) {
process($message);
} Internally, however, Linux::Event now amortizes the Future/async/await machinery across a batch of messages produced by the same native drain. This is the main reason it reached nearly 8x IO::Async throughput at 2.5 KB, while still retaining an advantage at much larger payload sizes.
Blog
This section provides an overview of the blog, showcasing a variety of articles, insights, and resources to inform and inspire readers.
-
How I Beat IO::Async Performance With futures
How Linux::Event::Async Became Faster The largest improvement did not come from changing the public async/await…
-
Hello world!
Welcome to WordPress. This is your first post. Edit or delete it, then start writing!
Leave a Reply
You must be logged in to post a comment.