How to Optimize Memory Management in VB 3.0 for Windows

ID: Q112860


The information in this article applies to:


SUMMARY

This article outlines how various memory areas in Visual Basic are managed. It covers the following areas:

GENERAL MEMORY MANAGEMENT

Windows Limits

Windows version 3.1 imposes a limit of 600 windows in existence at one time. All windows in all applications running in Windows count toward this limit. In Visual Basic, each form and non-graphical control (all controls except shape, line, image, and label) counts as one window.

Application Limits

Form Limits

Data Limits

Specific data limits for Visual Basic version 3.0 are well documented in Appendix D of the Microsoft Visual Basic "Programmer's Guide." Please look there for information on data limits.

Global Name Table

Each application uses a single Global name table (up to 64K in size) that contains all Global names. Global names include the following:
NOTE: When your project is made into an .EXE file, most of the Global name table is not needed. Visual Basic takes special provisions to ensure that only the names that are needed are included with the .EXE (such as DLL function names).

Global Symbol Table

Each application has a single Global symbol table that is up to 64K in size. This table contains descriptive information about each of the items named in the Global name table. Specifically, this table contains:
NOTE: When your project is made into an .EXE file, the Global symbol table does not exist. It is only needed within the Visual Basic environment to create the .EXE file itself.

Global Data Segment

Each Visual Basic application receives a single data segment of up to 64K (minus overhead) to store the actual data referenced by Global variables and Global constants.

Space for Global string constant descriptors is also allocated in this data segment. However, the actual string data for the Global string constant is stored in a segment of up to 32K (minus overhead) allocated separately from dynamic memory.

A custom control (.VBX) is allocated space in the same 32K segment for any strings obtained with the Visual Basic API VBCreateHlstr. If the custom control does not deallocate this space, because it needs to reference these strings when it is unloaded or it doesn't clean up properly, the data in the segment may exceed 32K. If this occurs, Visual Basic allocates another segment to hold the excess data and links this new segment into the dynamic data segment chain.

Module Name Table

Each form and code module has a single Module name table that is up to 64K in size. The Module name table includes:
NOTE: As with the Global name table, when your project is made into an .EXE file, most of the Module name table is not needed.

Module Symbol Table

Each form and code module has a single Module symbol table that is up to 64K in size. The module symbol table contains much of the same information as the Global symbol table, except for Type definitions.

NOTE: As with the Module symbol table, when your project is made into an .EXE files most of the Module name table is not needed.

Module Data Segment

Each form and code module has its own 64K module data segment. The contents of the module data segment include:

Module Code Segment

Each Sub or Function procedure in a form or code module can contain up to 64K of p-code (the internal representation of your code). The module-level declaration section of each form or code module can also contain up to 64K of p-code. If the procedure-level or module-level p-code exceeds this limit, Visual Basic generates an "Out of Memory" error message. There is no specific internal limit to the total size of an individual form or code module.

The amount of p-code in a procedure or in the Declarations section of a module is roughly equivalent to the number of ASCII text characters in the code. The Visual Basic documentation recommends that you save your forms and modules as ASCII text and that you keep the size of individual procedure-level or module-level code in your form and code modules to under 64K. However, practical experience by Visual Basic programmers has shown that it is best to keep the size of these under 45K to 50K of ASCII text.

This estimate is true only when you are in the development environment. Visual Basic does not include identifiers (procedure, variable, and object names) or comments in the .EXE files you create, so the resulting .EXE is much smaller. If you don't exceed the 64K limit on p-code in the development environment, you won't exceed it in the finished .EXE.

System Resources

The data space (heap) attached to the Windows libraries (USER.EXE and GDI.EXE) is the space allocated for system resources. The data space used by the GDI library contains graphics information regarding brushes, fonts, icons, and so on. The data space used by the USER library contains style information pertaining to windows (forms) and controls. The data space for each of these libraries is limited to 64K. The "About Program Manager" menu command displays, as a percentage, the lower of these two areas of memory. If you run out of either of these two heaps you will receive an "Out of Memory" error message.

Visual Basic uses Windows resources shared with other Windows applications, so it is important to make efficient use of these resources to avoid "Out of Memory" messages. For larger applications, you should reduce the number of forms and controls loaded at any one time. Forms should be loaded only when needed. Here are some ways to reduce the number of controls:

Stack Space

Each Visual Basic application uses a single stack limited to 20K in size. The 20K size cannot be changed, so an "Out of Stack Space" error can easily occur if your program performs uncontrolled recursion, such as a cascading event.

Visual Basic itself uses the stack, even in a .EXE, so the practical limit is lower than 20K. A simple way to get an idea of this to make an .EXE file of a form with a single command button on it with the following code. Each Gosub takes 4 bytes of stack space, so you can then calculate how much free stack space you had when your procedure started.

   Sub Command1_Click ()
      Dim frameCount as Integer
      frameCount = 0
   Overflow_Stack:
      Me.Cls
      frameCount = frameCount + 1
      Print frameCount * 4
      GoSub Overflow_Stack
   End Sub 



