Post

Visual Basic .NET Internals

  • Published: May 2003
  • Authors: Derek Hatchard (Hatchard Software) and Scott Swigart (3 Leaf Solutions)

Summary: The Microsoft .NET Framework has opened a new world for Visual Basic developers. Visual Basic .NET combines the power of the .NET Framework and the common language runtime with the productivity benefits that are the hallmark of Visual Basic. Although the Visual Basic .NET language looks the same on the surface, the internal implementation of the language and the compiler have evolved significantly since Visual Basic 6. This paper examines those internals so that you can make choices that affect developer productivity, code safety, and execution speed.

In addition to examining the compiler and language internals, this paper explores potential performance “gotchas” and discusses best practices. This will enable you to achieve optimal performance from your Visual Basic .NET code. However, the guidelines in this paper should be used in conjunction with a performance-oriented application design. Proper application architecture, well-coded algorithms, and appropriate use of asynchronous methods will always play a larger role in overall performance than tweaking language usage (except in potential bottlenecks, such as long-running loops).

Design Goals of Visual Basic .NET

Visual Basic .NET is not a language that stands in isolation — it is the latest offering in the Visual Basic product line. Visual Basic as a language and development tool has a strong history and a large following. Two major design goals for Visual Basic .NET were to preserve identical functionality for identical language keywords and to provide the same types of productivity enhancements that have made previous versions of Visual Basic so popular.

Compatibility

An important goal for Visual Basic .NET was to retain as much backward compatibility as possible, while providing more power and flexibility for the developer. This was accomplished by providing “helper” methods such as Rnd (which is a static method on an imported class) and intrinsic language features, such as CInt, to provide functionality identical to that of Visual Basic 6. While there are a few specific methods that don’t provide the same performance characteristics as Visual Basic 6, you can fully expect that a Visual Basic .NET application will significantly outperform its Visual Basic 6 counterpart. In addition, some of the functionality provided by the Visual Basic Runtime is also available through the System name space. In most cases there is no significant performance difference when using the Visual Basic Runtime vs. the System name space; and to reiterate, whether you use the Visual Basic Runtime, or the System name space, you can expect your Visual Basic .NET code to significantly outperform functionally identical Visual Basic 6 code. It is also worth mentioning that the Visual Basic Runtime methods often perform additional checks that are not done by the System methods, and this can result in more robust and reliable code.

While Visual Basic .NET provides a language that Visual Basic 6 developers are immediately familiar with, it has also been enhanced to provide full support for object-oriented constructs. In addition, Visual Basic .NET has complete access to the Framework Class Library (FCL). These enhancements provide the Visual Basic .NET developer with an extensive set of new tools to apply when building solutions.

Developer Productivity

Visual Basic has always been a productivity tool with an emphasis on convenience, ease of use, and protecting programmers from common mistakes. In most cases, Visual Basic .NET provides productivity enhancements that do not carry any performance penalty. For example, the Handles keyword provides a simple declarative way to perform event wiring that performs just as well as the more verbose syntax used by other languages. For the scenarios where Handles does not provide the needed flexibility, you can use the AddHandler syntax to perform event wiring programmatically. This is a classic example of the Visual Basic .NET philosophy: to provide you with a very productive development model but always let you drop to a lower level when you need to take more control.

Because Visual Basic .NET provides you with choices, it is important for you to know when there are productivity vs. runtime performance trade offs. For example, Visual Basic .NET provides implicit type conversions and integer overflow checking by default. Implicit type conversion relieves programmers from writing explicit type conversions but can also result in poorer performance in certain cases. For example implicit conversion takes significantly longer than DirectCast when converting an Object to an Integer (This is also known as unboxing; see the section Conversion Functions, CType, DirectCast, and System.Convert for further details). Integer overflow checking certainly results in safer code, but those checks are performed at run time and do carry a performance penalty. In many cases the Visual Basic Runtime or the Visual Basic .NET compiler provides logic that you might otherwise have to write yourself. By understanding the inner workings of Visual Basic .NET, you can make informed decisions about productivity, safety, and performance.

Visual Basic .NET Compiler

All Framework compilers generate Intermediate Language (IL) that is later compiled to native code by the Just-In-Time (JIT) compiler. The same IL executes exactly the same on the common language runtime no matter which language was originally used. Runtime differences between Visual Basic .NET code and equivalent code in another language are caused by the respective compilers generating different Intermediate Language.

Comparing compilers requires an understanding of the default behavior of each compiler to ensure that equivalent options are being used. Similar code segments compiled with dissimilar compiler options can have drastically different performance characteristics. But as will be shown in the remainder of this section, Visual Basic .NET code performs identically to C# code when both are compiled with equivalent options.

Debugging and nop Instructions

Visual Basic .NET allows you to set breakpoints on non-executing lines of code such as End If, End Sub, and Dim statements. To facilitate this popular debugging technique, the compiler inserts nop instructions as placeholders for the non-executing lines of code (since non-executing lines are not translated into IL instructions). The nop instruction is a “no operation” instruction — it does not perform any meaningful work yet can consume a processing cycle.

You can observe this if you launch Visual Studio .NET, create a new Visual Basic .NET application, compile it using the default Debug configuration, and then view the assembly with the MSIL Disassembler (Ildasm.exe).

.method public static void  Main() cil managed
{
  .entrypoint
  .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = ( 01 00 00 00 ) 
  // Code size       14 (0xe)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldstr   "Hello World"
  IL_0006:  call    void [mscorlib]System.Console::WriteLine(string)
  IL_000b:  nop
  IL_000c:  nop
  IL_000d:  ret
} // end of method Module1::Main

By default, the Visual Basic .NET compiler (vbc.exe) does not generate nop instructions. They are only generated when the /debug option for the compiler is explicitly set, which is exactly what the Debug configuration in Visual Studio .NET does. When you compile using the Release configuration in Visual Studio .NET, the /debug option is not used so the nop instructions are not generated.

In contrast, the C# compiler does not produce as many nop instructions even when compiling with the /debug option. Since the Visual Basic .NET and C# compilers do not behave equivalently when used with the /debug option, you should compile using the Release configuration mode when comparing compilers, especially for performance comparisons.

Integer Overflow Checking

Unlike the other Framework SDK compilers, vbc.exe generates overflow checks for integer operations by default (for Visual Studio .NET users, this includes both the Debug and Release configurations).

As a result, benchmarks results have occasionally been posted that do not accurately compare the performance of Visual Basic .NET to other languages, because the benchmarks do not take into account integer overflow checking and other default compiler settings. The following code is an example:

Visual Basic .NET

1
2
3
4
5
6
7
8
9
10
Public Shared Sub RunLoop()  
  Dim I As Integer
  Dim max As Integer = 500000000
  Dim sum As Integer = 0

  For I = 1 To max
    sum += 1
  Next

End Sub 

C#

1
2
3
4
5
6
7
8
9
10
11
public static void RunLoop()
{
  int i;
  int max = 500000000;
  int sum = 0;
  int loopLimit = max;
  for (i = 1; i <= loopLimit; i++)
  {
    sum += 1;
  }
}

