Visual Basic speed tweaks

Why optimise?

That's actually quite a good question. In the "old" days, when processor speeds were measured in hundreds of megahertz and memory came in multiples of 32 or 64, it was important to do your best to eke out the maximum performance afforded to you by your chosen language.
Indeed, Visual Basic suffered a bad reputation in those days. It is simplicity itself to design a window (or a "form" in VB speak) and attach events to it. However being that easy to use comes with the drawbacks associated with any typical Microsoft Automation product - namely a heavy dependence on DLLs and OCXs, lots of baggage, and events 'hidden' from the programmer and handled automatically.
VB will always be slower than C, which itself may be slower than a competently put together assembler program - though few people these days would write an entire application in assembler, the end no longer justifies the means when modern compilers can output quality code and the development time of an application written in a high level language can be many times shorter than the same thing written in assembler.
 
These days, however, it is perhaps more a question of morals. I mean, when you are talking of a gigabyte or two of memory and a three gigahertz processor, the actual difference between "any old code" and "nice optimised code" is probably less time than the interval between the two clicks of a double-click. Heck, you might even be able to write a script interpreter and express your program as a script to be executed by your script interpreter... and it would still work adequately quickly!
On the other hand, some of the speed results are surprising, so perhaps there's more to it than good karma after all?
Remember - an awful lot of software is repetitive. For example, you don't twiddle one byte of an array holding a graphic - you may "sharpen" or "colourise", which will require the same procedure to be applied to hundreds of thousands of bytes, if not millions. As we can see from the following results, some small alterations are trivially unimportant until we see them in the context of millions of iterations.
 
So ask yourself this. "Any old code" is perfectly acceptable for "does this work?" experiments. For talking to your digital satellite receiver, decoding astronomical data sets based upon a 'hunch'. Whatever. When the end result is the most important thing, you can write any code to help you get there, and tidy it up once you have working code. Fair enough. That's the lazy-hacker approach where you sit in front of the computer and bash out code to an idea, without those tedious formalities such as "writing a spec".
 
However when it comes to projects that you care about, things you want to be robust and to last, is "any old code" good enough? Is it really?
Perhaps on today's computers you can shave off a microsecond. It is worth doing not because you are a speed freak, but because you'll be coding properly.
 
You may find some of these suggestions to be surprising.

 

A quick note on timings

The timings depends heavily on your system. Processor speed, memory speed, and system load. It is possible that the same machine may result in different timings depending on what else is running.
 
These tests should only be taken as relative times. If something takes a second and two seconds on my machine, that could easily scale to a quarter and a half on a faster machine. In any case, we can see one method takes twice as long as the other.
 
My computer is an Acer Travelmate with a 466MHz Celeron processor and 64Mb RAM. It runs Windows 98SE in 800x600 24bit. All tests were written into a program which was compiled to p-code (my VB does not offer to make true .EXEs). Each test executes the example code fragment twenty million times. How long it took is output to a file. The reason for the repeated execution is to really show up the difference in timing.
 
The test program is available as both an executable and source, so you can test it on your system.
I would be interested in seeing the results of running these tests when compiled as a genuine .EXE.

 

IF there is logic...

Assuming we have a statement along the lines of:
   If ((ProcOne = True) And (ProcTwo = True)) Then...
Most languages will use short-circuit logic, that if the ProcOne function does not return True then the ProcTwo function won't even be executed.
But not VB. Oh no, VB will execute both procedures regardless of whether or not there is any sanity to it.
 
The workaround is ugly, but might be justified if the called procedures are involved:
   If ((PA = 1) And (PB = 1))   26.07 s
   If (PA = 1) Then If (PB = 1) 13.58 s
Where PA and PB simply return zero.
 
The example is a bit of a lie. VB5 cannot chain IFs on one line, so the code is actually:
   If (PA = 1) Then
     If (PB = 1) Then
     End If
   End If
Which takes more space, but certainly aids show the flow. You could nest these several levels deep - but don't forget to put the clause most likely to equate to false on the outside.
 
Remember, however, that this is more directed towards calling functions in If statements - simply comparing variables is not going to show much difference in speed.
 
