Practical Tips For Boosting The Performance Of Windows Forms Apps

Milena Salman

 

This article discusses:

  • Improving application startup time
  • Tuning up control creation and population
  • Improving painting performance
  • Rendering text and images
  • Efficient resource management
This article uses the following technologies:
Windows Forms, .NET Framework 2.0

Code download available at: WindowsFormsPerformance.exe (605 KB)
Browse the Code Online

Windows Forms allow you to build a rich and responsive user interface for your applications. In this article I’ll discuss a number of techniques you can use to ensure that Windows® Forms-based apps provide optimal performance as well. I’ll discuss common performance-critical scenarios such as startup, control population, and control painting. Plus, I’ll discuss how to design and code for performance in your application. Together these techniques should give you a good, basic foundation for getting the most out of your UIs.

 

Snappy Startup
Startup time is an important performance metric for most applications. Quick startup leaves the user with a favorable perception of your application’s performance and usability. There are two terms that describe how an application starts up: warm startup and cold startup. If you start any managed application immediately after restarting your computer, then close the app and start it again. You will usually notice a pretty significant difference in startup time. The first (cold) startup will have to perform disk I/O operations to bring all of the required pages into memory. The second (warm) startup is significantly faster because it reuses the pages that are already present in memory.
To get the warm startup effect, you don’t necessarily have to have previously run the same application. Just starting any managed application will bring into memory many pages from the Microsoft® .NET Framework binaries, and this will improve startup time for any managed applications that start later.
When designing for performance, it is important to distinguish between these two scenarios and to set separate goals for them. While warm startup can be improved by reducing CPU utilization, cold startup is affected mostly by disk I/O. Therefore, some optimizations improve some scenarios and not others.
I will start with some general performance advice applicable to a starting application written in any managed code. Then I’ll give you some tips specific to improving the performance of Windows Forms-based apps. For detailed guidelines on improving the startup performance of managed applications, see the CLR Inside Out column by Claudio Caldato in the February 2006 issue of MSDN®Magazine at CLR Inside Out: Improving Application Startup Time.
One way to improve cold startup is to reduce the number of DLLs that you load into your application. This directly affects the number of pages that have to be touched during application startup. Additionally, reducing the number of modules loaded reduces the CPU overhead associated with loading a module, also improving warm startup times.
You can diagnose how modules are being loaded into your application using the Load Module event filter within the WinDbg debugger in the Platform SDK. Alternatively you can use the following WinDbg command to see why a specific module was loaded:

sxe ld:foo.dll

Review the call stacks and see if some of the module loads could be avoided. You can also use the “ca ml” option of the MDbg managed debugger, available as part of the .NET Framework SDK. In some cases you can avoid loading additional modules by simply refactoring your code. If your application loads lots of DLLs that you own, consider merging them into fewer bigger DLLs where it makes sense to do so.

