David Stampher David Stampher - 3 months ago 41
Vb.net Question

VB.Net BeginGetResponse much faster than using Await GetResponseAsync?

I am trying to start using tasks, but I wanted to compare the speed difference when using a standard HttpWebRequest.BeginGetResponse.

From what I have found, it is taking ~600ms to send and complete 100 requests to example.com using BeginGetResponse

However, using Await GetResponseAsync is taking 5x that. Around 3000ms. In production, that really matters alot to me when scaled up. Am I doing something wrong, or is Await GetResponseAsync inherently slower than BeginGetResponse?

Imports System.Net

Public Class Form1
Private sw As New Stopwatch
Private respCounter As Integer
Private iterations As Integer = 100

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
sw.Start()

For i = 1 To iterations
Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com")
Dim state As New RequestState
state.req = req
req.BeginGetResponse(AddressOf respCallback, state)
Next
End Sub

Private Sub respCallback(ar As IAsyncResult)
Dim state As RequestState = ar.AsyncState
state.resp = state.req.EndGetResponse(ar)
state.respStream = state.resp.GetResponseStream
state.respStream.BeginRead(state.buffer, 0, 1024, AddressOf readCallback, state)
End Sub

Private Sub readCallback(ar As IAsyncResult)
Dim state As RequestState = ar.AsyncState
Dim read As Integer = state.respStream.EndRead(ar)
If read > 0 Then
state.respBody += System.Text.ASCIIEncoding.ASCII.GetString(state.buffer, 0, read)
state.respStream.BeginRead(state.buffer, 0, 1024, AddressOf readCallback, state)
Else
state.Dispose()
respCounter += 1
If respCounter = iterations Then
respCounter = 0
sw.Stop()
Debug.WriteLine(sw.ElapsedMilliseconds)
sw.Reset()
End If
End If
End Sub

Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
sw.Start()

For i = 1 To iterations
Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com")
Using resp As WebResponse = Await req.GetResponseAsync
Using sr As New IO.StreamReader(resp.GetResponseStream)
Dim respBody As String = Await sr.ReadToEndAsync
End Using
End Using
respCounter += 1
If respCounter = iterations Then
respCounter = 0
sw.Stop()
Debug.WriteLine(sw.ElapsedMilliseconds)
sw.Reset()
End If
Next

MsgBox("Execution!")
End Sub

End Class

Public Class RequestState
Implements IDisposable

Public req As HttpWebRequest
Public resp As HttpWebResponse
Public respStream As IO.Stream
Public buffer(1024) As Byte
Public respBody As String

#Region "IDisposable Support"
Private disposedValue As Boolean ' To detect redundant calls

' IDisposable
Protected Overridable Sub Dispose(disposing As Boolean)
If Not disposedValue Then
If disposing Then
' TODO: dispose managed state (managed objects).
respStream.Close()
respStream.Dispose()
resp.Close()
End If

' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
' TODO: set large fields to null.
End If
disposedValue = True
End Sub

' TODO: override Finalize() only if Dispose(disposing As Boolean) above has code to free unmanaged resources.
'Protected Overrides Sub Finalize()
' ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
' Dispose(False)
' MyBase.Finalize()
'End Sub

' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above.
Dispose(True)
' TODO: uncomment the following line if Finalize() is overridden above.
' GC.SuppressFinalize(Me)
End Sub
#End Region
End Class

Answer

is Await GetResponseAsync inherently slower than BeginGetResponse?

It's difficult to address your specific performance concern without a good Minimal, Complete, and Verifiable code example. That said…

It seems to me that you're comparing apples and oranges here. First and foremost, there's a major difference in the implementations. In your BeginGetResponse() version, you initiate all of the requests concurrently, so assuming the web server will tolerate it, they complete in parallel. In your GetResponseAsync() version, you only initiate a new request after the previous one completes.

This serialization will necessarily slow everything down.

Beyond that, the BeginGetResponse() version performs all of its work in the IOCP thread pool, while the GetResponseAsync() version uses the single UI thread to handle completion of I/O events. The UI thread is a bottleneck, both because it can only do one thing at a time, and because you have to wait for it to be available from performing other tasks before it can move on to dealing with the I/O completions (a variation on the "can only do one thing at a time" issue).

In addition to that, you also have to deal with the latency involved in the message loop that dequeues the asynchronous completions for execution in the UI thread.

It wouldn't surprise me at all to find that the GetResponseAsync() approach is slower, when used in the way you're using it.

If you want better performance from it, you should probably use ConfigureAwait(false) in your async calls. Of course, this assumes you can otherwise minimize the interaction with the UI thread (e.g. the processing of the results does not actually need direct posting back to the UI thread). But doing so will tell the framework to not bother marshaling completions back to the UI thread. At least in the code you posted, this would be safe, as you don't actually interact with UI objects in the async event handler method.

All that said, when I changed your code so that it would run the GetResponseAsync() version concurrently, I found that at least with the web server I tested with, it worked just as fast as the BeginGetResponse() version. It was able to complete 100 iterations in just over 10 seconds in both cases.

Private Async Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
    sw.Start()

    Dim tasks As List(Of Task(Of String)) = New List(Of Task(Of String))

    For i = 1 To iterations
        Dim req As HttpWebRequest = HttpWebRequest.Create("http://example.com/")

        tasks.Add(ReadResponse(req))
    Next

    Await Task.WhenAll(tasks)

    sw.Stop()
    Debug.WriteLine(sw.ElapsedMilliseconds)
    sw.Reset()
    MsgBox("Execution!")
End Sub

Private Async Function ReadResponse(req As HttpWebRequest) As Task(Of String)
    Using resp As WebResponse = Await req.GetResponseAsync
        Using sr As New IO.StreamReader(resp.GetResponseStream)
            Dim respBody As String = Await sr.ReadToEndAsync

            Return respBody
        End Using
    End Using
End Function

It's possible with a faster web server, you might start to run into the UI-thread-as-a-bottleneck issue, but I would say the primary difference is likely just that the two implementations really aren't even logically the same.

Comments