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.
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 andString.Equals
are not equivalent. For case-sensitive equality comparisons,CompareOrdinal
(and thereforeStrCmp
) tends to be marginally faster thanString.Equals
becauseString.Equals
considers the culture associated with the current thread when doing its evaluation. This requires slightly more work than the comparison done byString.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 becauseStrCmp
usesCompareOrdinal
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), andLong
(8 bytes). In Visual Basic 6,Integer
is 2 bytes andLong
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 ofDateTime.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 byMessageBox.Show
. Strictly speakingMessageBox.Show
returns aDialogResult
value andMsgBox
returns aMsgBoxResult
value. The values in these enumerations have the same meanings:OK = 1
,Yes = 6
,No = 7
, and so on. You can also useCType
to convert aMsgBoxResult
to aDialogResult
.
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 fromObject
assures the compiler that you are confident of the type embedded in theObject
variable. UnlikeCType
and the other Visual Basic .NET conversion keywords,DirectCast
does not perform type coercion on the embedded value, even if a conversion is defined. CallingDirectCast(expression, Double)
on anObject
that contains a boxedInteger
value will cause a runtime exception even though anInteger
toDouble
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 Try
…Catch
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
- Performance Optimization in Visual Basic .NET
- Performance Considerations for Run-Time Technologies in the .NET Framework
- Performance Tips and Tricks in .NET Applications
- Language Changes in Visual Basic
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 On
…Error
. 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.
Right()
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.