Furthermore, it may be worth using local variables if you want to access calculations or substrings more than once, especially within an If loop.

 

My own software

I am guilty of many of the mistakes described below. In time I will have to work through my code and optimise as described.
I think the main problem arises in expecting VB to behave like C, which, frankly, is a completely daft expectation, albeit a shame as despite C's occasional obfuscation it is a logical language in how it works and what it does.
I do not write this as some sort of programming guru. I write this as somebody who noticed some speed differences (almost as a side effect of a trivial modification) and decided to investigate further...

 

Form properties

If you don't actually draw anything on your forms themselves, you don't need to set the AutoRedraw property. Doing so keeps a bitmap copy of the form at the current screen colour depth, so Windows can automatically replace any parts of the form that become obscured. This can lead to an unnecessary waste of memory.
If you do need to draw to the form, depending on how you do it, AutoRedraw may be faster than linking into the Form_Paint event.
 
The large printed manual (or Books Online) goes into detail about Z-ordering, layering, and how the ClipControls property affects this. Put simply, if you don't use layering or buttons-on-graphics, set the ClipControls property to False. Then the computer won't waste time calculating clipping rectangles for no good reason.

 

Bitmap wastage

When you instruct VB to create a bitmap, either explicitly in code or by putting something into a picturebox control in a form... the result will be a bitmap of the requested size at the current system colour depth.
Initially this may be acceptable to you, however soon you'll find it can be a memory and speed sink, sucking the vitality out of your software.
 
