an F# key-value store

Three Variations on Asynchronous IO in Fredis.net

In an attempt to improve the performance of Fredis.net, to bring it as close a possible to that of the Microsoft Open Tech version of Redis, I implemented three different versions of async message processing. These different async implementations have large differences in performance. The three implementations are

2. hybrid ‘async at the borders’, the first socket read of an incoming message and the final write/flush of a reply are async, all other reads and writes are synchronous.

3. 100% async using SocketAsyncEventArgs, adapted to work with F# Async computation expressions.

The graphs below show the number of requests per second Fredis.net can process for the PING_INLINE, PING_BULK, GET, SET, INCR and MSET commands, for each type of async IO. The number of clients ranges from 1 to 1024. The data was generated by redis-benchmark running on the same machine as Fredis.net.

Surprisingly, to me at least, the hybrid-async/sync option was faster than using fully asynchronous socketAsyncEventArgs (except for PingInline, which i think is a special case, as it does only requires three read/write ops). I suspect what happens is that the first async read pulls-in more bytes than asked for, subsequent synchronous reads are fast as the data is already available and sync reads do not pay the costs of async. Similarly sync writes may be buffered but not sent, before an async flush triggers the socket write op. Because this the code is async ‘at the borders’ there is no thread blocking while waiting for an incoming client message.

Async function calls do more work than the corresponding sync call due to their thread-hopping, continuation calling nature. To quantify async overhead* I used BenchmarkDotNet, and wrote a simple program that compares Stream.AsyncRead, which returns an F# Async, and Stream.Read. I also benchmarked Stream.ReadAsync, which returns a TPL Task, and C# async/await because why not. This benchmark is not intended to measure the advantages of async IO, there is no IO being performed. An array of bytes was written to a MemoryStream, then MemoryStream sync and async read functions were timed by BenchmarkDotNet. (code is at the end of this article)