The Coding Monkey

Friday, July 15, 2005

Reason #458 Why Visual Basic.NET Sucks

I can't help it. VB.NET sucks, and yesterday I discovered yet one more reason why it's a poorly developed language. I was forwarded this link from a coworker on how using the equals operator to compare strings was significantly slower than using the .Equals method on strings. I couldn't believe it. How could they be that different? They call the same underlying code right? Unfortunately that blog post doesn't actually say why it's slower, just that it is, and that you should always use the .Equals method instead of the = operator. I however had to investigate. Please be warned. This post turned out to be very long because it contains a lot of code, and emitted IL.

I wrote the following console application to verify that there was a difference:

Option Strict On

 

Imports System.Security

 

Namespace Playground

 

   Public Module VBPlayground

 

        <SuppressUnmanagedCodeSecurity()> _

        Private Declare Auto Function QueryPerformanceCounter Lib "kernel32.dll" (ByRef lpPerformanceCount As Long) As Boolean

        <SuppressUnmanagedCodeSecurity()> _

        Private Declare Auto Function QueryPerformanceFrequency Lib "kernel32.dll" (ByRef lpFrequency As Long) As Boolean

 

        Private m_lFreq As Long

 

        Public Sub Main()

 

            Dim lStart As Long

            Dim lStop As Long

 

            QueryPerformanceFrequency(m_lFreq)

 

            Dim strOne As String = "One"

            Dim strTwo As String = "Two"

 

            QueryPerformanceCounter(lStart)

            Dim bEqualsOp As Boolean = (strOne = strTwo)

            QueryPerformanceCounter(lStop)

            WriteOutTime(lStart, lStop, "Equals Operator")

 

            QueryPerformanceCounter(lStart)

            Dim bEqualsFunc As Boolean = (strOne.Equals(strTwo))

            QueryPerformanceCounter(lStop)

            WriteOutTime(lStart, lStop, "Equals Function")

 

            System.Console.Read()

 

        End Sub

 

        Private Sub WriteOutTime(ByVal lStart As Long, ByVal lStop As Long, ByVal strMsg As String)

            Dim lDiff As Long = lStop - lStart

            Dim lTime As Double = lDiff / m_lFreq

            Console.WriteLine(String.Format("{0}: {1} sec", strMsg, lTime))

        End Sub

 

   End Module

 

End Namespace



So what are the results you ask? They were surprising... at least to me:

Equals Operator: 0.00150214622249476 sec
Equals Function: 1.9555558038801E-06 sec

That's a difference of two orders of magnitude! What on Earth could be going on under the hood to be causing this? So I continued my investigation by looking at my test program in ILDASM to see what IL code was emitted by the compiler. Here's where it got interesting:

.method public static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// Code size 116 (0x74)
.maxstack 3
.locals init ([0] bool bEqualsFunc,
[1] bool bEqualsOp,
[2] int64 lStart,
[3] int64 lStop,
[4] string strOne,
[5] string strTwo)
IL_0000: nop
IL_0001: ldsflda int64 Playground.VBPlayground::m_lFreq
IL_0006: call bool Playground.VBPlayground::QueryPerformanceFrequency(int64&)
IL_000b: pop
IL_000c: ldstr "One"
IL_0011: stloc.s strOne
IL_0013: ldstr "Two"
IL_0018: stloc.s strTwo
IL_001a: ldloca.s lStart
IL_001c: call bool Playground.VBPlayground::QueryPerformanceCounter(int64&)
IL_0021: pop
IL_0022: ldloc.s strOne
IL_0024: ldloc.s strTwo
IL_0026: ldc.i4.1
IL_0027: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.StringType::StrCmp(string,
string,
bool)

IL_002c: ldc.i4.0
IL_002d: ceq
IL_002f: stloc.1
IL_0030: ldloca.s lStop
IL_0032: call bool Playground.VBPlayground::QueryPerformanceCounter(int64&)
IL_0037: pop
IL_0038: ldloc.2
IL_0039: ldloc.3
IL_003a: ldstr "Equals Operator"
IL_003f: call void Playground.VBPlayground::WriteOutTime(int64,
int64,
string)
IL_0044: nop
IL_0045: ldloca.s lStart
IL_0047: call bool Playground.VBPlayground::QueryPerformanceCounter(int64&)
IL_004c: pop
IL_004d: ldloc.s strOne
IL_004f: ldloc.s strTwo
IL_0051: callvirt instance bool [mscorlib]System.String::Equals(string)
IL_0056: stloc.0
IL_0057: ldloca.s lStop
IL_0059: call bool Playground.VBPlayground::QueryPerformanceCounter(int64&)
IL_005e: pop
IL_005f: ldloc.2
IL_0060: ldloc.3
IL_0061: ldstr "Equals Function"
IL_0066: call void Playground.VBPlayground::WriteOutTime(int64,
int64,
string)
IL_006b: nop
IL_006c: call int32 [mscorlib]System.Console::Read()
IL_0071: pop
IL_0072: nop
IL_0073: ret
} // end of method VBPlayground::Main

