Peter Meinl
Peter Meinl

Reputation: 2586

Is Async I/O generally a little slower than sync I/O?

While being good for thread hygiene I expected asynchronous I/O to always be a little slower than synchronous I/O. My tests seem to prove that async I/O sometimes is faster than sync. What am I missing here?

[EDIT] My initial measurements were wrong (I did not delete them to not invalidate comments made).

Here are some measurements with the timing loop fixed:
ADO_DataReaderSync per iteration=4,17ms
ADO_DataReaderASyncReader per iteration=3,55ms (only ExecuteReaderAsync)
ADO_DataReaderASyncRead  per iteration=11,28ms (ExecuteReaderAsync and ReadAsync)
FileIO_ReadToEndSync SmallFile per iteration=3,67ms
FileIO_ReadToEndAsync SmallFile per iteration=8,97ms
FileIO_ReadToEndSync LargeFile per iteration=266,34ms
FileIO_ReadToEndAsync LargeFile per iteration=322,05ms

BUGGY MEASUREMENTS:

ADO_ReadSync elapsed:00:00:00.0012249 per iteration=0,12249ms
ADO_ReadAsync elapsed:00:00:00.0050702 per iteration=0,50702ms
ADO_DataReaderSync elapsed:00:00:00.0090513 per iteration=0,90513ms
ADO_DataReaderASync elapsed:00:00:00.0044125 per iteration=0,44125ms
FileIO_ReadSync LargeFile elapsed:00:00:00.0655596 per iteration=6,55596ms
FileIO_ReadAsync LargeFile elapsed:00:00:00.0003056 per iteration=0,03056ms
FileIO_ReadSync SmallFile elapsed:00:00:00.0005619 per iteration=0,05619ms
FileIO_ReadAsync SmallFile elapsed:00:00:00.0002955 per iteration=0,02955ms

Test code used:

Module Module1
Private Const _connectionString = "Data Source=zulu;Initial Catalog=AdventureWorks;Integrated Security=True"

Sub Main()
    DoTimed("ADO_ReadSync", Sub() ADO_ScalarSync(), 10)
    DoTimed("ADO_ReadAsync", Async Sub() Await ADO_ScalarAsync(), 10)
    DoTimed("ADO_DataReaderSync", Sub() ADO_DataReaderSync(), 10)
    DoTimed("ADO_DataReaderASync", Async Sub() Await ADO_DataReaderASync(), 10)

    Const filePathLargeFile = "O:\Temp\TestFiles\In\todo.txt"
    Const filePathSmallFile = "O:\Temp\TestFiles\In\eula.txt"
    DoTimed("FileIO_ReadSync LargeFile", Sub() FileIO_ReadSync(filePathLargeFile), 10)
    DoTimed("FileIO_ReadAsync LargeFile", Async Sub() Await FileIO_ReadAsync(filePathLargeFile), 10)
    DoTimed("FileIO_ReadSync SmallFile", Sub() FileIO_ReadSync(filePathSmallFile), 10)
    DoTimed("FileIO_ReadAsync SmallFile", Async Sub() Await FileIO_ReadAsync(filePathSmallFile), 10)

    Console.WriteLine("...")
    Console.ReadLine()
End Sub

Function ADO_ScalarSync() As Integer
    Using cnx As New SqlClient.SqlConnection(_connectionString)
        Dim cmd As New SqlCommand("SELECT COUNT(*) FROM Production.Product", cnx)
        cnx.Open()
        Return cmd.ExecuteScalar
    End Using
End Function

Async Function ADO_ScalarAsync() As Task(Of Integer)
    'Beginning in the .NET Framework 4.5 RC, these methods no longer require Asynchronous Processing=true in the connection string
    Using cnx As New SqlClient.SqlConnection(_connectionString)
        Dim cmd As New SqlCommand("SELECT COUNT(*) FROM Production.Product", cnx)
        cnx.Open()
        Return Await cmd.ExecuteScalarAsync
    End Using
End Function

Function ADO_DataReaderSync() As List(Of String)
    Using cnx As New SqlClient.SqlConnection(_connectionString)
        Dim cmd As New SqlCommand("SELECT * FROM Production.Product", cnx)
        cnx.Open()
        Using rdr As SqlDataReader = cmd.ExecuteReader
            Dim productNames As New List(Of String)
            While rdr.Read
                productNames.Add(rdr("Name"))
            End While
            Return productNames
        End Using
    End Using
End Function

Async Function ADO_DataReaderASync() As Task(Of List(Of String))
    Using cnx As New SqlClient.SqlConnection(_connectionString)
        'Await cnx.OpenAsync() 'I would only use .OpenAsync if the DB is commonly down and we would hang on the timeout
        Dim cmd As New SqlCommand("SELECT * FROM Production.Product", cnx)
        cnx.Open()
        Using rdr As SqlDataReader = Await cmd.ExecuteReaderAsync
            Dim productNames As New List(Of String)
            While rdr.Read
                productNames.Add(rdr("Name"))
            End While
            Return productNames
        End Using
    End Using
End Function

Function FileIO_ReadSync(filePath As String) As Long
    Using reader As New StreamReader(filePath)
        Dim fileString = reader.ReadToEnd
        Return fileString.Length
    End Using
End Function

Async Function FileIO_ReadAsync(filePath As String) As Task(Of Long)
    Using reader As New StreamReader(filePath)
        Dim fileString = Await reader.ReadToEndAsync().ConfigureAwait(False)
        Return fileString.Length
    End Using
End Function

End Module

Public Module Timing
Function DoTimed(name As String, operation As Action, Optional iterations As Integer = 1) As TimeSpan
    operation() 'Warmup

    Dim stw = Stopwatch.StartNew
    DoIterate(operation, iterations)
    stw.Stop()
    Console.WriteLine("{0} elapsed:{1} per iteration={2}ms", name, stw.Elapsed, stw.Elapsed.TotalMilliseconds / iterations)
    Return stw.Elapsed
End Function

Sub DoIterate(action As Action, Optional iterations As Integer = 1)
    For i = 0 To iterations - 1
        action()
    Next
End Sub

End Module

Upvotes: 1

Views: 1423

Answers (2)

paxdiablo
paxdiablo

Reputation: 881103

Depends on your definition of slower :-)

It's possible that async I/O will be slower simply because it has to be run in a separate thread of execution which takes time to set up - this of course depends on the implementation. Even if the threads are already running for async I/O such as when you've set them up by using I/O completion ports, the time taken to communicate the information between threads may be a factor.

However, the fact that you can continue doing other things while waiting for the I/O to finish means that the total time taken for all operations may well be less.

So, while the I/O may be slower, the overall time taken for all operations will generally be better.

In the cases where async seems to be faster, that could be an incredibly efficient implementation, caching of information or a dozen other possibilities, one of which may be a faulty benchmark on your part.

Presumably you're using async because you want to do something else (including possibly keeping a GUI thread responsive). If so, it doesn't really matter how slow it is (within reason).

Upvotes: 3

Stephen Cleary
Stephen Cleary

Reputation: 456332

Your timing loop is not waiting for the asynchronous operations to complete, so it's just timing how long it takes to start them.

"Proving" anything using microbenchmarking is quite difficult.

Upvotes: 2

Related Questions