WinForms ListView Performance

While working on improving the performance of the version control UI for Visual Studio Team Foundation Server, I encountered a number of potential problems with the WinForms ListView control.

As it turns out, adding items to a ListView can be VERY expensive if the list is sorted (as they should be). Below is a test program I threw together to demonstrate some of the performance tradeoffs and mistakes I've encountered. 

The first example is "Normal mode". I say this is normal because this is probably what most people do to start with - add a sorter to the ListView early on and be done with it - set it and forget it. Then you just add your of items. With a few hundred items it performs just fine. However, once you get over a certain threshold the performance degrades significantly. What happens is that ListView gets sorted after every ListViewItem is added to the Items collection. This is very sloooooow. On my computer it took 123,843 milliseconds to build the list and resulted in 70,621,263 calls to my Compare function. OUCH!

The next example, No Sorter1, is the first optimization folks seem to make. You remove the sorter, add your items, set the sorter back, and then call Sort. This is a noticeable improvement. The problem here is that there is no need to call Sort! When you set the sorter back WinForms will call Sort for you. On my computer it took 1812 milliseconds and resulted in 85,030 calls to my Compare function. Much better!

The next example, No Sorter2, is the optimization that a lot of folks miss because the previous one helped so much. This one really matters when you have an expensive Compare function. It's the same as the previous example with the unnecessary Sort call removed. On my computer it took 1765 milliseconds and resulted in 55,226 calls to my Compare function. Still better!

Next is to use AddRange. As it turns out WinForms has an opportunity to do an even better job than you internally because they have direct access to their interactions with the underlying Win32 ListView control. On my computer it took 546 milliseconds and resulted in 55,226 calls to my Compare function. This is LOTS better!

The final example uses the virtual capabilities of the ListView object. The way this works is you maintain your own cache of ListViewItem objects and supply them in a callback when you are asked. This results in a dramatic speed up when dealing with a large number of items. For this case I just used Array.Sort. On my computer it took 46 milliseconds and resulted in 91,800 calls to my Compare function. 

Note that in one of the cases I was working on, the virtual mode performed worse than the AddRange and the No Sorter modes. Our IComparer in that case is far more expensive than the simple one in my example.

MEASURE!!!

 
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
 
namespace ListViewPerf
{
    public partial class Form1 : Form
    {
        const int ListEntryCount = 5000;
        static int seed = 7919;         //  use a constant for repeatable results
 
        String[] strings;
        ListViewItem[] virtualItems;
        MySorter sorter;
        DateTime start;
 
        public Form1()
        {
            Random rand = new Random(seed);
            strings = new String[ListEntryCount];
 
            for (int i = 0; i < ListEntryCount; i++)
            {
                strings[i] = rand.Next().ToString();
            }
 
            sorter = new MySorter();
 
            InitializeComponent();
        }
 
        void PreCall()
        {
            listView1.ListViewItemSorter = null;
 
            if (!listView1.VirtualMode)
            {
                listView1.Items.Clear();
            }
            else
            {
                virtualItems = null;
            }
            listView1.VirtualMode = false;
            sorter.ResetCounter();
            start = DateTime.Now;
        }
 
        void PostCall(string s)
        {
            TimeSpan ts = DateTime.Now - start;
 
            MessageBox.Show(s + " took " + ts.TotalMilliseconds.ToString() + " milliseconds\nAnd resulted in " +  sorter.Counter.ToString() + " Compare calls");
        }
 
        void FillListViewWithStrings()
        {
            for (int i = 0; i < ListEntryCount; i++)
            {
                ListViewItem lvi = new ListViewItem(strings[i]);
                listView1.Items.Add(lvi);
            }
        }
 
        private void normalButton_Click(object sender, EventArgs e)
        {
            PreCall();
 
            listView1.ListViewItemSorter = sorter;
 
            FillListViewWithStrings();
 
            PostCall("Normal mode");
        }
 
        private void noSorter1Button_Click(object sender, EventArgs e)
        {
            PreCall();
 
            FillListViewWithStrings();
 
            listView1.ListViewItemSorter = sorter;
            listView1.Sort();
            
            PostCall("No Sorter1 mode");
        }
 
        private void noSorter2Button_Click(object sender, EventArgs e)
        {
            PreCall();
 
            FillListViewWithStrings();
 
            listView1.ListViewItemSorter = sorter;
 
            PostCall("No Sorter2 mode");
        }
 
        ListViewItem[] BuildListViewItems()
        {
            ListViewItem[] items = new ListViewItem[ListEntryCount];
 
            for (int i = 0; i < ListEntryCount; i++)
            {
                items[i] = new ListViewItem(strings[i]);
            }
 
            return items;
        }
 
        private void addRangeButton_Click(object sender, EventArgs e)
        {
            PreCall();
 
            listView1.ListViewItemSorter = sorter;
            ListViewItem[] items = BuildListViewItems();
            listView1.Items.AddRange(items);
            
            PostCall("AddRange mode");
        }
 
        private void virtualButton_Click(object sender, EventArgs e)
        {
            PreCall();
 
            virtualItems = BuildListViewItems();
            Array.Sort(virtualItems, sorter);
            listView1.VirtualMode = true;
            listView1.VirtualListSize = virtualItems.Length;
 
            PostCall("Virtual mode");
        }
 
        private void listView1_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
        {
            e.Item = virtualItems[e.ItemIndex];
        }
    }
 
    public class MySorter : IComparer
    {
        int counter;
 
        public void ResetCounter()
        {
            counter = 0;
        }
 
        public int Counter
        {
            get { return counter; }
        }
 
        int IComparer.Compare(Object x, Object y)
        {
            ListViewItem lviX = (ListViewItem)x;
            ListViewItem lviY = (ListViewItem)y;
            counter++;
            return String.Compare(lviX.Text, lviY.Text);
        }
 
    }
}