Just-in-time (JIT) compilation of methods can consume many CPU cycles and easily become a bottleneck during application startup. To avoid this overhead you can precompile assemblies using the Native Image Generator, NGen.exe (see Speed: NGen Revs Up Your Performance with Powerful New Features). NGen performs all of the work of the JIT compiler, but accomplishes this work up front, then persists those changes to disk, saving CPU cycles at run time. Using NGen to precompile your binaries has an additional performance benefit: native code pages can be shared between processes, while JIT-compiled code pages are private to a process and cannot be reused.
Despite the advantages, precompilation is not always a a panacea for startup time. While warm startup will likely benefit from NGen precompilation, cold startup may not improve or might even be a little slower because the pages containing compiled code now need to be loaded from disk. However, if JIT compilation is eliminated completely, you might see a benefit even for cold startup since the pages required for the JIT compiler itself would not need to be loaded. It is good to measure both the cold and warm startup times to assess the impact of NGen.
Another technique is to install strong-named assemblies in the Global Assembly Cache (GAC) to avoid strong-name signature verification, a costly process that touches every page of the assembly before it is loaded. If you decide to use NGen, it’s even more important to have strong-named assemblies in the GAC and avoid DLL rebasing.
Every binary (DLL or EXE) has a preferred base address—a location in virtual memory where it is to be loaded. This address is specified at build time. If you use the Visual Basic® or C# compiler, then all of your binaries will get the same base address (0x400000) if no explicit build directions to the contrary are given. Thus, at load time, your EXE will be placed at this address and all of your DLLs will be have to be placed elsewhere in virtual memory (rebased), since their preferred base address is occupied.
When a DLL is rebased, the loader updates all of the absolute addresses in the DLL to reflect the new load address. This means every page that contains an address that needs to be adjusted will be touched when the DLL gets rebased. Moreover, in order to become writable, a page needs to be copied and backed by a pagefile. At that point the page is private to the process and cannot be shared with other processes.
To avoid the rebasing performance hit, you can explicitly specify a preferred base address by using the /baseaddress compiler switch for each DLL in your application. Assign base addresses to the DLLs beginning from some predetermined starting point (for example, 0x10000000), and leave gaps between the addresses that are large enough to give some room for growth to the DLL that will lie in that range.
NGen images tend to be significantly bigger than Intermediate Language (IL) images and this increases the impact of rebasing. NGen sets the same base address for the image it creates as the address specified in the IL image. Therefore, if you decide to NGen your assemblies, then when you assign base addresses for your IL images you should leave enough space to fit NGen images. Usually you will need two or three times more space for an NGen image than for the equivalent IL image.
You can check that your base address assignment worked properly by verifying actual load addresses and comparing them with preferred load addresses. To accomplish this, you can use the task list sample application, TList.exe, and the Microsoft COFF Binary File Dumper, Dumpbin.exe. This command shows the actual load address for each DLL loaded in the context of the application with process ID 1240:

tlist 1240

Dumpbin allows you to check the preferred base address for your DLL, as shown here:

dumpbin /headers test.dll

Looking at the output of this command, you’ll see OPTIONAL HEADERS with an image base listed, something like this:

75F70000 image base (75F70000 to 75F78FFF).

This means the preferred base address of test.dll is 75F70000 and that the address range of 75F70000 to 75F78FFF should be available in order to load this DLL at its preferred base address.

 

Faster UI Loading
The perception of startup performance depends largely on the delay until the first UI is shown. You should minimize the logic required to display the UI. This includes any work performed in the Load event of the form because this is evaluated as the form is in the process of showing itself.
If you use expensive network or database calls, make them asynchronous on a different thread to avoid blocking the UI. In Windows Forms 2.0, the BackgroundWorker component can be used to push work onto a background thread. In Figure 1, BackgroundWorker is used to recursively find all files matching a given pattern (for example, “*.txt”) in a given folder and its subfolders. The background thread updates the UI thread on its progress, and the UI thread adds the files or subfolders to a TreeView representation in response to progress updates. The resulting TreeView is filtered so that subfolders that don’t contain matching files are not shown.
Figure 1 Using a Background Thread
Imports System.IO