Procedure arguments and local variables in Sub and Function procedures take up stack space at run time. However, Global variables, module-level variables, and procedure-level variables declared with the STATIC keyword, and arguments in Sub or Function procedures that are defined with the STATIC keyword, don't take up stack space because they are allocated in the Module Data Segments of form and code modules.

When Code and Resources Are Loaded or Unloaded

When an application is executed, memory is allocated for the Global Data Segment and the appropriate data is loaded into it. This stays persistent until the application terminates.

When a form is loaded, system resources are allocated for the Form and all controls on that form. In addition, memory is allocated for the Module data segment and the appropriate data is loaded into it.

Each Visual Basic form or module consists of one or more code segments in the .EXE file. The code segments are marked LOADONCALL, MOVEABLE, and DISCARDABLE, which you can verify by using a copy of EXEHDR.EXE to dump the file header information. Thus, on first reference to the form or module, Windows loads the code associated with it. Windows is then free to discard it and demand-load it as it sees fit (for example, if you run low on memory). Unloading a form will not force the code segment out of memory, it's still present until Windows decides to discard the code segment.

Unloading a form does unload the instance of that form and all resources used by it and the controls on that form. However, when a form is unloaded, the Module Data Segment is not unloaded; it remains in memory until the application terminates. Thus, if you load a Form, change the values of some module level variables, unload the Form, and then load it again, the value you last assigned to the module level variable in the Form will still be present. To deallocate memory used by the Module data segment in the form, use the following line of code after you unload the form:

   Unload Form2
   Set Form2 = Nothing 

This will clear all data in the Module code segment for the form.

"OUT OF MEMORY" AND RELATED ERROR MESSAGES

Common Ways to Avoid "Out of Memory"

Here are some tips to help you avoid the "Out of Memory" error message:
There is a special version of error #7, "Out of Memory -- Global Name Space", which can occur when the name space is filled. To deal with this error, minimize the size of the names used in event, private, and Global Sub and Function procedures. See the "Global Name Table" discussion above for details.

Common Ways to Diagnose "Out of Memory"

When you receive an "Out of Memory" error, there may be few limits to the lengths you will have to go to diagnose and locate the actual source of the problem. Make a backup of your project and place it in a temporary directory. Take any drastic measures on that backup copy. The following are some tips to help you narrow the focus in finding the cause of the "Out of Memory" error:

Verifying a Memory Leak

Memory leaks can occur when memory is allocated to perform a particular process but is not deallocated when the process concludes.

You can use a tool such as HeapWalker from the Windows Software Development kit or Visual C/C++. HeapWalker allows you to analyze your application's memory requirements and memory profile by seeing how many code segments from your application are in memory at any given time. WPS.EXE which ships with the Visual Basic CDK in the professional edition, can be used to determine which .DLL and .EXE files are running.

The Windows API GetFreeSpace() function scans the Global heap and returns the number of bytes of memory currently available. However, the results of function in a large application can be misleading. The best way to use this function is to create a demonstration program that tracks memory before and after a given process. It is important to minimize the scope of that process as much as possible or else the results will be inconclusive.

OPTIMIZATION RECOMMENDATIONS

Optimization techniques are generally a trade-off between size and speed. Some optimizations have multiple effects, for example, reducing size can improve speed because less memory may be needed which causes less disk swapping. Some Visual Basic speed optimizations affect system resource availability.

System Resource Overview

Under Windows 3.x system resources fall into one of two categories, USER and GDI. USER resources consist of Window and Menu handles. GDI resources consist of Device Context handles, Brushes, Pens, Regions, Fonts, and Bitmaps. USER and GDI resources are stored in the corresponding Windows internal DLL's data segments which have a maximum size of 64K. Resource availability refers to how much of the 64K data segment is still available for new resources. A resource handle is an offset into the data segment to the resource's internal control structure.

The Program Manager's available resource indicator in the About Box displays the least available of the two type of resources. The biggest violator of USER resources is the creation of windows (for example, Dialog Boxes, Forms, Controls). Every window has a variable length internal control structure. The base size for a Window's control structure is 64 bytes. Additional bytes are specified by the Window's Class in order to support the specific functionality of the window. Standard controls in Windows and Visual Basic consume from 66 to 160 bytes. Visual Basic includes 4 graphical controls (Label, Shape, Line, and Image) which do not create Windows and hence consume ZERO USER resources.