Holy cow! This means that if you use the equals operator... you're actually calling into old VB6 libraries using COM Interop. When this occurs, you get a major lag because you have to marshal the string out of the managed memory into unmanaged memory. Using the .Equals method on the other hand keeps everything in managed memory and does a much faster comparison. That explains the difference. My question is... Who was the absolute moron on the VB.NET compiler team who made that decision?

Of course I couldn't stop there. I had to write the same program in C# in order to verify that they don't do anything nearly as stupid. Here is the same program in C#... I believe I got them as close as humanly possible to keep the test valid:

using System;

using System.Security;

using System.Runtime.InteropServices;

 

namespace Playground

{

   class CSPlayground

   {

      [SuppressUnmanagedCodeSecurity, DllImport( "kernel32.dll" )]

      private static extern bool QueryPerformanceCounter( ref long lpPerformanceCount );

 

      [SuppressUnmanagedCodeSecurity, DllImport( "kernel32.dll" )]

      private static extern bool QueryPerformanceFrequency( ref long lpFrequency );

 

      private static long m_lFreq = 0;

 

      [STAThread]

      public static void Main()

      {

         long lStart = 0;

         long lStop  = 0;

 

         QueryPerformanceFrequency( ref m_lFreq );

 

         string strOne = "One";

         string strTwo = "Two";

 

         QueryPerformanceCounter( ref lStart );

         bool bEqualsOp = ( strOne == strTwo );

         QueryPerformanceCounter( ref lStop );

         WriteOutTime( lStart, lStop, "Equals Operator" );

 

         QueryPerformanceCounter( ref lStart );

         bool bEqualsFunc = ( strOne.Equals( strTwo ) );

         QueryPerformanceCounter( ref lStop );

         WriteOutTime( lStart, lStop, "Equals Function" );

 

         System.Console.Read();

      }

 

      private static void WriteOutTime( long lStart, long lStop, string strMsg )

      {

         long lDiff   = lStop - lStart;

         double lTime = (double)lDiff / m_lFreq;

         Console.WriteLine( String.Format( "{0}: {1} sec", strMsg, lTime ) );

      }

   }

}



Here is the output of this program:

Equals Operator: 9.77777901940051E-06 sec
Equals Function: 2.23492091872012E-06 sec

Aha! Nick... there is a difference there too. The operator takes about 4 times longer than the .Equals method. True... but at least its not 2 hundred times different like with VB.NET. So why is there a difference with C#? Here is the IL for this program:

.method public hidebysig static void Main() cil managed
{
.entrypoint
.custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 )
// Code size 110 (0x6e)
.maxstack 3
.locals init ([0] int64 lStart,
[1] int64 lStop,
[2] string strOne,
[3] string strTwo,
[4] bool bEqualsOp,
[5] bool bEqualsFunc)
IL_0000: ldc.i4.0
IL_0001: conv.i8
IL_0002: stloc.0
IL_0003: ldc.i4.0
IL_0004: conv.i8
IL_0005: stloc.1
IL_0006: ldsflda int64 Playground.CSPlayground::m_lFreq
IL_000b: call bool Playground.CSPlayground::QueryPerformanceFrequency(int64&)
IL_0010: pop
IL_0011: ldstr "One"
IL_0016: stloc.2
IL_0017: ldstr "Two"
IL_001c: stloc.3
IL_001d: ldloca.s lStart
IL_001f: call bool Playground.CSPlayground::QueryPerformanceCounter(int64&)
IL_0024: pop
IL_0025: ldloc.2
IL_0026: ldloc.3
IL_0027: call bool [mscorlib]System.String::op_Equality(string,
string)

IL_002c: stloc.s bEqualsOp
IL_002e: ldloca.s lStop
IL_0030: call bool Playground.CSPlayground::QueryPerformanceCounter(int64&)
IL_0035: pop
IL_0036: ldloc.0
IL_0037: ldloc.1
IL_0038: ldstr "Equals Operator"
IL_003d: call void Playground.CSPlayground::WriteOutTime(int64,
int64,
string)
IL_0042: ldloca.s lStart
IL_0044: call bool Playground.CSPlayground::QueryPerformanceCounter(int64&)
IL_0049: pop
IL_004a: ldloc.2
IL_004b: ldloc.3
IL_004c: callvirt instance bool [mscorlib]System.String::Equals(string)
IL_0051: stloc.s bEqualsFunc
IL_0053: ldloca.s lStop
IL_0055: call bool Playground.CSPlayground::QueryPerformanceCounter(int64&)
IL_005a: pop
IL_005b: ldloc.0
IL_005c: ldloc.1
IL_005d: ldstr "Equals Function"
IL_0062: call void Playground.CSPlayground::WriteOutTime(int64,
int64,
string)
IL_0067: call int32 [mscorlib]System.Console::Read()
IL_006c: pop
IL_006d: ret
} // end of method CSPlayground::Main

You have to remember that in .NET, all operators are actually static methods. So when you are using the = operator, it has to probably do a cast or two, then it calls the .Equals method on one of them, which incurrs a bit of overhead to push items on the stack. That probably would be enough to account for the small difference in times.

However at least my faith was reaffirmed that C# is far superior to VB.NET. But the lesson was learned very well. Always use the .Equals method on strings in VB.NET.

Update: Read the comments for more information, and my responses...