The code segments look equivalent (the extra C# variable loopLimit is used to match Visual Basic .NET behavior, which is to copy max to a temporary local variable). Both methods require two integer addition operations — one for the increment of the sum variable and one for the increment of the loop counter. By default, the Visual Basic .NET compiler will generate the IL instruction add.ovf for these addition operations. The add.ovf instruction includes an overflow check and throws an exception if the sum exceeds the capacity of the target data type. By contrast, the default output of the C# compiler is the IL instruction add, which does not include an overflow check. Using these default compiler options would mean a performance advantage for the C# version of RunLoop because of the overflow checks done by the Visual Basic .NET.

If an overflow is considered an error in your application, the add.ovf instruction results in safer code. Otherwise, integer overflow checking can be disabled for the Visual Basic .NET compiler using the /removeintchecks option. Alternatively, integer overflow checking can be enabled for the C# compiler using the /checked option. These options can also be controlled in Visual Studio .NET using the project properties dialog box.

An accurate comparison of the two RunLoop versions requires comparing the IL generated with equivalent compiler options for overflow checking. The following graph shows the average number of seconds that RunLoop executed in an informal performance test (on a modestly equipped computer) for both the Visual Basic .NET and C# compilers, with and without integer overflow checks. As can be clearly seen, equivalent compiler options yield equivalent performance results.

VB RunLoop Graph

These identical performance results are not surprising if you look at the IL generated by each compiler, which is almost identical for the two RunLoop versions. The table below shows the resulting IL when integer overflow checking is disabled for both compilers. The only difference is the order of the instructions on lines IL_0008, IL_0009, and IL_000a (the only implication is that the Visual Basic .NET version has an extra integer value on the stack while max is being copied to a temporary variable).

Tip for Reading IL: The common language runtime is stack-based. A two-operand operation such as add pops the two top values from the stack and adds them. In the left column, line IL_000e pushes the contents of a local variable (sum) onto the stack. The next line pushes the 4-byte integer constant 1 onto the stack. The next line pops the two values, adds them, and pushes the result on the stack. The next line, IL_0011, pops the result from the stack and stores it in a local variable (sum again).

vbc.exe-generated IL

.method public static void  RunLoop() cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init ([0] int32 I,
           [1] int32 max,
           [2] int32 sum,
           [3] int32 _Vb_t_i4_0)
  IL_0000:  ldc.i4     0x1dcd6500
  IL_0005:  stloc.1
  IL_0006:  ldc.i4.0
  IL_0007:  stloc.2
  IL_0008:  ldc.i4.1
  IL_0009:  ldloc.1
  IL_000a:  stloc.3
  IL_000b:  stloc.0
  IL_000c:  br.s       IL_0016
  IL_000e:  ldloc.2
  IL_000f:  ldc.i4.1
  IL_0010:  add
  IL_0011:  stloc.2
  IL_0012:  ldloc.0
  IL_0013:  ldc.i4.1
  IL_0014:  add
  IL_0015:  stloc.0
  IL_0016:  ldloc.0
  IL_0017:  ldloc.3
  IL_0018:  ble.s      IL_000e
  IL_001a:  ret
} // end of method Class1::RunLoop

csc.exe-generated IL

.method private hidebysig static void  RunLoop() cil managed
{
  // Code size       27 (0x1b)
  .maxstack  2
  .locals init ([0] int32 i,
           [1] int32 max,
           [2] int32 sum,
           [3] int32 loopLimit)
  IL_0000:  ldc.i4     0x1dcd6500
  IL_0005:  stloc.1
  IL_0006:  ldc.i4.0
  IL_0007:  stloc.2
  IL_0008:  ldloc.1
  IL_0009:  stloc.3
  IL_000a:  ldc.i4.1
  IL_000b:  stloc.0
  IL_000c:  br.s       IL_0016
  IL_000e:  ldloc.2
  IL_000f:  ldc.i4.1
  IL_0010:  add
  IL_0011:  stloc.2
  IL_0012:  ldloc.0
  IL_0013:  ldc.i4.1
  IL_0014:  add
  IL_0015:  stloc.0
  IL_0016:  ldloc.0
  IL_0017:  ldloc.3
  IL_0018:  ble.s      IL_000e
  IL_001a:  ret
} // end of method Class1::RunLoop

Unique Visual Basic Constructs

There are a number of constructs that are unique to the Visual Basic language. These include Modules, Handles, Optional Parameters, and Late Binding. Used correctly, these constructs can provide significant developer productivity enhancements over other languages that you can use with the .NET Framework. In addition, these constructs provide compatibility with Visual Basic 6 code. This section will explore the internal operation of these constructs so that you can determine when they are appropriate in your development.

Modules

Visual Basic programmers are accustomed to placing subroutines and functions for global use in modules. In Visual Basic .NET, modules are available using the new Module keyword:

1
2
3
4
5
6
7
Module Module1

    Function GlobalFunction() As Boolean

    End Function

End Module

Visual Basic .NET modules are actually special classes even though they are not created with the Class keyword. The compiler generates a NotInheritable (sealed) class that contains only Shared methods. A module can contain a Shared constructor but no instance constructors. The compiler does not generate a default constructor nor can you declare a private constructor. Therefore you cannot create an object from a module.

A module is implicitly imported into every source file in a project so the methods and properties of the module can be accessed without being fully qualified. This provides the same functionality as global methods and variables available in prior versions of Visual Basic.

Modules give you a quick and convenient mechanism for exposing shared functions and variables. When programming against the .NET Framework, many other languages require you to explicitly create a NotInheritable class, explicitly make all methods Shared, and create a private constructor (most compilers create a default constructor if you do not at least provide a private one).

Recommendation: Since modules fit completely within the object-oriented conventions of the .NET Framework, Visual Basic .NET programmers should not hesitate to use them when appropriate.

Event Handling

As previously mentioned, Visual Basic .NET provides a convenient declarative technique for wiring event handlers in your code. First you declare a variable using the WithEvents keyword. Then you designate one or more methods as an event handler using the Handles keyword. Alternatively, Visual Basic .NET allows you to wire event handlers programmatically using the AddHandler keyword. The following code sample shows Timer objects wired using both techniques:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Public Class Class1

  Public WithEvents timer1 As System.Timers.Timer
  Public timer2 As System.Timers.Timer

  Sub New()
    timer1 = New System.Timers.Timer()
    WireTimer2()
  End Sub

  Sub WireTimer2()
    timer2 = New System.Timers.Timer()
    AddHandler timer2.Elapsed, AddressOf timer2_Elapsed
  End Sub

  Public Sub timer1_Elapsed(ByVal sender As Object, _
                            ByVal e As System.Timers.ElapsedEventArgs) _
                            Handles timer1.Elapsed
    ' Respond to event
  End Sub

  Public Sub timer2_Elapsed(ByVal sender As Object, _
                            ByVal e As System.Timers.ElapsedEventArgs)
    ' Respond to event
  End Sub

End Class

For the code sample shown above, the compiler replaces the original timer1 field with a public property called timer1 and a new private field called _timer1. The setter method for the new timer1 property, set_timer1, contains the IL for wiring the timer1_Elapsed method to the timer1.Elapsed event. The first part of set_timer1 removes any previous event handler wiring. This step is important if you need to change the object that timer1 refers to — you have to write this logic yourself when using AddHandler. The actual event wiring comes in the form of three IL instructions:

ldvirtftn  instance void TimerHandlers.Class1::timer1_Elapsed(object,
                    class [System]System.Timers.ElapsedEventArgs)
newobj     instance void 
[System]System.Timers.ElapsedEventHandler::.ctor(object, 
                                                 native int)
callvirt   instance void [System]System.Timers.Timer::add_Elapsed(
                    class [System]System.Timers.ElapsedEventHandler)

Compare the statements from set_timer1 with the IL generated from the AddHandler statement:

ldvirtftn  instance void TimerHandlers.Class1::timer2_Elapsed(object,
                    class [System]System.Timers.ElapsedEventArgs)
newobj     instance void 
[System]System.Timers.ElapsedEventHandler::.ctor(object,
                                                 native int)
callvirt   instance void [System]System.Timers.Timer::add_Elapsed(
                    class [System]System.Timers.ElapsedEventHandler)

As you can see, the same three IL instructions are used for wiring events programmatically (AddHandler) and declaratively (WithEvents and Handles). Code using the declarative approach tends to be easier to write and maintain but has a few limitations: it cannot be used for Shared events (a WithEvents variable is required); it cannot be used with arrays of WithEvents variables; and WithEvents variables must be early-bound. AddHandler can always be used when declarative event wiring does not give you the flexibility you need.

AddHandler vs. Handles is a good example of the Visual Basic philosophy of giving you full access to the facilities provided by the .NET Framework, but also providing a simpler syntax for the most common scenarios.

Recommendation: You should use Handles throughout your code, and only use AddHandler for specific scenarios where it is required.