Visual Basic Controls: USER Resource Byte Use


   Form .......... 206 | FileListBox .... 160 | PictureClip ..... 72
   MDIForm ....... 420 | Shape ............ 0 | SpinButton ...... 72
   PictureBox ..... 90 | Line ............. 0 | SSCheck ......... 72
   Label ........... 0 | Image ............ 0 | SSFrame ......... 80
   TextBox ........ 76 | Data ............ 72 | SSOption ........ 72
   Frame .......... 74 | Grid ............ 72 | SSCommand ....... 76
   CommandButton .. 66 | OLE ............ 164 | SSPanel ......... 80
   CheckBox ....... 76 | AniPushButton .. 170 | SSRibbon ........ 80
   OptionButton ... 76 | CommonDialog ... 166 |
   ComboBox ...... 136 | CrystalReport .. 166 |
   ListBox ....... 160 | Gauge ........... 80 |
   HScrollBar ..... 80 | Graph ........... 80 |
   VScrollBar ..... 80 | MhState ......... 72 |
   Timer .......... 72 | MSComm .......... 72 |
   DriveListBox .. 160 | MaskEdBox ....... 76 |
   DirListBox .... 160 | Outline ......... 82 

Menu USER Resources

Menus also consume USER resources although their impact is very minimal. An application could have a total of several hundred menu options and still only increase the USER resource use by 24 bytes. There is ZERO resource use for the top level menu. USER resources are consumed and freed dynamically as menus are opened. The first level drop-down menu consumes 24 bytes. The second, third, forth, and so on consume 132 bytes each of USER resources. For example, a displayed third level menu would consume 288 bytes, 24 bytes for the first level, 132 bytes for the second level, and 132 bytes for the third level. Popup menus that can appear anywhere on the screen, usually at the mouse cursor location, also consume 132 bytes of USER resources.

System Resource Optimizations

The best way to reduce USER resources is to reduce the number of created windows at any one time. Any loaded form consumes USER resources regardless of whether it is visible:
  1. Break large forms (forms with many controls) into several smaller forms. Each of the smaller forms can be loaded and unloaded as necessary. Since only one of these forms at any given time is loaded, system resources will be used less. However, the added overhead of loading and unloading forms will most likely result in slower application performance, so there is a tradeoff.


  2. Implement control simulation. For example, a form could implement a toolbar by using 20 command buttons, consuming 1320 bytes of system resources. The toolbar can be implemented more efficiently with two image controls and one PictureClip control. The first image control contains one bitmap that looks like the entire toolbar. The PictureClip control contains one bitmap that contains the button down appearance of all buttons on the toolbar. When the user clicks on the toolbar (image control) the corresponding down picture is retrieved from the PictureClip control, placed in the second image control, and placed over the toolbar in the correct position. The second image control is hidden on the mouse up giving the appearance of button depression. This solution consumes 72 bytes of USER resources for the one PictureClip control. The image controls are graphical controls which consume zero USER resources.

    A second area of control simulation is text boxes. Applications that are data entry intensive contain many text-box controls. For example, if a form contained 64 text-box controls, they would consume 4864 bytes of USER resources. A simulation solution would use 1 text-box control and 64 label controls. When the user clicks on a label to edit it, the text box is resized and moved to the clicked label. This requires more coding to handle the one text box, tabbing, and access keys. This solution consumes 76 bytes of USER resources for the one text-box control. The label controls are graphical controls which consume zero USER resources.


  3. Consider redesigning the user interface. For example, many option buttons can be replaced by one combo box and many check boxes can be replaced with one multiselect list box. Some controls may be an overkill for the desired functionality. For example, using a picture box, when all that is needed is an image control.


Size Optimizations