Sadly the answer is not simple. You will need to brush up on the GDI API (if those six letters don't mean anything to you, give up while you are ahead). Then you will need to create a "device context" for the screen, then a "bitmap object". Then you will need to create "text handles" or "brush handles" and associate them with the bitmap to create another context. Then you will need to do the actual drawing into the in-memory bitmap. Then free the text or brush handles. Then you'll have to get VB to repaint from your in-memory bitmap. And, when all is done and you are exiting, you'll need to release all of those contexts.
 
Is it any wonder people say "to hell with it!" and just use a VB-provided picture with .CurrentX and .Line and .Font.Bold? It is as if GDI is the most complicated way possible to get anything done!

 

Copying pictures

The easiest way to copy pictures is to assign the .Picture property of one to the other.
 
If you wish to resize along the way, you'll need a GDI call called "StretchBlt". This takes a source and destination hDC, a source and destination offset, and a source and destination size (width and height).
Remember that this GDI call works in pixels, while the picture property of a picturebox works in HiMetric.
 
Sadly, don't expect any niceties such as anti-aliasing. You can "prefer white" (seems fairly useless), "prefer black" (good for text), or "best keep colours" (best for pictures).
If you would like to work with a text/colour graphic, then you're all out of luck. Prefer black will keep the text readable but the graphic will look horrible. Prefer colour will result in a good scaled graphic, but the text will have been converted by simply chopping out bits.
 
If you want anti-aliasing, you will want to use GDI+ which seems to like abstract "picture objects" instead of the GDI "device context and object handles" method.

I saw a program that performed the anti-alias by averaging the pixels itself, instead of using the API. It was not particularly quick, but it worked.

 

 

Now we've pointed out what a pain in the butt GDI can be, it is time to concentrate on purely VB code optimisations.
What follows may surprise you.

 

Assigning a blank string

You'll always have a need to assign a blank string. Many might be tempted to simply assign "", however doing this takes a staggering eight times longer than assigning the null string value:
   MyStr$ = ""                  16.14 s
   MyStr$ = vbNullString         3.94 s
Please be aware that while the above will work in VB programs, there is an important difference in that the vbNullString value is a null pointer while the "" value is a pointer is an empty string. This may be of critical importance if/when calling DLLs (i.e. Windows API functions). For normal internal use, however, vbNullString is where it's at.

 

How to tell if a string is empty...

You have probably guessed that comparing with "" will be slow, and using the vbNullString value will be faster.
This is correct, but it may be improved upon. We can check for the string length and make some speed savings.
We can improve still further. Recall that internally VB holds strings as a double-byte Unicode value (even if it is near impossible to get VB on W98SE to speak katakana!), so there is one final trick. The LenB command returns the actual number of bytes used and not the string length. No matter, we simply wish to compare against zero - though surprisingly it turns out that this method is slightly slower then asking for the simple string length.
Compare (assuming MyStr$ is "heyrick"):
   If (MyStr$ = "")              7.20 s
   If (MyStr$ = vbNullString)    6.59 s
   If (Len(MyStr$) = 0)          4.86 s
   If (LenB(MyStr$) = 0)         5.21 s

 

Character codes

Remember that I said strings are held as Unicode? When you call Asc or Chr$ you are actually asking VB to make an explicit conversion between the 16 bit Unicode string and the 8 bit code page (often misnamed "ANSI") character set.
A way around this, and a way to speed up your programs, is no longer to assume characters may be represented by a byte. Code to handle the 16 bit Unicode and you can then call AscW and ChrW$, the results speak for themselves (MyStr$ is "x")...
   MyVal = Asc(MyStr$)           9.99 s
   MyVal = AscW(MyStr$)          4.34 s
   MyStr$ = Chr$(120)           21.45 s
   MyStr$ = ChrW$(120)          17.99 s

 

That $ is important!

We may have thought to thank VB for finally removing the clutter of the $ symbols from the BASIC language, so you can write code such as:
   process = LCase(Trim(inline))
which, lets face it, is nicer than:
   process$ = LCase$(Trim$(inline))
 
Sadly, we fall into that deep dark anomaly known as the variant variable type. Perhaps having grown up on BBC BASIC and progressing to C and ARM assembler, I find the concept of a variable that can be any type as being a highly weird concept.
Frankly, I shun it like the GOTO. Unfortunately I have to modify most of my software to add in all those silly dollar signs.
Why? Because my code is riddled with anomalous variants. Gah!
 
Here's how it works: When you don't implicitly define a return type for the string functions, stupid VB does not return a string type, it returns a variant. Therefore:
   process = LCase(Trim(inline))
can be broken down as:

Tell me all that messing around with the variants is not silly?
Where "**" is the string literal " HeyRick ", the results speak for themselves:
   pr = LCase(Trim(**))       141.66 s
   pr$ = LCase$(Trim$(**))    105.80 s

 

Casting and conversions

Compared to C, VB's casting is pretty poor. This isn't helped by values being signed, and an apparent inability to handle unsigned values without tricks.
 
So you want to convert a string to a long? Piece of cake, right? Just call:
   MyLong = Val("1234")
This is good news and bad news. The good news is that the Val command will return zero if the thing to be evaluated is not a number. The bad news is, Val is a "Jack of all trades" and hence it isn't terribly quick. If you are doing a string integer value conversion, do you want VB to be wasting time with possible floating point stuff?
A quicker way is to explicitly cast, like:
   MyLong = CLng("1234")
There is good news and bad news here, also. The good news is the speed increase is worthwhile, twice as fast! The bad news is that a non-number is an error.
You might wish to consider Val if there is any possibility of a non-numerical input (remember that a blank string is not a number). On the other hand, if you know your value to convert is always going to be a number... check these results:
   MyLong = Val("1234")          75.39 s
   MyLong = CLng("1234")         30.25 s

 

Vanquishing variants

I assume you wrote something like:
   Dim MyStr$ As String, MyVal As Long
If you did not, and you simply wrote:
   Dim MyStr, MyVal
Then VB will be happy to allocate storage for two variants (unless you use something such as "DefInt A-Z").
 
On the other hand, if you didn't enable explicit variable declaration and then pre-define your variables, then your coding style needs some revision...
 
I tend to abuse Integer as I am used to RISC OS which offers 32 bit integers. I had been led to believe that Long was a better choice on a PC. Under VB, this is not necessarily true.
More examples:
   MyInt = 12345                 3.39 s
   MyLong = 12345                3.42 s
   MyLong = 12345&               3.05 s
   MyVar = 12345                 4.31 s
Remember these timings are for twenty million iterations. As you can see, there is not much in it. Long with long is best, as it is the native word size of the x86 processor. However int with int is not much worse, followed by long from an int (remember to type appropriately).
 
Where you will notice the speed hit is if you persistently use the wrong type for its purpose. Look at the documentation for FreeFile or Seek() - both expect the file pointer variable to be an integer. If you use a long, VB will have to cast for you, an operation which is not necessary as you could use an integer in the first place.
Conversely, file positions are expressed as long, because it is quite likely a file will be larger than 32K. To use an integer will mean not only will VB have to cast, but also that the program will crash and burn if the value exceeds the limited capacity of an integer.
 
So when you are faced with choosing between integer or long, don't simply assume long unless you are sure you won't be using it with any built-in functions that expect an integer... and if you assign or compare embedded values with this long, don't forget the '&' suffix so you'll be comparing like with like...
And yes, I do mean:
   For MyLoop = 1& To 511&

 

Let's give it a whirl...

Simply download speedtest.zip (20K) which contains the SpeedTest executable, plus complete VB5 project and source code.

 

Then what?

A file, called VB speed log.txt will be created in the same directory as the executable.
 
For my 466MHz Celeron / 64Mb / Win98SE system compiling to p-code using VB5 LE SP3, the results were as follows:
VB timings log file - created 2007/03/15 at 22:01
=================================================

Times are for repeating the process 20,000,000 times.

Assigning to an Int from an Int      3.385
Assigning to a Long from an Int      3.419
Assigning to a Long from a Long      3.052
Assigning to a Variant from an Int   4.305

If loop with two functions together  26.074
If loop with two functions separate  13.578

Blanking a string with ""            16.137
Blanking a string with vbNullString  3.939

String blank test with ""            7.197
String blank test with vbNullString  6.591
String blank test with Len() = 0     4.861
String blank test with LenB() = 0    5.211

Converting string to int using Asc   9.998
Converting string to int using AscW  4.335
Converting int to string using Chr   25.423
Converting int to string using Chr$  21.445
Converting int to string using ChrW$ 17.987

MyStr = LCase(Trim(" HeyRick ")      141.658
MyStr$ = LCase$(Trim$(" HeyRick ")   105.8

Convert string num to long with Val  75.392
Convert string num to long with CLng 30.245

Tests completed after 530.504 seconds.

 

The code

' SpeedTest v0.01
' by Rick Murray  15th March 2007
' http://www.heyrick.co.uk/software/vbopti.html
'

Option Explicit

' A multimedia function to return the number of milliseconds since the last reset.
Declare Function timeGetTime Lib "WinMM" () As Long

' Variables. :-)
Public FP As Integer    ' File Pointer
Public LN As String     ' Logfile Name
Public TL As Long       ' Timing Loop value
Public TS As String     ' Temp String
Public ST As Long       ' Start Time
Public ET As Long       ' End Time
Public RT As Single     ' Resultant Time
Public BT As Long       ' Begin Time

Public Const LC As Long = 20000000 ' Loop Count

' Here be dragons...
Public Sub Main()
  Dim MyInt As Integer, MyLong As Long, MyVar As Variant, MyStr As String
  
  On Error Resume Next
  
  ' Confirm
  If (MsgBox("This test program will take a while (perhaps several minutes)." & vbCrLf & _
             "It is recommended that you close all other applications and let this " & _
             "program run" & vbCrLf & "without disturbances, and that you run it TWICE " & _
             "and take an average of the results." & vbCrLf & vbCrLf & _
             "Continue?", vbQuestion + vbYesNo + vbDefaultButton1) = vbNo) Then End

  ' Load and open the status window
  Load frmProgress
  frmProgress.Show
  DoEvents
  
  ' Screen hourglass
  Screen.MousePointer = vbHourglass

  ' The log file name is...
  LN = App.Path & "\VB speed log.txt"
  
  ' Open log file
  FP = FreeFile
  Open LN For Binary Access Write As FP

  ' Write a header
  TS = "VB timings log file - created " & Format(Now, "Short Date") & _
       " at " & Format(Now, "Short Time") & vbCrLf
  Put #FP, , TS
  TS = String$((Len(TS) - 2), "=") & vbCrLf & vbCrLf
  Put #FP, , TS
  TS = "Times are for repeating the process " & Format(LC, "##,###,###") & _
       " times." & vbCrLf & vbCrLf
  Put #FP, , TS
  
  ' Record start time
  BT = timeGetTime
  
  
  ' ========
  ' PART ONE - variable timings
  ' ========
  ST = timeGetTime
  For TL = 1& To LC
    MyInt = 12345 ' <-- we assume this will assign 12345 as an Int, NOT via a Variant!
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Assigning to an Int from an Int      " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyLong = 12345
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Assigning to a Long from an Int      " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyLong = 12345&
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Assigning to a Long from a Long      " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyVar = 12345
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Assigning to a Variant from an Int   " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 1, 7


  ' ========
  ' PART TWO - IF timings
  ' ========
  ST = timeGetTime
  For TL = 1& To LC
    If ((PA = 1) And (PB = 1)) Then
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "If loop with two functions together  " & CStr(RT) & vbCrLf
  Put #FP, , TS
  
  ST = timeGetTime
  For TL = 1& To LC
    If (PA = 1) Then
      If (PB = 1) Then
      End If
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "If loop with two functions separate  " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 2, 7
  
  
  ' ==========
  ' PART THREE - Blank string timings
  ' ==========
  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = ""
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Blanking a string with " & Chr$(34) & Chr$(34) & "            " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = vbNullString
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Blanking a string with vbNullString  " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 3, 7


  ' =========
  ' PART FOUR - Testing is a string is blank timings
  ' =========
  MyStr$ = "heyrick"
  ST = timeGetTime
  For TL = 1& To LC
    If (MyStr$ = "") Then
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "String blank test with " & ChrW$(34) & ChrW$(34) & "            " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    If (MyStr$ = vbNullString) Then
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "String blank test with vbNullString  " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    If (Len(MyStr$) = 0) Then
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "String blank test with Len() = 0     " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    If (LenB(MyStr$) = 0) Then
    End If
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "String blank test with LenB() = 0    " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 4, 7


  ' =========
  ' PART FIVE - To and from ASCII timings
  ' =========
  MyStr$ = "x"
  ST = timeGetTime
  For TL = 1& To LC
    MyInt = Asc(MyStr$)
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Converting string to int using Asc   " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyInt = AscW(MyStr$)
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Converting string to int using AscW  " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = Chr(MyInt)
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Converting int to string using Chr   " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = Chr$(MyInt)
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Converting int to string using Chr$  " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = ChrW$(MyInt)
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Converting int to string using ChrW$ " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 5, 7


  ' ========
  ' PART SIX - With and without $ timings
  ' ========
  ST = timeGetTime
  For TL = 1& To LC
    MyStr = LCase(Trim(" HeyRick "))
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "MyStr = LCase(Trim(" & ChrW$(34) & " HeyRick " & ChrW$(34) & ")      " & _
       CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyStr$ = LCase$(Trim$(" HeyRick "))
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "MyStr$ = LCase$(Trim$(" & ChrW$(34) & " HeyRick " & ChrW$(34) & ")   " & _
       CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 6, 7


  ' ==========
  ' PART SEVEN - Number to string cast timings
  ' ==========
  ST = timeGetTime
  For TL = 1& To LC
    MyLong = Val("1234")
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Convert string num to long with Val  " & CStr(RT) & vbCrLf
  Put #FP, , TS

  ST = timeGetTime
  For TL = 1& To LC
    MyLong = CLng("1234")
  Next
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(ST)) / 1000#)
  TS = "Convert string num to long with CLng " & CStr(RT) & vbCrLf & vbCrLf
  Put #FP, , TS
  frmProgress.SetProgress 7, 7


  ' Done, close file and open it...
  ET = timeGetTime
  RT = ((CSng(ET) - CSng(BT)) / 1000#)
  TS = "Tests completed after " & CStr(RT) & " seconds." & vbCrLf
  Put #FP, , TS
  Close #FP
  Shell "Notepad " & ChrW$(34) & LN & ChrW$(34), vbNormalFocus
  
  ' Final tidyups
  Screen.MousePointer = vbDefault
  frmProgress.Hide
  Unload frmProgress
  End
End Sub

Private Function PA() As Integer
  PA = 0
End Function

Private Function PB() As Integer
  PB = 0
End Function

 


Return to software index


Copyright © 2007 Rick Murray