Late Binding

Late binding is the process of matching an Object variable to the implementation of the type of object it references at run time. All languages that support the .NET Framework can support late binding by using System.Object variables and methods from the System.Reflection name space for invoking object members. Visual Basic .NET provides implicit support for late binding by allowing you to write code as if objects were early bound. The compiler generates IL to use Visual Basic Runtime features that do the reflection work for you. Consider the following call to the Show method of an instance of Form1, which derives from System.Windows.Forms.Form:

1
2
3
Dim o As Object
o = New Form1()
o.Show()

The IL generated by the compiler is a single (though expensive) method call:

call   void [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.LateBinding::LateCall(object,
                                        class [mscorlib]System.Type,
                                        string,
                                        object[],
                                        string[],
                                        bool[])

Late binding consumes significantly more resources than early binding (locating implementation at compile time) since the common language runtime is forced to perform type checking and member lookup at run time. However, late binding provides benefits that make it extremely useful in many situations. Consider an application that automates the printing of files and keeps a log of each file printed. The files may be Word documents, Excel spreadsheets, or some other type of file. Rather than recoding the same logic for each type of file, or requiring objects to implement the same interface (which you may not be able to do if you are not the author of the objects) you could use one method (for example, PrintOutAndLog) that takes an Object as a parameter and calls the object’s PrintOut method. The application can be extended to use object models for any file type — as long as there is a PrintOut method, PrintOutAndLog can print the file.

The PrintOutAndLog method can be placed in a class (or module) in its own source file with Option Strict Off to allow late binding. The rest of your application can then use Option Strict On to prevent late binding in other files. Refer to the section Use Option Strict On for more details on Option Strict.

Recommendation: Late binding should not be used in most applications, but you should use Visual Basic’s implicit late binding for scenarios where you would otherwise have to write reflection code.

Optional Parameters

Visual Basic .NET is unique in its support for both optional parameters and method overloading. Both features are handled by the Visual Basic .NET compiler. For method overloads, the compiler matches the supplied argument list to the appropriate overloaded version of the method and generates a call to that method version in the IL. For example, consider the following call to MessageBox.Show:

1
System.Windows.Forms.MessageBox.Show("Overload")

The IL generated by the compiler for this method call is:

ldstr      "Overload"
call       valuetype 
[System.Windows.Forms]System.Windows.Forms.DialogResult
    [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)

The compiler matches the Visual Basic .NET source code to a specific overloaded version of MessageBox.Show and calls that version in the IL.

Unlike overloaded methods, a method with optional parameters is a single version with default values for one or more parameters. If a parameter value is not supplied, the default value is inserted by the compiler. The default value is inserted in the method call in the calling assembly. If the default values are changed, the calling assembly will continue to use the original default values. Unlike Visual Basic 6, you cannot determine whether parameter values were included in the original method call or if the default values were provided by the compiler. Consider the following call to InputBox:

1
InputBox("Prompt")

The IL generated for this method call is as follows:

ldstr      "Prompt"
ldstr      ""
ldstr      ""
ldc.i4.m1
ldc.i4.m1
call       string 
      [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::InputBox(string,
                                                                string,
                                                                string,
                                                                int32,
                                                                int32)

The default values for the last four parameters were inserted by the compiler and are used for the method call when the IL executes. The InputBox method cannot differentiate between the above call and the following:

1
InputBox("Prompt", "")

Visual Basic no longer includes the IsMissing function to determine if an optional parameter was omitted in the calling code nor is there any special runtime handling of optional parameters. All optional parameters must have a default value for the compiler to insert in the IL when a parameter is omitted.

Since overloaded methods and methods with optional parameters are treated like any other method call at run time, there is no performance-related reason to choose one over the other. However, bear in mind that other languages, most notably C#, do not support optional parameters. Overloaded methods are generally more appropriate than optional parameters if a component is to be used from C# code.

Support for optional parameters gives Visual Basic .NET programmers a distinct productivity advantage over programmers using other languages when dealing with certain components (e.g., COM methods that include optional parameters). For example, compare the following Visual Basic .NET and C# versions of the same call to the SaveAs method of a Word document object:

1
2
3
4
' Visual Basic .NET
Dim wrd As New Microsoft.Office.Interop.Word.DocumentClass
Dim fileName As String = "c:\automation.doc"
wrd.SaveAs(fileName)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// C#
Microsoft.Office.Interop.Word.DocumentClass wrd;
wrd = new Microsoft.Office.Interop.Word.DocumentClass();
object fileName = "c:\automation.doc";
object optional = System.Reflection.Missing.Value;
wrd.SaveAs(ref fileName, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional, 
ref optional);

The Visual Basic .NET compiler inserts the default value, System.Reflection.Missing.Value, automatically whereas the C# compiler does not. The C# programmer has no choice but to include every optional parameter in the method call.

Recommendation: Visual Basic will provide significant advantages over other languages when calling methods that accept optional arguments. Be aware that if you author methods that accept optional arguments, and those methods are called from other languages, those languages will have to supply a parameter for each argument position.

Visual Basic Runtime

If you are leveraging the strengths of the Visual Basic .NET language, you will want to use the Visual Basic Runtime classes and methods. This section highlights the performance characteristics of the most prominent features of the Visual Basic Runtime and makes recommendations for choosing between Visual Basic Runtime features and alternatives in the System name space.

Overview of the Visual Basic Runtime in Visual Basic .NET

Visual Basic developers have long associated the term “Visual Basic Runtime” with a set of core library files, such as msvbvm60.dll, that are required for Visual Basic 6.0 (and prior) programs to run. In Visual Basic .NET, the term “Visual Basic Runtime” refers to the set of classes in the Microsoft.VisualBasic name space. The Visual Basic Runtime provides the underlying implementation for global Visual Basic functions and language features such as Len, IsDate, and CStr. And though the new Visual Basic Runtime provides similar facilities as its predecessors, it is entirely managed code (developed in Visual Basic .NET) that executes on the common language runtime. Furthermore, the Visual Basic Runtime is part of the .NET Framework, so it is never something separate that your application has to carry or deploy.

Many of the methods in the Visual Basic Runtime actually use methods and properties from the System name space (for example, Len() returns String.Length). In some cases you can achieve equivalent results by accessing .NET Framework class library classes directly, but typically you will be more productive using the Visual Basic Runtime when authoring your code. In many cases the Visual Basic Runtime wrappers provide additional functionality that you would have to code yourself if using the System name space directly. In other cases, such as IsDate, there is no directly equivalent functionality in the System name space.

If for some reason you choose to use only System name space classes instead of Visual Basic Runtime features and carefully avoid all language features supported by the Visual Basic Runtime, you can end up with IL that does not use any resources from the Visual Basic Runtime. You need to be aware, however, that you cannot choose whether or not your program references the Visual Basic Runtime, even if your programs do not use it. Although you can remove the project-wide import of the Microsoft.VisualBasic name space in Visual Studio .NET, the compiler still requires the presence of Microsoft.VisualBasic.dll in order to support language features that could appear in your code (such as late binding and string comparison). Microsoft.VisualBasic.dll is part of the Framework so it is reasonable for the compiler to assume it will be available. Furthermore, your assembly manifest will still reference the Microsoft.VisualBasic assembly although it is not loaded at run time if its resources are not used (meaning no extra overhead is incurred for the reference).

As the rest of this section will illustrate, avoiding the Visual Basic Runtime for performance reasons is generally unnecessary and the extra effort is likely to impede productivity, particularly for programmers with a Visual Basic 6 background.

Tip for Understanding the Visual Basic Runtime: All of the Visual Basic Runtime methods can be examined by opening Microsoft.VisualBasic.dll with Ildasm.exe. The .NET Framework class library features described in this section can be examined by opening mscorlib.dll and System.dll with Ildasm.exe.

String Comparisons

The Visual Basic .NET comparison operators (=, >, >=, <, <=, <>) translate into calls to the Visual Basic Runtime method Microsoft.VisualBasic.CompilerServices.StringType.StrCmp(String, String, Boolean) when used to compare strings. The Boolean parameter provides support for culture-aware case-insensitive string comparisons — for example, when using the Option Compare Text statement.

StrCmp calls either String.CompareOrdinal or Globalization.CompareInfo.Compare depending on the Boolean value it receives. The System.String class also provides a Compare method, which is simply a wrapper that calls Globalization.CompareInfo.Compare (further references to the Compare method in this section mean Globalization.CompareInfo.Compare).

Since StrCmp is a lightweight wrapper around .NET Framework class library methods, performance differences between using StrCmp and using the NET Framework class library methods directly are insignificant. The major caveat is that the Boolean value in the call to StrCmp (as generated by the Visual Basic .NET compiler) is influenced by both the /optioncompare compiler option and the Option Compare statement (if present in a source file, Option Compare overrides /optioncompare for that source file). Algorithms based on assumptions about case-sensitivity or culture awareness should use CompareOrdinal or Compare directly rather than the intrinsic string comparison operators.

The following observations on string comparisons will be of interest to some Visual Basic .NET programmers:

  • The Visual Basic .NET = operator and String.Equals are not equivalent. For case-sensitive equality comparisons, CompareOrdinal (and therefore StrCmp) tends to be marginally faster than String.Equals because String.Equals considers the culture associated with the current thread when doing its evaluation. This requires slightly more work than the comparison done by String.CompareOrdinal, which ignores national language or culture.
  • The Compare method can do case-sensitive culture-aware comparisons. The intrinsic comparison operators cannot do these comparisons because StrCmp uses CompareOrdinal for case-sensitive comparisons, which ignores culture.

Recommendation: Use the language operators when comparing strings under most circumstances. Call CompareOrdinal or Compare directly when performing a large number of successive comparisons or when Option Compare settings might adversely affect the algorithm.

Len Method

The Visual Basic Runtime contains an overloaded version of Len for each of the built-in value types (including DateTime) as well as versions for String and Object.

For all value types, Len returns the number of bytes for the data type. For the built-in value types such as Integer, the overloaded versions simply return the appropriate constant value for its accepted parameter type. For example, the IL for Len(Integer) is shown in the following table.

   
IL_0000: ldc.i4.4` Load 4-byte integer constant 4
IL_0001: ret Return top value on the stack

Note The primitive integer types in Visual Basic .NET are Byte (1 byte), Short (2 bytes), Integer (4 bytes), and Long (8 bytes). In Visual Basic 6, Integer is 2 bytes and Long is 4 bytes.

Len(String) checks for a non-null reference and then returns String.Length (the number of characters in the string).

Len(Object) returns the number of bytes required to write the object to disk. If the object is a reference to a String, Len returns the number of characters (characters in String are 2-byte Unicode characters).

Recommendation: Use Len for value types and objects as needed. Use Len for strings since reading String.Length has no significant performance advantage over calling Len.

Replace Method

The String data type is immutable, meaning that a String instance cannot be changed once it is created. String operations in the .NET Framework cause one or more new strings to be created in the managed memory heap. If the original string is discarded (that is, there are no more accessible references to it), the string becomes eligible for future garbage collection. A large number of successive string operations can result in an overwhelming number of abandoned strings requiring garbage collection, which negatively impacts application performance.

The Visual Basic .NET Replace method is designed to exactly replicate the functionality of the Visual Basic 6 Replace function by splitting the source string using the search string as a delimiter. The resultant array of strings is joined with the replacement string as the delimiter. For n replacements, the Replace method generates at least (n + 1) strings that will be discarded immediately and therefore require garbage collection. As a result, using the Replace method over large strings requiring thousands of replacements can become a performance bottleneck. Consider some of the following alternatives when performing a large number of replace operations.

Note Test runs were measured by obtaining the before and after values of DateTime.Now.Ticks. System.GC.Collect() was called immediately before each read of DateTime.Now.Ticks so that extra garbage collection costs were measured, not just base execution time.

For case-sensitive replacements, the String.Replace and StringBuilder.Replace methods are extremely efficient. Use StringBuilder.Replace when performing multiple replacements on the “same” string for a slight performance gain over String.Replace.

For case-insensitive replacements, the Regex class provides a Replace method using a regular expression (string pattern) which can be arbitrarily complex. Regex.Replace is therefore much more powerful than the Visual Basic Runtime Replace method. The performance of Regex.Replace varies with the size of string and number of replacements to be performed. Regex.Replace tended to perform marginally worse than the Visual Basic Runtime Replace method for simple replacements during testing for this paper. However, Regex.Replace uses significantly less memory and generates significantly fewer objects requiring garbage collection.

For case-insensitive replacements, consider using an algorithm that scans the original string using CompareInfo.IndexOf (or scans an uppercase copy of the original string using String.IndexOf) and builds a new string in a StringBuilder using the Append method. The performance will be better than the Replace method, significantly less memory will be used, and fewer objects will be created, thus requiring less garbage collection. A sample algorithm is shown in the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Public Shared Function FastReplace(Expr As String, Find As String, _
                                   Replacement As String) As String
  Dim builder As System.Text.StringBuilder
  Dim upCaseExpr, upCaseFind As String
  Dim lenOfFind, lenOfReplace As Integer
  Dim currentIndex, prevIndex As Integer

  builder = New System.Text.StringBuilder()
  upCaseExpr = Expr.ToUpper()
  upCaseFind = Find.ToUpper()
  lenOfFind = Find.Length
  lenOfReplace = Replacement.Length
  currentIndex = upCaseExpr.IndexOf(upCaseFind, 0)
  lenOfReplace = 0
  Do While currentIndex >= 0
    builder.Append(Expr, prevIndex, currentIndex - prevIndex)
    builder.Append(Replacement)
    prevIndex = currentIndex + lenOfFind
    currentIndex = upCaseExpr.IndexOf(upCaseFind, prevIndex)
  Loop  If prevIndex < Expr.Length Then
    builder.Append(Expr, prevIndex, Expr.Length - prevIndex)
  End If
  Return builder.ToString()
End Function

Recommendation: Use a consistent approach for case-sensitive and case-insensitive replacements. The Visual Basic Runtime Replace method is suitable for both unless dealing with large strings requiring thousands of replacements. For thousands of replacements, use an algorithm such as FastReplace for case-insensitivity; use String.Replace for individual case-sensitive replacements; and use StringBuilder.Replace for multiple case-sensitive replacement operations on the same string.

Mid, Left, and Right Methods

The Mid, Left, and Right methods in Visual Basic .NET all call String.Substring. However, the Visual Basic Runtime methods are optimized to prevent string allocation for some common cases. For example, the following simple case is 85 percent faster using the Visual Basic Runtime methods versus the System methods:

1
2
3
4
5
6
7
  Dim s1, s2 As String 
  s2 = "abc"
 
  ' fast, prevents string allocation
  s1 = Left(s2, 3)
  ' SubString is not optimized for this case
  s1 = s2.SubString(0, 3)

In addition, calling Substring directly does not offer any significant performance benefit, and is more complex than Left and Right.

Recommendation: Use Mid, Left, and Right, which are found in the VisualBasic namespace.

Mid Assignment Statement

The Mid assignment statement allows you to replace a specified number of characters in a string with characters from another string. For example, the following code causes var to refer to a new string object containing “ABC XYZ GHI”:

1
2
  Dim var As String = "ABC DEF GHI"
  Mid(var, 5, 3) = "XYZ"

The Mid assignment statement is implemented in the Visual Basic Runtime using a StringBuilder. Its performance characteristics are very close to using String.Substring with concatenations such as:

1
  var = var.Substring(0, 4) + "XYZ" + var.Substring(7)

Each use of the Mid assignment statement requires the creation of a new StringBuilder instance. For numerous substitutions, using a single StringBuilder instance directly is more efficient.

Recommendation: Use the Mid statement for convenience or readability for individual substitutions and in migrated Visual Basic 6 code. Use a StringBuilder instance directly for numerous substitutions in the same string.

CurDir Method

The Visual Basic Runtime contains two overloaded versions of CurDir that call methods in the System.IO.Directory name space. The first accepts no parameters and calls System.IO.Directory.GetCurrentDirectory. The other accepts a drive letter as a Char and calls System.IO.Path.GetFullPath.

Recommendation: Use CurDir for convenience or consistency with legacy Visual Basic code. Calling the System.IO.Directory methods directly does not offer significant performance benefits.

Dir Method

In order to reproduce Visual Basic 6 functionality, the Visual Basic .NET implementation for the Dir keyword requires several methods which store assembly-level state information. The Visual Basic Runtime implementation of Dir is obligated to perform a number of checks and manipulations with the path passed to Dir(String, Microsoft.VisualBasic.FileAttribute) in order to emulate the Visual Basic 6 Dir function. Calls to Dir are eventually handled by classes in the System.IO name space once the Visual Basic Runtime methods have done their initial processing. Although the Dir function is convenient, accessing the System.IO name space directly is significantly faster. For example, retrieving a list of file names from C:\WINNT on a Windows 2000 Server installation took approximately 4 times longer using Dir than using System.IO.FileSystemInfo and System.IO.DirectoryInfo, the same classes used by Dir.

However, occasional calls to Dir will not introduce any significant performance degradation.

Recommendation: Use Dir in upgraded code for consistency. Use Dir for convenience in applications that do not access the file system extensively. Use System.IO name space classes for applications that access the file system extensively or when optimal file I/O performance is critical.

FileOpen, FileSystemObject, and System.IO Namespace

The Visual Basic 6 file I/O commands have been removed as language keywords and implemented as method calls in the Visual Basic Runtime library. Open and Close have been renamed FileOpen and FileClose (when upgrading a Visual Basic 6 project, Open and Close commands are translated to FileOpen and FileClose method calls by the Visual Basic 6 Upgrade Wizard automatically). These I/O methods from the Visual Basic Runtime library are wrappers for objects and method calls from the System.IO name space. Although these I/O methods are fine for use in upgraded code, new development should generally use the System.IO name space directly to avoid the overhead cost of emulating the Visual Basic 6 file I/O commands. In testing for this paper, reading 260,000 lines from a 5-MB file line by line was 15 to 20 times faster using System.IO.StreamReader.ReadLine than reading with Microsoft.VisualBasic.LineInput.

ASP and VBScript developers use the FileSystemObject class from the Microsoft Scripting Runtime for file access. Using these COM classes in Visual Basic .NET will cause managed applications to incur extra interoperability overhead costs that can be avoided by using the System.IO classes. In general, if the Framework provides a mechanism for performing some operation, either through the Microsoft.VisualBasic namespace, or through the System name space, you should use the Framework rather than invoking objects through COM Interop.

Recommendation: Use Visual Basic Runtime file I/O commands for convenience. Use FileSystemObject in upgraded code only. Use the System.IO name space directly when performance is critical.

Shell Method

Shell has been the conventional way to launch another application from a Visual Basic application. The System name space contains the Diagnostics.Process class which can be used to produce the same results as Shell. Shell is simpler to use, requiring only one line of code while the Process class can require several lines to produce the same behavior:

1
Shell("c:\winnt\notepad.exe", AppWinStyle.MaximizedFocus, False, -1)
1
2
3
4
5
Dim p As System.Diagnostics.Process = New Process()
p.StartInfo.FileName = "c:\winnt\notepad.exe"
p.StartInfo.WindowStyle = ProcessWindowStyle.Maximized
p.StartInfo.UseShellExecute = False
p.Start()

The code shown above would still launch Notepad without changing UseShellExecute but p.Start() would call ShellExecuteEx rather than CreateProcess as called by Shell.

Although more coding is required to use it, the Process class is more powerful than the Shell method. Like its Visual Basic 6 precursor, Microsoft.VisualBasic.Interaction.Shell is only capable of launching files that are executable files (such as .exe and .bat files). Its call chain leads to CreateProcess in the Win32 API. The Process class also calls CreateProcess when Process.StartInfo.UseShellExecute is explicitly set to False. Otherwise Process.Start uses the operating system shell to launch the specified file. If the file is non-executable or a URI, the OS shell opens the file or resource using the default action for that file or URI type (that is, the default verb — usually open). Using the operating system shell in Visual Basic 6 requires a Declare statement to call the API function directly.

Using the Process class with UseShellExecute = False is sometimes slightly faster than the Shell method but the very slight performance gain is negligible and insignificant against the overhead of loading a new process unless you are launching dozens of short-lived processes from the UI thread.

Recommendation: For launching only executable files, there is no compelling performance reason to choose Process over Shell, and Shell is often more productive. Use Process when features not provided by Shell are required (for example, I/O redirection or UseShellExecute).

MsgBox versus MessageBox.Show

In Visual Basic .NET, the MsgBox method is a Visual Basic Runtime wrapper around a call to the MessageBox.Show method from the System.Windows.Forms name space. MsgBox does some extra work to emulate the behavior of the Visual Basic 6 MsgBox function before culminating in a call to MessageBox.Show. The minute cost of these emulating steps is insignificant, particularly when compared to the time it takes a user to react to a dialog box.

Note MsgBox returns the same integer values returned by MessageBox.Show. Strictly speaking MessageBox.Show returns a DialogResult value and MsgBox returns a MsgBoxResult value. The values in these enumerations have the same meanings: OK = 1, Yes = 6, No = 7, and so on. You can also use CType to convert a MsgBoxResult to a DialogResult.

The choice between MsgBox and MessageBox is a matter of consistency. If you are migrating a Visual Basic 6 application to Visual Basic .NET, there is no compelling reason to replace calls to MsgBox with MessageBox.Show.

Recommendation: Use MsgBox throughout your code.

InputBox Method

The Visual Basic Runtime implements the InputBox method using an internal class that extends Windows.Forms.Form. It contains a Label control, two Button controls (OK and Cancel), and a TextBox control. The implementation is approximately what you would have to code yourself to emulate the Visual Basic 6 InputBox method. There is no performance disadvantage to using InputBox and it is more convenient than building your own.

Recommendation: Use InputBox.

Rnd versus System.Random

The Microsoft.VisualBasic name space includes the Rnd method for generating pseudo-random single precision numbers (4 bytes) using the same algorithm as the Rnd function in Visual Basic 6. The .NET Framework class library provides the System.Random class which includes methods for generating random Integer, Double, and Byte values.

One instance of System.Random can produce pseudo-random numbers significantly faster than repeated calls to Rnd. In informal testing for this paper for Integer, Single, and Double values, System.Random was approximately ten times faster than Rnd. However, it took 5000 random number generations just to get a measurable difference. Since most applications rarely require more than a few random numbers, there is generally no compelling performance reason to choose System.Random over Rnd.

Recommendation: Use Rnd for migrated Visual Basic 6 code, for convenience or consistency in new development, or when the Visual Basic 6 algorithm is required. Use System.Random if thousands of random numbers are being generated, or if System.Random behavior is desired (for example, time-based seed by default).

Working with Dates

Although the underlying format of Date has changed, the Visual Basic keywords for working with dates are still available. The Visual Basic .NET data type Date is actually an alias for System.DateTime. The DateTime type is a value type so beware of boxing and unboxing when treating date values as reference types (see section Use Value Types but Avoid Excessive Boxing for an explanation of value types and boxing).

A DateTime object is represented by an eight-byte integer. In Visual Basic 6, a Date is represented by an eight-byte double precision number. Refer to the MSDN Documentation on DateTime.FromOADate and DateTime.ToOADate for help on converting between the Visual Basic 6 and .NET Framework representations.

Generally speaking, the Visual Basic keywords for date operations are wrappers for methods and properties from the System.DateTime class and do not introduce significant performance penalties. For example, the Visual Basic keyword Now returns the DateTime value returned by System.DateTime.Now. There are some cases, however, where the Visual Basic Runtime library methods perform extra steps in order to match Visual Basic 6 behavior. If your code does a considerable amount of date manipulation, consider using the System.DateTime methods directly. For example, calling DateDiff (DateInterval.Day, DateA, DateB) results in the following (reverse engineered from IL to Visual Basic):

1
2
3
  Dim local3 As TimeSpan
  local3 = DateB.Subtract(DateA)
  Return CLng(Math.Round(Conversion.Fix(local3.TotalDays)))

The same result can be obtained more efficiently by calling TimeSpan.Days directly rather than using DateDiff, which must round TotalDays (TimeSpan.TotalDays returns a double-precision value, not an integer).

Also be aware that while IsDate is a significant productivity enhancement, the IsDate method leads to a call to DateTime.Parse and that the exception thrown by Parse is handled automatically if the expression is not a valid date. Because throwing exceptions is an expensive way to control program flow, IsDate should be used cautiously over large collections of data that are likely to contain a significant number of invalid dates. The following style of using IsDate should also be minimized since it leads to a duplicate parsing operation:

1
2
3
  If IsDate(variableHere) = True Then
    dateVar = System.DateTime.Parse(variableHere)
  End If

Recommendation: IsDate is an excellent choice for validating user input and most other date validation. For applications that loop over a large number of dates that are likely to contain invalid date formats, consider using System.DateTime methods directly. For other date operations, use the Visual Basic .NET date functions.

Conversion Functions, CType, DirectCast, and System.Convert

Visual Basic .NET includes data type conversion keywords, many of which are carried over from Visual Basic 6. But unlike the Visual Basic 6 functions, these keywords are not function calls but intrinsic language features. The keywords CBool, CByte, CChar, CShort, CInt, CLng, CSng, CDbl, CDec, CDate, CObj, and CStr map to Visual Basic Runtime method calls, .NET Framework class library method calls, or IL type conversion instructions. The exact method call or IL instructions generated depends on the expression against which the conversion is being applied. Some conversions are optimized away, such as CInt(123.45) which is replaced with the integer constant 123 in the IL. This is an example where using the Visual Basic Runtime results in better performance than using the System name space. CInt("123") becomes a call that leads to calls to System.Double.Parse then System.Math.Round. CStr(4853) is ultimately handled by System.Int32.ToString. Conversions that are not optimized away eventually lead to methods in the System name space, but there is no significant performance benefit when using the System name space methods directly. Furthermore, the Visual Basic compiler is able to perform certain optimizations on conversions using the language keywords that it does not perform on conversions done through the System name space.

The CType keyword is a special conversion feature that requires both an expression and a type:

1
  CType(expression, type)

When used with primitive types such as Integer, the CType keyword behaves exactly like the other conversion keywords for the primitive types. For example, CType("123", Integer) produces the same IL as CInt("123"). CType can also be used to convert object references and to convert between value types such as enumerations:

1
2
3
4
  Dim returnValue As MsgBoxResult
  returnValue = MsgBox("Do you agree?", vbYes + vbNo)
  Dim result As DialogResult
  result = CType(returnValue, DialogResult)

The CType keyword will automatically perform type coercion if a conversion is defined between the original type and the target type.

For conversions between reference types that the compiler cannot guarantee to be successful (for example, Object to DataSet), CType becomes the IL instruction castclass. Visual Basic .NET includes the DirectCast keyword which also translates into a castclass instruction for reference type conversions. For conversions that are guaranteed to succeed, such as System.ApplicationException to System.Exception, the castclass instruction is unnecessary. In such cases, using CType or leaving out an explicit conversion statement altogether allows the compiler to simply copy object references, avoiding the extra castclass processing.

For Object to String conversions, DirectCast is faster than CType. DirectCast becomes a single castclass instruction rather than a call to the StringType.FromObject method.

For Object to value type conversions (that is, unboxing), DirectCast is again faster than CType. DirectCast becomes a single unbox instruction rather than a call to the Visual Basic Runtime FromObject method for the target type (for example, IntegerType.FromObject). In testing for this paper, converting Object to Integer with DirectCast was more than 10 times faster than CType.

Note Using DirectCast to convert from Object assures the compiler that you are confident of the type embedded in the Object variable. Unlike CType and the other Visual Basic .NET conversion keywords, DirectCast does not perform type coercion on the embedded value, even if a conversion is defined. Calling DirectCast(expression, Double) on an Object that contains a boxed Integer value will cause a runtime exception even though an Integer to Double conversion is defined.

The System.Convert class is another alternative for performing type conversions. It contains methods for converting to the primitive types and strings. In some cases the intrinsic Visual Basic .NET conversion keywords map to calls into the System.Convert class.

Recommendation: For most conversions, use the intrinsic language conversion keywords (including CType) for brevity and clarity and to allow compiler optimizations when converting between types. Use DirectCast for converting Object to String and for extracting value types boxed in Object variables when the embedded type is known (that is, coercion is not necessary).

UBound vs. Array.GetUpperBound

The UBound method has been the conventional way to determine the upper bound of an array in Visual Basic. Arrays in Visual Basic .NET implicitly inherit from System.Array, which contains the GetUpperBound method. The Visual Basic .NET implementation of UBound simply calls the GetUpperBound method of the array that it receives as a parameter. Therefore there is no significant cost for using UBound rather than calling GetUpperBound directly.

Recommendation: Use UBound for all development.

Cost of Loading Microsoft.VisualBasic.dll

The library in which the Visual Basic Runtime is defined, Microsoft.VisualBasic.dll, is less than 300 KB in .NET Framework version 1.0 and version 1.1. A loaded application that uses the Visual Basic Runtime library will tend to use several hundred kilobytes more than an application that does not.

Although the Visual Basic Runtime uses some additional memory, there is no perceptible performance penalty. As shown earlier in this paper, Visual Basic .NET and C# applications compiled with comparable compiler options run equally fast.

Microsoft.VisualBasic.Compatibility Namespace

The Visual Basic 6.0 Compatibility library is distinct from the Visual Basic Runtime. The Microsoft.VisualBasic.Compatibility name space is used by the tools that upgrade Visual Basic 6.0 code to Visual Basic .NET. It is a bridge to support Visual Basic 6 features that are not directly supported by the .NET implementation of Visual Basic. Unlike the Visual Basic Runtime, the compatibility library is not implicitly referenced by all Visual Basic .NET applications. When you upgrade a Visual Basic 6 project to Visual Basic .NET, the upgrade wizard adds a reference to Microsoft.VisualBasic.Compatibility.

The compatibility classes should not be used for new development. The Microsoft.VisualBasic.Compatibility name space adds a layer of complexity to your Visual Basic .NET application and introduces some minimal performance costs that could be eliminated by recoding portions of the application. In addition, the Compatibility name space often contains many classes that wrap COM objects, and as stated earlier, depending on COM objects is not as optimal as a pure managed implementation. However, the library itself is not overly inefficient and the performance costs will probably not be noticeable in most applications. The rest of this section highlights a few prevalent Visual Basic 6 compatibility features and their performance characteristics.

Fixed-Length Strings

In Visual Basic 6, a string could be declared with a fixed size like this:

1
Dim aFixedLenString As String * 100

Visual Basic .NET uses immutable strings so the benefits of using fixed-length strings in Visual Basic 6, such as for buffers during multiple string operations, are no longer applicable. The compatibility library provides the FixedLengthString class for code migrated from Visual Basic 6:

1
Dim myFixedLengthString As New VB6.FixedLengthString(100)

Changing the Value property of a FixedLengthString causes two temporary strings to be created and then immediately discarded (the original string is discarded as well). Code segments with repeated use of FixedLengthString objects can be recoded to use StringBuilder classes if performance becomes an issue. The StringBuilder class provides a constructor that lets you define the initial capacity of the string.

Control Arrays

Visual Basic .NET does not support control arrays in the Visual Basic 6 style. The compatibility library provides a set of container classes that mimic control arrays by associating indices with controls. Controls are added to a container, such as an instance of VB6.ButtonArray. The container handles the events raised by its contained controls and then raises its own corresponding events. So when a user clicks on a Button, the Button.Click event is handled by the container control, which then raises its own Click event.

The control array classes add an extra layer of indirection for event handling that is not discernible in a typical GUI application. The bulk of the work for a control array class happens during initialization. The extra cost of adding a control to a control array container is measurable with a large number of controls but certainly not noticeable for a user. For example, adding 500 Button controls to a control array takes about 20 percent longer than just adding 500 Button controls to a form, but the difference is a mere fraction of a second.

In Visual Basic 6, you were required to use a control array if you wanted a single method to handle events from multiple objects. With Visual Basic .NET, you can point events from many objects (even different types of objects) at the same method with the Handles keyword:

1
2
3
4
5
Private Sub ColorChoice_CheckedChanged(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles optRed.CheckedChanged, _
    optGreen.CheckedChanged, optBlue.CheckedChanged

End Sub

Recommendation: Only use the compatibility library control arrays when upgrading Visual Basic 6 code. Otherwise, use the Visual Basic .NET Handles keyword.

ZOrder Method

The compatibility library provides a ZOrder method that wraps calls to Control.BringToFront and Control.SendToBack. The Control class methods should be used for new development; however, use of the ZOrder method in upgraded code does not have a significant performance cost.

Optimizing Visual Basic .NET Applications for Performance

The remainder of this paper provides some guidance on optimizing your Visual Basic .NET applications for better performance. Some of these tips are specific to Visual Basic. The others deal with using the .NET Framework class library and common type system appropriately. In most cases, the recommendations here are most applicable for frequently used code segments such as those inside loops. This list is not exhaustive. Consult the Additional Resources section of this paper for sources of more information and tips.

Use Option Strict On

Setting Option Strict On prevents implicit type conversions that can be very costly at run time, including implicit boxing and unboxing of value types. It also prevents implicit type conversions that are not guaranteed to succeed, such as String to Integer. The following implicit conversion will compile with Option Strict Off even though it contains an invalid type conversion that will fail at run time:

1
2
3
4
Dim o As Object
Dim I As Integer
o = "Not an Integer!!!"
I = o

When using Option Strict On, potentially unsafe conversions such as the one shown above cannot be done implicitly — they cause a compile error. This compile-time assistance can help you catch logic errors up front (at compile time) as well as avoid expensive run-time type checking and coercion logic. The most expensive and potentially dangerous conversions that are disallowed with Option Strict are those that convert to/from Object and to/from String. Refer to the section Conversion Functions, CType, DirectCast, and System.Convert for details on explicit conversions in Visual Basic .NET.

Option Strict On also prevents implicit late binding that can seriously degrade the performance of an application (refer to the section Late Binding for more details). Late binding should be avoided except for situations where you would be using reflection to perform the same operations.

When working with the same method or property from different types using late binding, consider using interfaces instead, if possible. Although method calls will still have to be mapped to the proper implementation at run time, they will execute faster than a method call on a late-bound object requiring type checking and member lookup. For example, this code uses the IDataAdapter interface rather than Object to work with both SqlDataAdapter and OleDbDataAdapter instances:

1
2
3
4
5
6
7
8
Dim adapter As IDataAdapter
Dim sqlOriginal As New SqlClient.SqlDataAdapter()
adapter = DirectCast(sqlOriginal, IDataAdapter)
adapter.TableMappings.Add("Table1", "Orders")

Dim oleDbOriginal As New OleDb.OleDbDataAdapter()
adapter = DirectCast(oleDbOriginal, IDataAdapter)
adapter.TableMappings.Add("Table1", "Orders")

Use Option Compare Binary

Unless there is a compelling reason to do expensive case-insensitive comparisons throughout your applications, always work with Option Compare Binary rather than Option Compare Text. With Option Compare Binary, string comparisons are done by comparing the underlying numeric value of each constituent Char. With Option Compare Text, string comparisons must ignore case and use comparison rules for the current culture. The extra processing required for case-insensitive culture-aware comparisons is inherently more expensive than simple ordinal comparisons.

Use Structured Exception Handling Rather Than On Error Statement

Visual Basic still supports the On Error statement for unstructured error handling. The .NET Framework also provides structured exception handling for dealing with run-time errors. Code encapsulated within a Try block can be associated with multiple Catch sections that handle specific types of exceptions. Unlike use of the On Error statement, your programs will not incur significant performance penalties using structured exception handling with Try blocks.

Hint: For a dramatic performance difference, compare a Try block against an On Error Resume Next statement inside a loop compiled with the Release configuration in Visual Studio .NET. You do not have to handle any errors to see the performance advantage of a TryCatch block.

Throw Exceptions Frugally

Although Try blocks introduce minimal overhead, throwing an exception is an expensive operation that should only be used in exceptional circumstances. Do not use exceptions to control program flow such as to return a success code from a method. Use output parameters instead (or a return value).

Use StringBuilder

String concatenations inside a loop are an obvious example where immutable strings can have a serious performance cost in your Visual Basic .NET applications. A faster alternative requiring fewer memory allocations is the StringBuilder class in the System.Text name space.

The StringBuilder class provides a mutable string-like object which tends to be more efficient than String for appending, inserting, removing, and replacing characters. However, a StringBuilder object maintains a resizable buffer that requires more overhead than a simple String. The StringBuilder class should only be used when the number of string operations justifies the extra overhead.

In testing for this paper, about 200 successive concatenations of 50-character strings were required to get a measurable difference between String.Concat and StringBuilder.Append. However, there are no ideal numbers for choosing between String and StringBuilder. Actual performance differences will vary depending on the number of operations, the size of the strings involved, the amount of heap space being used by other objects (which can prompt more frequent garbage collection), and the hardware on which an application is running.

The relative performance advantage of StringBuilder over String increases as either the number of operations or the size of the strings (or both) increase. As a general rule, choose StringBuilder over String when performing multiple operations on strings of more than a few dozen characters or when doing more than a few dozen operations.

Use Value Types but Avoid Excessive Boxing

Value types include primitive types such as Integer, Double, and DateTime as well as types defined in structures and enumerations. Local value type variables are normally allocated space in the memory stack rather than the more costly managed memory heap (where reference types are created). Working with value types is generally faster than working with reference types. However, in some situations you can only use references types. An example of this situation is a method that only accepts Object as a parameter type. If a value type is passed as an argument, a new object is automatically created in the heap and the value of the argument is embedded in the object. This is called boxing. Although the occasional boxing operation is not detrimental to performance, excessive use of value types treated as reference types can lead to significant performance degradation.

Be Cautious with the IIf Function

The IIf function in the Visual Basic Runtime library behaves the same as the IIf function in Visual Basic 6 (and all other functions) — if a method call is used as an argument, the method is called and the return value passed on as the parameter value. The IIf function accepts three parameters and has the following signature:

1
2
3
IIf(ByVal Expression As Boolean, 
    ByVal TruePart As Object, 
    ByVal FalsePart As Object)

The function returns TruePart if Expression is true. Otherwise the function returns FalsePart. If method calls are used for TruePart or FalsePart, the methods are called regardless of the value of Expression. This can have negative performance implications when arguments are calls to long-running methods or when IIf is used inside a loop. However, if you are not invoking methods for the TruePart or FalsePart, then IIf can streamline your code.

“As New” Variable Declaration / Instantiation

Although this section does not contain a Visual Basic .NET performance tip per se, programmers using Visual Basic .NET should be aware of the following:

1
  Dim var As New <type>

is simply a shortcut for:

1
  Dim var As <type> = New <type>

Many Visual Basic 6 programmers have avoided the use of the As New style of declaring and instantiating object variables because of the performance penalty in Visual Basic 6. In Visual Basic .NET, the As New syntax does not introduce a performance penalty and can be used freely.

Conclusion

Visual Basic .NET seeks to maximize developer productivity without limiting any of the power of the .NET Framework. It accomplishes this goal through a number of unique language constructs and through an extensive run-time library. Visual Basic .NET also defaults to code safety by enabling integer overflow checking by default. However, you as the developer have the ultimate say about safety versus performance.

The Visual Basic Runtime provides many familiar utility methods that will speed development and aid in porting your Visual Basic 6 code. You can choose to leverage the conveniences of the Visual Basic Runtime which are provided in addition to the System classes. In most instances, using the Visual Basic Runtime methods will not have any affect on overall application performance, and it will enhance consistency with other Visual Basic code. The only exception is where you utilize certain Visual Basic Runtime functions for a very large number of operations. This paper has documented the specific functions that you should be aware of in these circumstances. It is also worth noting that if an application is experiencing performance bottlenecks, it is not likely to be remedied by simply utilizing the System methods, as performance and scalability are products of the overall application architecture.

Visual Basic also provides facilities for migrating existing Visual Basic code to the .NET Framework. The Microsoft.VisualBasic.Compatibility name space contains classes and methods specifically for this task. It is recommended that these classes be used when upgrading, but they should not be used for new code.

As you have seen, Visual Basic provides complete access to the Framework plus a number of additional tools for you to leverage as a developer. These come in the form of language constructs (late binding, optional parameters, and so on), and a runtime which includes methods and compiler services. With an understanding of the internals of Visual Basic, you can decide how to leverage the Visual Basic feature set to achieve the optimal balance of developer productivity, code safety, and execution speed.

Additional Resources

Appendix: Summary of Recommendations and Best Practices

The following provides a summary of the recommendations in this document listed alphabetically according to feature.

As New

Use it. Same as using = New <type>.

Compatibility Library (Microsoft.VisualBasic.Compatibility namespace)

Do not use for new development. Use for upgraded code only.

Conversion Keywords (CInt, CLng, and so on)

For most conversions, use the intrinsic language conversion keywords (including CType) for brevity and clarity and to allow compiler optimizations when converting between types. Use DirectCast for converting Object to String and for extracting value types boxed in Object variables when the embedded type is known (that is, coercion is not necessary).

CType()

For most conversions, use the intrinsic language conversion keywords (including CType) for brevity and clarity and to allow compiler optimizations when converting between types. Use DirectCast for converting Object to String and for extracting value types boxed in Object variables when the embedded type is known (that is, coercion is not necessary).

CurDir()

Use CurDir for convenience or consistency with legacy Visual Basic code. Calling the System.IO.Directory methods directly does not offer significant performance benefits.

Date functions

Use them. IsDate is an excellent choice for validating user input and most other date validation. For applications that loop over a large number of dates that are likely to contain invalid date formats, consider using System.DateTime methods directly. For other date operations, use the Visual Basic .NET date functions.

Debug Configuration

Use Release configuration for faster applications in production.

Dir()

Use Dir in upgraded code for consistency. Use Dir for convenience in applications that do not access the file system extensively. Use the System.IO name space classes for applications that access the file system extensively or when optimal file I/O performance is critical.

DirectCast()

For most conversions, use the intrinsic language conversion keywords (including CType) for brevity and clarity and to allow compiler optimizations when converting between types. Use DirectCast for converting Object to String and for extracting value types boxed in Object variables when the embedded type is known (that is, coercion is not necessary).

Exceptions

Use exceptions instead of OnError. Throw exceptions frugally. Do not use exceptions to control normal execution flow—exception-throwing is expensive.

FileOpen()

Use Visual Basic Runtime file I/O commands in upgraded code and for convenience. Use the System.IO namespace for optimal performance.

FileSystemObject (from Scripting Runtime)

Use Visual Basic Runtime FileSystemObject in upgraded code only. Use the File methods or System.IO namespace directly for new development.

IIf()

Use it, but be aware that it can have side-effects and performance implications when arguments are methods.

InputBox()

Use InputBox. There is no performance disadvantage to using InputBox and it is more convenient than building your own InputBox.

Integer Overflow Checking (Using)

Disable if integer overflows are not errors or are guaranteed never to occur in your application.

IsDate()

Use it. IsDate is an excellent choice for validating user input and most other date validation. For applications that loop over a large number of dates that are likely to contain invalid date formats, consider using System.DateTime methods directly.

Late Binding (Using)

Use in situations where you would have to write the equivalent reflection code.

Left()

Use Mid, Left, and Right when convenient.

Len()

Use Len for value types and objects as needed. Use Len for strings since reading String.Length has no significant performance advantage over calling Len.

MessageBox.Show()

Use MsgBox throughout your code.

Microsoft.VisualBasic.Compatibility

Do not use for new development. Use for upgraded code only.

Mid() Assignment Statement

Use the Mid statement for convenience or readability for individual substitutions and in migrated Visual Basic 6 code. Use a StringBuilder instance directly for numerous substitutions in the same string.

Mid()

Use Mid, Left, and Right when convenient.

Modules (Using)

Use it in instances where you would simply be creating utility classes with Static methods.

MsgBox()

Use MsgBox throughout your code.

Option Compare Binary

Use it whenever case-sensitive non culture-aware (i.e., ordinal) string comparisons are acceptable.

Option Strict On

Use it except in code files that must use implicit type conversions and/or implicit late binding.

Optional Parameters (Using)

Use when more convenient or appropriate than overloaded methods except when methods are called from languages that do not support optional parameters (for example, C#).

Release Configuration

Use Release configuration for faster applications in production.

Replace()

Use a consistent approach for case-sensitive and case-insensitive replacements. The Visual Basic Runtime Replace method is suitable for both unless dealing with large strings requiring thousands of replacements. For thousands of replacements, use an algorithm such as FastReplace for case-insensitivity; use String.Replace for individual case-sensitive replacements; and use StringBuilder.Replace for multiple case-sensitive replacement operations on the same string.

Use Mid, Left, and Right when convenient.

Rnd()

Use Rnd for migrated Visual Basic 6 code, for convenience or consistency in new development, or when the Visual Basic 6 algorithm is required. Use System.Random if thousands of random numbers are being generated, or if System.Random behavior is desired (that is, time-based seed by default).

Shell()

For launching only executable files, there is no compelling performance reason to choose Process over Shell, and Shell is often more productive. Use Process when features not provided by Shell are required (for example, I/O redirection or UseShellExecute).

String comparison operators(=, >, >=, <, <=, <>)

Use the language operators when comparing strings under most circumstances. Call CompareOrdinal or Compare directly when performing a large number of successive comparisons or when Option Compare settings might adversely affect the algorithm.

StringBuilder (Using)

As a general rule, choose StringBuilder over String when performing multiple operations on strings of more than a few dozen characters or when doing more than a few dozen operations.

Structured Exception Handling

Use it. Do not use the On Error for new development.

System.Random

Use Rnd for migrated Visual Basic 6 code, for convenience or consistency in new development, or when the Visual Basic 6 algorithm is required. Use System.Random if thousands of random numbers are being generated, or if System.Random behavior is desired (for example, time-based seed by default).

TryÂ…Catch

Use it. Do not use the On Error statement in new code.

UBound

Use UBound for all development.

Value Types

Use them whenever possible but beware of boxing and unboxing costs when treating value types as reference types.

Visual Basic Runtime

Use it. It is part of the Framework so it is never something separate that your application has to carry or deploy.

WithEvents and Handles (declarative event wiring)

Use it. Compiler generates the same IL as AddHandler.

About the Authors

Derek Hatchard is a software development consultant, trainer, and writer based in Moncton, New Brunswick, Canada. He has been working with the .NET Framework and Visual Studio .NET since the first public betas of each, and does frequent consulting with 3 Leaf Solutions. You can reach him at dotnet@hatchardsoftware.com.

Scott Swigart is a Sr. Principal at 3 Leaf Solutions. Through their mentoring, training, and consulting services, 3 Leaf specializes in helping companies build solutions using the latest technologies and best practices. You can reach him at scott@3leaf.com.

This post is licensed under CC BY 4.0 by the author.