The following are recommendations intended to help produce the smallest, fastest code possible. Please note that although saving two bytes here or four bytes there may not seem like a lot, just implementing recommendation 3 for null-length strings ("") globally throughout a product can provide significant size reduction! Furthermore, Visual Basic uses a scheme of aligning segments on a 256-byte boundary in the EXE, which means that if you use just one byte beyond the current 256-byte block, another 256-byte block will be allocated! So, some of these optimizations not only result in a time savings, they can also result in much smaller code!
  1. Put strings into external DLL's:



  2. Reduce unnecessary code replication.

    Having the same code in multiple places will generally increase the size of the app. It also increases maintenance costs. In some cases it may be necessary to have the same code in multiple places, and such instances should be commented to indicate the need.


  3. Use Constants rather than string literals:



  4. For strings that are used throughout the program and are Read Only, use Global string variables. Be cautious about this approach since the Global Symbol Table has a limit of 64k for storing variable names. It[ASCII 146]s best to use this method for strings that are repeated a lot.

    Define the strings as Global, set them up in an Initialization module, and then use them in other modules instead of constantly recreating things like Chr$(9):
    
         Global TABCHR as String * 1
         Global CR as String * 1
         Global CRLF as String * 2
    
         Sub Init()
           TABCHR = Chr$(9)
         CR = Chr$(13)
         CRLF = CR + Chr$(10)
         End Sub
     
    By constantly using Chr$(9), the code is larger, and it is slower since it must re-create the character every time that code is executed.


  5. Put IF/THEN constructs on one line if possible.

    The ENDIF generally takes up 6 bytes, thus changing:
    
          If X=2 Then
             debug.print "HI"
          EndIf
     
    to:
    
          If X=2 Then debug.print "HI"
     
    This winds up saving 6 bytes to do the exact same function.


  6. Check to see whether the Visual Basic function you're using is really needed.

    Sometimes this means checking the function to see if the returned value really needs the additional work you're doing, such as:
    
          Trim$(Format$(123))
     
    In which case, the Trim$() is totally unnecessary. Other times, this means looking at what is being created, and seeing if it could be done at design time, such as " 0" instead of using Str$(0).


  7. Put functions that are only used one time, or are used seldom in their own modules. Visual Basic loads modules on demand and Windows will discard them if they don[ASCII 146]t get used after a while:



  8. Use Bit fields rather than strings.

    Not only does an integer take up less space in memory than a 16 character string, but it is much faster to do comparisons! i.e., it would be much faster, and more efficient, to check for the existence of multiple flags via:
    
          If (Flags And TestVal) Then ...
     
    than an associated routine which has to scan multiple characters in a string to see if any of them are a "0" or "1".


  9. Remove unnecessary value checks.

    For example:
    
          If  bFlagToBeTested = True Then ...
     
    can be written as:
    
          If  bFlagToBeTested Then ...
     
    Removing the "= True" saves 6 bytes from the module or form and is faster to execute. (Note: It also means that any non-zero value can be evaluated as "True".)


  10. Do calculations manually at design time rather than asking the compiler to do them at run time.



  11. Move graphics from the application to a DLL.


This reduces the size of the executable file for updates. The .EXE can be updated and distributed without the redistribution of the graphics in the DLL, assuming that the graphics have not changed.

Speed Optimizations

WARNING: The following methods are designed to help machines with low memory (RAM). However, these methods may make debugging harder, and make it harder to maintain and reuse code. Therefore, this tradeoff should be considered when using the following techniques.
  1. Never reference an object property more than once.
    
       Poor:
    
          if Form1.Caption = "ABCD" then
          elseif Form1.Caption = "DEFG" then
          elseif Form1.Caption = "XYZ" then
          endif
    
       Better:
    
          Temp$ = Form1.Caption
          if Temp$ = "ABCD" then
          elseif Temp$ = "DEFG" then
          elseif Temp$ = "XYZ" then
          endif
    
       Even Better:
    
          Select Case Form1.Caption
             Case "ABCD"
             Case "DEFG"
             Case "XYZ
          End Select
    
       Best:
    
          CONST CASE1 = "ABCD"
          CONST CASE2 = "DEFG"
          CONST CASE3 = "XYZ"
          ...
          Select Case Form1.Caption
             Case CASE1
                Case CASE2
             Case CASE3
          End Select
      
    The last version is the best, because it uses constants for the strings (easier to maintain, less memory used, faster execution) and because it only references the variable (Form1.Caption) one time, rather than multiple times as the IF/ELSEIF constructs do. Using Select/Case statements will result in faster execution, however, it will be at the cost of slightly larger code.


  2. Avoid calling routines outside of the current form or module to reduce swapping problems on low-memory machines

    Calling routines in a central location is okay but this can lead to inefficiency. A routine in one module which calls a routine in a second module, which calls a third routine in yet another module, etc. means that all of those modules have to be loaded. Doing this can cause excessive hits on the Windows SwapFile. To alleviate this, make sure that as many of the subordinate routines that your routine needs are located in the same module.


  3. It is more efficient in Visual Basic to have local references to the current form's controls than it is to reference the proper form and then the control. Put subroutines or code that use controls in the form instead of a module whenever possible. For example:
    
       Module1
    
          Sub DoWork
          Form1.Text1.Text = "ABC"
          End Sub
    
       -versus-
    
       Form1
    
          Sub DoWork
          Text1 = ABC
          End Sub
       


  4. Make smaller modules:

    Normal Windows programmers look for segment sizes in the range of 4-8k. Since there is no easy way to determine how much space a particular module in Visual Basic is taking up, sometimes we can only guess. The best approach is to put subroutines that work together into the same module. Then try to make that module as small as possible.


  5. Use specific object types:



  6. Use Windows API functions to draw artwork or backgrounds on the form rather than using Picture boxes or Image controls. Using the Windows API will help make some of the forms draw quicker.



REFERENCES

Additional query words: 3.00 MemLeak


Keywords          : kbcode PrgOptMemMgt 
Version           : 3.00
Platform          : WINDOWS 
Issue type        : 

Last Reviewed: May 20, 1999