Public Class Form1
    Private DirInfo As DirectoryInfo
    Private Pattern As String

    Private Sub Form1_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        DirInfo = My.Computer.FileSystem.GetDirectoryInfo("C:\\")
        Pattern = "*.txt"
        Me.Text = "Searching for " & Pattern & " files"
        Me.TreeView1.Sorted = True
        Me.BackgroundWorker1.WorkerReportsProgress = True
        Me.BackgroundWorker1.RunWorkerAsync()
    End Sub

    Private Sub BackgroundWorker1_DoWork( _
        ByVal sender As System.Object, _
        ByVal e As System.ComponentModel.DoWorkEventArgs) _
        Handles BackgroundWorker1.DoWork

        For Each f As FileInfo In DirInfo.GetFiles(Pattern)
            Dim treeNode As TreeNode = New TreeNode(f.ToString)
            Me.BackgroundWorker1.ReportProgress(0, treeNode)
        Next

        For Each SubDir As DirectoryInfo In DirInfo.GetDirectories()
            Dim treeNode As TreeNode = GetMatchingFiles(SubDir)
            If Not treeNode Is Nothing Then
                Me.BackgroundWorker1.ReportProgress(0, treeNode)
            End If
        Next
    End Sub

    Private Sub BackgroundWorker1_ProgressChanged(_
        ByVal sender As Object, _
        ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
        Handles BackgroundWorker1.ProgressChanged

        Me.TreeView1.Nodes.Add((CType(e.UserState, TreeNode)))
    End Sub

    Private Sub BackgroundWorker1_RunWorkerCompleted( _
        ByVal sender As Object, _
        ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
        Handles BackgroundWorker1.RunWorkerCompleted

        Me.Text = "Ready"
    End Sub

    Private Function GetMatchingFiles(ByVal dir As DirectoryInfo) _
        As TreeNode

        Dim treeNode As TreeNode = Nothing
        For Each f As FileInfo In dir.GetFiles(Pattern)
            If treeNode Is Nothing Then
                treeNode = New TreeNode(dir.Name)
            End If
            treeNode.Nodes.Add(New TreeNode(f.ToString))
        Next
        For Each SubDir As DirectoryInfo In dir.GetDirectories()
            Dim subNode As TreeNode = GetMatchingFiles(SubDir)
            If Not subNode Is Nothing Then
                If treeNode Is Nothing Then
                    treeNode = New TreeNode(dir.Name)
                End If
                treeNode.Nodes.Add(subNode)
            End If
        Next
        Return treeNode

    End Function
End Class
The advantage of using a background thread here is that the UI is fully accessible while the search proceeds. Your user can scroll though the list of files found so far and expand subfolder nodes that have been added to the TreeView.
Operations that must be performed, but are not needed for displaying the first UI, can be done while the system is idle or on demand after the first UI is shown. For example, if you have a TabControl, populate only the topmost page on startup and retrieve the information for other pages when required. The code in Figure 2 includes a form with a TabControl containing six TabPages. I put all of the controls required for each TabPage on a UserControl and added this to a control collection of the TabPage when the TabPage is going to be shown. For simplicity I used the same UserControl for each of the TabPages and only slightly changed the context of some child controls, but you can easily have a separate UserControl corresponding to each TabPage.
Figure 2 Initializing Controls on Idle
public partial class Form2 : Form
{
    int currentIndex = 1;
    public Form2(bool onDemand)
    {
        InitializeComponent();
        if (!onDemand) InitTab(-1);
        else
        {
            InitTab(0);
            Application.Idle += new EventHandler(Application_Idle);
        }
    }

    private void InitTab(int index)
    {
        if (index == -1)
        {
            for (int i = 0; i < tabControl1.TabCount; i++)
            {
                InitTab(i);
            }
            return;
        }

        // if the TabPage has already been populated
        if (tabControl1.TabPages[index].Controls.Count > 0) return;

        UserControl control = new myTabPage(index);   
        if (control != null)
        {
            control.Dock = DockStyle.Fill;
            tabControl1.TabPages[index].Controls.Add(control);
        }
    }

    private void tabControl1_SelectedIndexChanged(
        object sender, System.EventArgs e)
    {
        InitTab(tabControl1.SelectedIndex);
    }

    private void Application_Idle(object sender, EventArgs e)
    {
        if (currentIndex >= tabControl1.TabCount)
        {
            Application.Idle -= new EventHandler(Application_Idle);
        }
        else InitTab(currentIndex++);
    }
}
For the topmost page I added the UserControl in Form’s constructor. For other pages I did it in response to firing of the TabControl.SelectedIndexChanged event when the corresponding page is going to be shown. The user will experience some delay when a TabPage is selected for the first time. Then the next time it is selected, there’s no delay since the TabPage has already been initialized.
In this example, I’ve intentionally slowed down the creation of the TabPages by showing a ListView with a large number of items. If I initialize all the TabPages up front, the time required to show a form is nine seconds on my system (Pentium III, 800 MHz, 512 MB RAM). By filling the TabPages on demand, showing each TabPage the first time takes about 1.5 seconds. If only the topmost page is populated, the entire form is shown in 1.5 seconds.
I can push this a little further by listening for the Application.Idle event and populating one TabPage at a time when the Idle event occurs. When the form is shown or a new TabPage is shown, it will take a user some time to look at the form and move the mouse to select another page. During this organic delay in the application operation, the Idle event will be processed and additional TabPages will be populated.
If other techniques fail to improve startup performance sufficiently, you might consider using splash screens and progress bars to keep users informed, like this example at Creating a Splash Screen Form on a Separate Thread. If you write your app in Visual Basic, you can use the My.Application.SplashScreen property of the .NET Framework 2.0 (see My.Application.SplashScreen Property).

 

Control Population
Many Windows Forms Controls such as ListView, TreeView, and Combobox display collections of items. Adding too many items to these collections can cause a bottleneck if no optimizations are applied. Fortunately, several control population improvements were incorporated into Windows Forms in the .NET Framework 2.0. These include the use of the BeginUpdate and EndUpdate methods internally within AddRange methods, optimizing TreeView population with AddRange, and optimizing sorted population of all controls.
The most common reason for slow control population is the repainting of the control after each change. A number of Windows Forms controls implement BeginUpdate and EndUpdate methods, which suppress repainting while the underlying data or control properties are manipulated. Using these methods allows you to make significant changes to your control (like adding items to its collection) while avoiding constant repainting. Here’s an example of using these methods:

listView1.BeginUpdate(); 
for(int i = 0; i < 10000; i++) 
{
    ListViewItem listItem = new ListViewItem("Item"+i.ToString() );
    listView1.Items.Add(listItem); 
} 
listView1.EndUpdate();
Bulk operations are always more efficient than individual changes. The preferred way to add items to the collections of controls such as ComboBox, ListView, or TreeView is to use the AddRange method, which lets you add an array of pre-created items at one time. Windows Forms does all of the possible optimizations to make this operation efficient. Often this just means that BeginUpdate and EndUpdate will be called for you. In some cases, however, additional optimizations will be done. For the TreeView control, for example, these optimizations are significant.
ListView population works considerably faster if it is performed after the ListView’s handle is created. If you need to show a form having a ListView with many items, add items to ListView in the Form.Load or Form.Show event handler. At this point the ListView handle is created already. If you add ListView items in the Form’s constructor (that is, before the ListView handle is created), the items will be added quickly, but showing the form will take significant time due to slow ListView rendering.
Unlike ListView, TreeView is populated significantly faster if nodes are added before the handle is created. If you use TreeView.AddRange you will not notice the difference; your TreeView will be populated quickly regardless of when you do it. However if you use the Add method and add nodes in the form’s constructor before the TreeView’s handle is created, your TreeView will be populated and shown as quickly as if you had used the AddRange method.

 

Data Binding Optimization
When populating databound controls such as ComboBox or ListBox, it is more efficient to set the DataSource property last, after ValueMember and DisplayMember are set. Otherwise your control will be repopulated as a result of the ValueMember change:

comboBox1.ValueMember = "Name";
comboBox1.DisplayMember = "Name";
comboBox1.DataSource = test;        

For this reason, the code just shown is more efficient than this:

comboBox1.DataSource = test;
comboBox1.ValueMember = "Name";
comboBox1.DisplayMember = "Name"

BindingSource.SuspendBinding and BindingSource.ResumeBinding are two methods that allow the temporary suspension and resumption of data binding. SuspendBinding prevents changes from being pushed into the data source until ResumeBinding is called.

These methods are designed to be used with simple bound scenarios such as TextBox or ComboBox data binding. The code in Figure 3 demonstrates that the iteration that suspends and resumes binding works approximately five times faster than the iteration that does not.
Figure 3 Suspending and Resuming Data Binding
private void button1_Click(object sender, EventArgs e)
{
    DataTable tbl = this.bindingSource1.DataSource as DataTable;
    Stopwatch sw1 = new Stopwatch();
    sw1.Start();
    for (int i = 0; i < 1000; i++)
    {
        tbl.Rows[0][0] = "row " + i.ToString();
    }
    sw1.Stop();

    Stopwatch sw2 = new Stopwatch();
    sw2.Start();
    this.bindingSource1.SuspendBinding();
    for (int i = 0; i < 1000; i++)
    {
        tbl.Rows[0][0] = "suspend row " + i.ToString();
    }
    this.bindingSource1.ResumeBinding();
    sw2.Stop();
    MessageBox.Show(String.Format("Trial 1 {0}\r\nTrial 2 {1}", 
        sw1.ElapsedMilliseconds, sw2.ElapsedMilliseconds));
}

private void Form1_Load(object sender, EventArgs e) 
{
    DataTable tbl = new DataTable();
    tbl.Columns.Add("col1");
    tbl.Rows.Add("one");
    tbl.Rows.Add("two");
    this.bindingSource1.DataSource = tbl;
    this.textBox1.DataBindings.Add("Text", this.bindingSource1, "col1"); 
}
Controls that implement complex data binding, such as the DataGridView control, update their values based on change events such as ListChanged, so calling SuspendBinding will not prevent them from receiving changes to the data source. You can use these methods in complex binding scenarios if you suppress ListChanged events by setting the RaiseListChangedEvents property to false.

 

Lightweight Layout
Changes such as resizing or realigning a control can also cause problems. For example, when child controls are added to a form or ToolStrip, a Control.Layout event is fired. A change to the size or location of a child control will cause a Layout event on a parent control. Font size changes will also cause a Layout event. In response to the Layout event, the form scales and arranges controls. Having too many Layout events processed while you create or resize your control may have a significant performance impact.
Component code generated by the Windows Forms designer starts with SuspendLayout and ends with ResumeLayout. This is done to avoid performing layout on the form while it is being created and populated with controls. The SuspendLayout method allows multiple actions to be performed on a control without generating a Layout event for each change. Use this technique whenever possible to minimize the number of Layout events.
Remember that SuspendLayout only prevents Layout events from being performed for that particular control. If controls are added to a panel, for example, SuspendLayout and ResumeLayout must be called for the panel and not for the parent form.
Changing properties such as Bounds, Size, Location, Visible, and Text for AutoSize controls will fire a Layout event. It will be even more expensive if these properties are changed in Form.Load, since all handles are created by then and thus many messages will be processed. This is another place where you should add SuspendLayout and ResumeLayout to prevent extra Layout events from occurring. If possible, make all of these changes within InitializeComponent—this way only one Layout event is ever needed.
Several properties dictate the size or location of a control: Width, Height, Top, Bottom, Left, Right, Size, Location, and Bounds. Setting panel1.Width and then panel1.Height causes twice the work of setting them both together via panel1.Size. Running the code in Figure 4 demonstrates the difference.
Figure 4 Testing Layout Performance
private void button1_Click(object sender, EventArgs e) 
{
    Stopwatch sw = new Stopwatch();

    sw.Start();
    for (int i = 1; i < 1000; i++) {
        panel1.Width = i;
        panel1.Height = i;
    }
    sw.Stop();

    Stopwatch sw2 = new Stopwatch();
    sw2.Start();
    for (int i = 1; i < 1000; i++) {
        panel1.Size = new Size(i, i);
    }
    sw2.Stop();
    MessageBox.Show(String.Format("Trial 1 {0}\r\nTrial 2 {1}",   
        sw.ElapsedMilliseconds, sw2.ElapsedMilliseconds));
}
If the handles are created you will notice the difference, even if you use SuspendLayout and ResumeLayout calls. SuspendLayout only prevents Windows Forms OnLayout from being called. It will not prevent messages about size changes from being sent and processed. You should try to set the property that reflects the most information you have. If you’re just changing the size, set Size; if you’re changing the size and location, change Bounds.

 

Painting Performance
If your Windows Forms application is drawing-intensive, it could benefit from enhanced painting performance. First, minimize the work that is done on every Paint event. Make your Paint event handler as lightweight as possible by moving any expensive operations out of it. For example, if you need to render text based on your current ClientSize, you can create the appropriately sized Font object in a Resize event handler instead of on every Paint event. If you need to create a Brush for drawing, you could cache it instead of recreating it repeatedly.
When you want your control to be redrawn, you can call its Invalidate method. The entire control will be redrawn if you do not pass any arguments to this call. In many cases you can optimize drawing performance by carefully calculating the area that needs to be repainted and passing that area as an argument to Invalidate.
If this does not work well enough, you might want to use the ClipRectangle structure that is included in the PaintEventArgs parameter of the OnPaint event. For example, you can draw a part of an image that was invalidated by doing the following:

Protected Overrides Sub OnPaint(_
    ByVal e As System.Windows.Forms.PaintEventArgs)

    e.Graphics.DrawImage(Me.RenderBmp, e.ClipRectangle, _
        e.ClipRectangle, GraphicsUnit.Pixel)

End Sub

Using this technique, however, frequently requires careful calculations (such as if scrolling or scaling is involved).

Windows Forms and the underlying Win32® architecture expose two layers of painting for any control: background and foreground. The Control.OnPaintBackground function is responsible for drawing background effects (typically background image and back color) and Control.OnPaint function is responsible for drawing foreground effects (images and text). If your control is not using the background image or background color, but instead custom paints everything in OnPaint, using the Opaque control style may help performance by skipping over painting logic that is not being used. In particular, if ControlStyles.Opaque is set to true on the control, the OnPaintBackground function will be skipped, as it is assumed that the OnPaint function will perform all the painting work (including painting the background). Additionally, this style might be advantageous when your control is completely covered by other controls and its background need not be painted.

 

Text and Images
You can measure and draw text on a Windows Forms Control in the .NET Framework 2.0 using the TextRenderer class. TextRenderer has two methods, MeasureText and DrawText, each with several overloads. The efficiency of your rendering operations depends on the overloads you choose and the options that you set in the TextFormatFlags argument.
For measuring single-line strings, you’re better off not using the TextFormatFlag.WordBreak flag. When this flag is set, GDI executes a reliable but costly algorithm to determine the places the text needs to be broken into. Similarly, do not use the TextFormatFlags.PreserveGraphicsClipping and TextFormatFlags.PreserveGraphicsTranslateTransform options if no clipping or transforms have been applied to the Graphics object because these options cause some expensive calculations to occur.
It’s better to use the TextRenderer method overloads that do not get IDeviceContext as an argument. These methods are more efficient because they use a cached screen-compatible memory device context rather than retrieving the native handle for device context from the internal DeviceContext and creating an internal object to wrap it.
If your application displays image files and contains an alpha component, blending, you can improve performance significantly by prerendering the images into a special premultiplied bitmap format. If an image has an alpha component, its color components should be multiplied by alpha when the image is displayed. Prerendering an image with premultiplied alpha eliminates several (three for RGB images) multiplication operations for each pixel in the image.
To use this technique, first load the image from a file, then render it to a bitmap with PixelFormat.Format32bppPArgb. Figure 5 uses this technique to set the Background image on a control.
Figure 5 Rendering a Background Image
Public Overrides Property BackgroundImage() As System.Drawing.Image
    Get
        Return Me.RenderBmp
    End Get
    Set(ByVal value As System.Drawing.Image)
        Me.RenderBmp = New Bitmap(Me.Width, Me.Height, _
            Imaging.PixelFormat.Format32bppPArgb)
        Dim g As Graphics = Graphics.FromImage(Me.RenderBmp)
        g.InterpolationMode = Drawing.Drawing2D.InterpolationMode.High
        g.DrawImage(value, New Rectangle(0, 0, Me.RenderBmp.Width, _
            Me.RenderBmp.Height))
        g.Dispose()
    End Set

End Property
In Windows Forms 2.0, the BackgroundImage property has a companion property called BackgroundImageLayout. In prior versions of the .NET Framework, the background image was automatically tiled by Windows Forms, letting the image repeat throughout the client area. The new BackgroundImageLayout property allows you to set your background image layout to Tile, Center, Stretch, or Zoom. To preserve application compatibility with previous versions of the .NET Framework, Tile layout is still the default.
In addition to adding a rich set of background image features to Windows Forms, the nondefault values of BackgroundImageLayout improve performance of your background image painting. The major drawback to the Tile setting is that a TextureBrush is required to repeat the image pattern. The creation of the TextureBrush is expensive as it involves scanning the entire image. Other layouts simply make use of the Graphics.DrawImage method, resulting in much better performance.
For layouts other than Tile, setting the BackgroundImage and BackgroundImageLayout properties may automatically turn on the DoubleBuffered property. In particular, if the image has some sort of transparency (as detected through ImageFlagsHasAlpha), the control will start using double buffering to increase performance. For Tile layout, the DoubleBuffered property is not turned on automatically, but you can always turn it on manually.
Double buffering is a technique used to make drawing faster and appear smoother by reducing flicker. The basic idea is to take the drawing operations used to paint your control and apply them to an off-screen buffer. Once all of the drawing operations have finished, this buffer is drawn as a single image onto the control. This usually reduces flicker and makes the application seem faster. This behavior can be achieved in the .NET Framework 2.0 by setting a control style to OptimizedDoubleBuffer, which is equivalent to setting the DoubleBuffer and UserPaint styles in previous versions of the Framework.
To fully enable double buffering, you must also set AllPaintingInWmPaint to true. When it is set, the window message WM_ERASEBKGRND is ignored, and both OnPaintBackground and OnPaint methods are called directly from the WM_PAINT message. If you set DoubleBuffering and AllPaintingInWmPaint to true, then OnPaintBackground and OnPaint will be called with the same buffered graphics object and everything will paint off-screen together and update all at once.
To benefit from double buffering you have to set both these styles to true or set the DoubleBuffered property on your control, like this:

SetStyle(ControlStyles.OptimizedDoubleBuffer | 
    ControlStyles.AllPaintingInWmPaint, true);
// OR
DoubleBufferred = true;  // sets both flags 

It is important to mention here that setting the DoubleBuffered property is not always the optimal solution to painting problems. This property should be used with care, depending on your application and targeted scenarios. The main drawback of setting DoubleBuffered is that it results in a significant amount of memory being allocated for painting. If you have to use double buffering, consider also invalidating small parts of the control so that only the most important parts need to be repainted.

Another side effect of setting AllPaintingInWmPaint to true can be a visible white trace over the child controls of your control when a window belonging to another application is moved across your window. This can happen because child control windows get many WM_ERASEBKGND messages and only one WM_PAINT message. Since WM_ERASEBKGND messages are ignored, repainting happens too rarely for them.

 

Applying Painting Techniques
The screen in Figure 6 demonstrates animation of text over a user control for which a background image is set. You can set different painting options to see how they affect painting smoothness and speed (this sample application is available for download from the MSDN Magazine Web site).

Figure 6 Sample Performance-Testing Application
The main form has a user control called DrawingSurface, which has a background image, some text (“Hello World!” by default), and two timers. The first timer controls animation speed and is set to 10ms by default. This can be changed through the Interval setting. When the interval elapses, the text is redrawn in a different position on the drawing surface. Depending on the animation effect, the text is either bouncing or spinning.
The second timer is used to update actual animation speed displayed in the lower-right corner of the drawing surface. By actual animation speed I mean the number of paint events handled during the previous second. Actual animation speed might be lower than the desired animation speed. For example, a 10ms interval means that you would like the text to change its position 100 times per second.
The Background Image Layout options let you compare drawing performance between Tile and other image layout flags. I chose Center layout just as an example (Zoom or Stretch would have the same effect as Center). In this application I realigned the image to fit the drawing surface size independently of the image layout chosen. However, when the background is painted, Tile layout affects performance. Also when Center layout is chosen, the DoubleBuffered option is set automatically for you. That isn’t the case when Tile layout is chosen.
You can see how different options affect performance by observing flickering effects and by checking the actual animation speed in the lower-right corner of the drawing surface. Animation speed depends on the drawing options, on the update interval you set, and on physical limitations of your machine.
In my tests, the best results were achieved when Double Buffering and Smart Invalidation techniques were applied, Background Image Layout was set to Center (or, more specifically, any layout other than Tile), and the BackgroundImage was prerendered using PixelFormat.Format32bppPArgb. Painting is smooth and flicker-free when all of these techniques are used. Plus, animation is faster.

 

Resource Management
The physical memory used by your application is an important performance metric. You should use the least possible memory and resources so there is as much as possible left over for other processes. This is not just about being a good citizen; your application will benefit from a lower memory footprint, and this benefit can be dramatic if your memory usage is big enough to consume available physical memory and push the machine into paging. But even if you are targeting high-end machines and paging is not your main threat, you should use memory wisely. The cost of memory management in the common language runtime (CLR) can be significant for applications that allocate memory carelessly.
Memory footprint, garbage collection, and managing native resources, such as window handles or GDI handles, are interdependent for Windows Forms applications. Failure to release unmanaged resources increases the pressure on the garbage collector (GC). Windows Forms attempts to track handle usage and may force additional garbage collection cycles by calling GC.Collect if it is in danger of running out of resources. Besides, the managed objects holding these resources will have to be finalized by the GC, which is more expensive than releasing them proactively in your application through the IDisposable interface.
When the lifetime of the object is explicitly known, the unmanaged resources associated with it should be released. If a class that you are using implements the Dispose pattern and you explicitly know when you are done with the object, definitely call Dispose. In the Dispose method you will call the same cleanup code that is in the Finalizer and inform the GC that it no longer needs to finalize the object by calling the GC.SuppressFinalization method. This will improve performance since all the unreferenced objects in generation zero will be collected during one GC cycle. If a collectable object needs to be finalized, at least two GC cycles will be required for collecting it.
Objects that implement IDisposable usually do so because they are holding onto resources that should be freed deterministically. Windows Forms controls, for example, hold Windows handles that need to be released. If you add and remove controls from your form dynamically, you should call Dispose on them—otherwise you’ll accumulate extra unwanted handles in your process. You only need to dispose the topmost control since its children will be disposed automatically.
If you show a form modally (using the ShowDialog method) you need to Dispose it after it is closed. It is not disposed automatically in this case, to enable you to inquire on its state after it is closed. Note that other forms are automatically disposed when they are closed.
Graphics objects such as brushes, pens, and fonts implement IDisposable as well, since they hold onto GDI handles. Usually you create these objects in the OnPaint method, which is called each time the control needs to be redrawn. If you don’t dispose them immediately, you may accumulate a large number of these objects and hold a lot of GDI handles before these objects are finalized by the GC. If you are writing your code in C# or Visual Basic, the using statement is a handy way to do this:

using (Pen p = new Pen(Color.Red, 1)) 
{
    e.Graphics.DrawLine(p, 0,0, 10,10);
}

Using p As Pen = New Pen(Color.Red, 1)
    e.Graphics.DrawLine(p, 0,0, 10,10)
End Using

If you’re using C++, its stack-allocation semantics are also very useful in this regard:

Pen p(Color::Red, 1);
e->Graphics->DrawLine(%p, 0,0, 10,10);
You should try to avoid invoking GC.Collect if you possibly can. The GC is self-tuning and will adjust itself according to application memory requirements. In most cases, programmatically invoking the GC will hinder that automatic tuning. The only time GC.Collect might be useful is when you know that you have just released a lot of memory and you would like it to be collected immediately. Generally, however, it is more efficient to allow the GC to simply manage itself.
Twelve Performance Tips
  • Load fewer modules at startup
  • Precompile assemblies using NGen
  • Place strong-named assemblies in the GAC
  • Avoid base address collisions
  • Avoid blocking on the UI thread
  • Perform lazy processing
  • Populate controls more quickly
  • Exercise more control over data binding
  • Reduce repainting
  • Use double buffering
  • Manage memory usage
  • Use reflection wisely
If your form has more than 200 child controls, you might think of ways to design it more efficiently. Each control consumes native resources, and managing a large number of controls is expensive. Instead, consider using ListView, TreeView, DataGridView, ListBox, ToolStrip, and MenuStrip. The individual items for these controls generally do not require a native handle to back each item.
If physical memory is in short demand and paging occurs, physical disk usage will be high (since pages are written to disk). Your application will perform unbearably slowly in this case and you will desperately need to reduce memory usage. The profiling tool in Visual Studio Team System will be helpful here. You can also use this tool to look at managed memory allocations, or you can use the CLR’s profiling support (CLR Profiler: No Code Can Hide from the Profiling API in the .NET Framework 2.0). Also you might want to use the Virtual Address Dump (VaDump) tool to look at the process working set:

VaDump –sop <pid>

 

Conclusion
The sidebar “Twelve Performance Tips ” provides you with a grab bag of ways to optimize application performance. All of them provide you with definite performance advantages, used both individually or together. Now you should be well-equipped with skills that will allow you to write fast and efficient Windows Forms applications that will impress your users and keep them happy while using your applications.

 

Milena Salman is a Software Design Engineer on the .NET Client (Windows Forms) team at Microsoft. She focuses on performance tuning of Windows Forms. You can reach Milena at msalman@microsoft.com. Thanks to Shawn Burke and Jessica Fosler from the Windows Forms team for their valuable contribution to this article.
This entry was posted in Best Practices. Bookmark the permalink.

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s