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...

15 Comments:

  • That's an interesting post. Since you seem to know a fair bit about Visual Basic .NET (even if you don't want to) I'm thinking you can help me out with a program i'm having with a VB.net program I'm writing. It's an ASP.net application that has a page where there's an HTML table being generated. I added the table in the form editor and fill up the table with the automatically created subroutine that Visual Studio made for me in the code. The problem is that the table seems to reinitialize itself after the subroutine finishes. What I mean is, is when I try to access the table in a subroutine I wrote, the table is empty. I was thinking it had to do with scope, but Visual Studio declared the variable for me with a"Protected WithEvents" modifier.

    Thanks.

    By Anonymous Anonymous, at August 15, 2005 8:58 AM  

  • The reality is that its hard to say without seeing the code.

    More than likely what's happening is that you've setup your subroutine to run before the table rows have been added to the table object. ASP.NET is actually not my area of expertise, most of my work is in Windows Forms, so I don't remember the exact order that things happen when a page is loaded. Look through the automatically generated code to see where the rows get added. Its either in the Constructor (New method), or its in response to the page load event. Then make sure your custom method is called after that occurrs.

    Also, changing Private to Protected is not modifying the scope, it is modifying the Visibility, the scope remains the same.

    Hope that helps.

    By Blogger Nick, at August 17, 2005 4:12 PM  

  • Apparently C# sucks also.

    By Anonymous Anonymous, at October 25, 2005 5:29 PM  

  • i ran your code on .net 2.0 and it seems to be fixed.
    Equals Operator: 3.7993655618242E-05 sec
    Equals Function: 3.43619091253218E-05 sec

    By Anonymous Anonymous, at November 20, 2005 5:04 PM  

  • i ran your code on .net 2.0 and it seems to be fixed.
    Equals Operator: 3.7993655618242E-05 sec
    Equals Function: 3.43619091253218E-05 sec

    By Anonymous Anonymous, at November 20, 2005 5:05 PM  

  • Please see the following post regarding my rebuttal to your statements:

    (Rebuttal) Reason #458 Why Visual Basic .NET Sucks
    http://addressof.com/blog/archive/2005/12/07/9298.aspx

    By Anonymous Cory Smith, at December 07, 2005 2:10 AM  

  • The Microsoft.VisualBasic library is not a COM library, it is .NET functions for VB's use..

    You may be thinking of Microsoft.VisualBasic.Compatibility and it's also not a COM library..

    I glad you enjoy the language of your choice, but please don't make silly accusations without checking the facts

    By Anonymous Anonymous, at December 07, 2005 2:56 AM  

  • Well its always good to have more information Cory, thanks for sharing more details... just two comments however.

    You don't actually explain the performance difference that I (and others - check the very first link in my post) have seen.

    Secondly, this does disspell the Microsoft perpetuated myth that the two languages are identical under the hood, and only differ through syntax. The underlying calls that the two make are different, and in fact you show that the VB libraries perform tasks that the C# libraries don't, which could lead to unexpected results. If this is true, then MSDN should have seperate doc libraries for VB.NET implementations of .NET Framework functions and C# ones.

    By Blogger Nick, at December 07, 2005 8:49 AM  

  • I should also say that "Absolute moron" was way out of line here... and I do apologize for that. I wrote this quite a while ago (in blogging terms), so I can't even remember the circumstances that lead to the post. It is very unlike me, which I'm sure is of little consolation to those on the team who might have read this and been insulted.

    I do stand by my performance data, and my musings as to why different functional behavior (as described in Cory's post) would even be used. Equals should mirror = all the time... and should not differ between languages. That just makes good basic sense.

    By Blogger Nick, at December 07, 2005 12:54 PM  

  • Nick,

    It guys like you that give us C# developers and the blogging community a bad name.

    Please check your facts before you posting this drivel.

    By Anonymous Anonymous, at December 08, 2005 8:51 PM  

  • I posted examples show the performance difference between the two languages and include the projects (in VB.NET and C#) to back up those numbers. I also explain why the numbers float between test runs. The way you tested it to come to this conclusion is completely wrong and you should really take the time to look at this again. If you find a flaw in my tests, please let me know.

    By Anonymous Cory Smith, at December 09, 2005 2:22 AM  

  • Cory Smith gives me a forceful reminder of why I read his blog. In two entertaining entries, he first demolishes Nick Schweitzer, and then dances on the head of Michael Campbell...

    By Anonymous Mark Pearce, at December 15, 2005 2:48 AM  

  • Well... I believe assembly language is by far more superior than any language on earth.

    main:
    movl $5, %eax
    movl $1, %ebx
    L1: cmpl $0, %eax //compare 0 with value in eax
    je L2 //jump to L2 if 0==eax (je - jump if equal)
    imull %eax, %ebx // ebx = ebx*eax
    decl %eax //decrement eax
    jmp L1 // unconditional jump to L1
    L2: ret

    In my test, of one iteration, the time took 0.34722941940021E-07 sec

    Thus showing the superiority of assembly language!

    By Anonymous Anonymous, at January 16, 2006 1:25 PM  

  • Can you see what this MS MVP has to say, and if he is wrong in his assertions about your post?

    http://msmvps.com/blogs/bill/default.aspx

    By Anonymous Anonymous, at January 19, 2006 8:03 AM  

  • I am starting to regret following Bill's link here to see what kind of ignorant anti VB.Net postings are out there. I find myself making my first Blog comment ever just because I can't stand to see the crap being discussed. The = operator provides for VB backwards compatibility which includes respecting the Option Compare rules. The .Equals operator does not. Blindly following the "performance tip" to change which one is used is likely to cause some unhealthy side effects in many situations. I am impressed with Nick for being brave enough to publicly display his ignorance in this manner, many would not have that courage.

    By Anonymous Anonymous, at January 27, 2006 1:19 AM  

Post a Comment

<< Home