Tuesday, March 21, 2006

WinForms ListView Performance - ListView collections

The WinForms ListView has a few very handy collections - CheckedItems and CheckedIndices.  SelectedItems is a collection of the ListViewItems that are selected.  SelectedIndices is a collection of the indexes of the ListViewItems that are selected.  Those values can be used to pick out ListViewItems from the ListView.Items collection.  CheckedItems and CheckedIndices are just like the SelectedXXX collections only they are for the items that are checked.


The SelectedXXX collections perform very well.  Underneath the covers they are implemented by sending the Win32 ListView control a LVM_GETNEXTITEM message specifying LVNI_SELECTED to find the next selected item.  The Count properties are implemented by sending the Win32 ListView control a LVM_GETSELECTEDCOUNT message.


On the other hand...


The CheckedXXX collections are very expensive.  Underneath the covers they enumerate the listview Items collection for just about every operation.  To retrieve the Count for the CheckedXXX collections WinForms will walk the entire Items collection and test the Checked property of each ListView Item.  Querying the Checked property of each item results in sending a LVM_GETITEMSTATE message to the Win32 ListView control to query for the checked state.  To reference an item from the CheckedXXX collections iterates over the ListView Items collection stopping at the index you ask for.  So the worst case performance is if all items in the ListView are checked.


My previous ListView examples were working with 5000 items.  Well, for the CheckedXXX collections just a few hundred items is enough to exhibit performance problems depending on how you write your code.  It doesn't make much difference whether you use the CheckedItems collection or the CheckedIndices collection since the CheckedItem collection basically does its job via the CheckedIndices collection.  What makes a huge difference is how you access the items.  Index lookup is very slow because of how the next item is found by starting at the beginning again each time.  If for some reason you must do this, then at least cache the Count value at the start of your loop if possible because it is very expensive to recompute.  The fastest way to use these collections is via enumeration such as a foreach() loop.  Even that is not as fast as just enumerating the Items collection yourself and testing each item's Checked property yourself.


For the Team Foundation Server version control UI, we sometimes have 50,000 or more items if you are doing a big branch or adding an existing tree.  I ended up keeping a separate cache of the checked items myself - I just updated the cache during ListView Checked events.  Ideally we would have used a virtual ListView but there were other unrelated performance problems with sorting that made this actually perform worse in our case.


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 = 500;
        static int seed = 7919;         //  use a constant for repeatable results
 
        String[] strings;
        bool[] checksInit;
        object dontCare;
        int processed;
 
        public Form1()
        {
            Random rand = new Random(seed);
            strings = new String[ListEntryCount];
            checksInit = new bool[ListEntryCount];
 
            for (int i = 0; i < ListEntryCount; i++)
            {
                strings[i] = rand.Next().ToString();
                checksInit[i] = (rand.Next() % 2) == 0;
            }
 
            InitializeComponent();
        }
 
        void FillListView()
        {
            ListViewItem[] items = new ListViewItem[ListEntryCount];
 
            for (int i = 0; i < ListEntryCount; i++)
            {
                items[i] = new ListViewItem(strings[i]);
                items[i].Checked = checksInit[i];
            }
            listView1.Items.AddRange(items);
        }
 
        //  make the compiler believe we're doing something important here...
        void ProcessListViewItem(ListViewItem lvi)
        {
            dontCare = lvi.Tag;
            processed++;
        }
 
        void CheckAll()
        {
            foreach (ListViewItem item in listView1.Items)
            {
                item.Checked = true;
            }
        }
 
        void FetchViaItems()
        {
            processed = 0;
            int count;
            DateTime start;
            TimeSpan tsCheckedIteratorWaySlow;
            TimeSpan tsCheckedIterator;
            TimeSpan tsCheckedEnumerator;
 
            start = DateTime.Now;
            for (int i = 0; i < listView1.CheckedItems.Count; i++)
            {
                ListViewItem lvi = listView1.CheckedItems[i];
                ProcessListViewItem(lvi);
            }
            tsCheckedIteratorWaySlow = DateTime.Now - start;
 
            start = DateTime.Now;
            count = listView1.CheckedItems.Count;
            for (int i = 0; i < count; i++)
            {
                ListViewItem lvi = listView1.CheckedItems[i];
                ProcessListViewItem(lvi);
            }
            tsCheckedIterator = DateTime.Now - start;
 
            start = DateTime.Now;
            foreach (ListViewItem lvi in listView1.CheckedItems)
            {
                ProcessListViewItem(lvi);
            }
            tsCheckedEnumerator = DateTime.Now - start;
 
            MessageBox.Show("Checked ListViewItems via CheckedItems collection:\n" +
                "CheckedIteratorWaySlow " + tsCheckedIteratorWaySlow.TotalMilliseconds.ToString() + " milliseconds\n" +
                "CheckedIterator " + tsCheckedIterator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "CheckedEnumerator " + tsCheckedEnumerator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "Processed " + processed.ToString()
            );
        }
 
        private void ViaItemsButton_Click(object sender, EventArgs e)
        {
            listView1.Items.Clear();
            FillListView();
           
            //  First test a random scattering of checked items
            FetchViaItems();
           
            //  Then test with all checked
            CheckAll();
            FetchViaItems();
        }
 
        void FetchViaIndeces()
        {
            processed = 0;
            int count;
            DateTime start;
            TimeSpan tsCheckedIteratorWaySlow;
            TimeSpan tsCheckedIterator;
            TimeSpan tsCheckedEnumerator;
 
            start = DateTime.Now;
            for (int i = 0; i < listView1.CheckedIndices.Count; i++)
            {
                ListViewItem lvi = listView1.Items[listView1.CheckedIndices[i]];
                ProcessListViewItem(lvi);
            }
            tsCheckedIteratorWaySlow = DateTime.Now - start;
 
            start = DateTime.Now;
            count = listView1.CheckedIndices.Count;
            for (int i = 0; i < count; i++)
            {
                ListViewItem lvi = listView1.Items[listView1.CheckedIndices[i]];
                ProcessListViewItem(lvi);
            }
            tsCheckedIterator = DateTime.Now - start;
 
            start = DateTime.Now;
            foreach (int index in listView1.CheckedIndices)
            {
                ListViewItem lvi = listView1.Items[index];
                ProcessListViewItem(lvi);
            }
            tsCheckedEnumerator = DateTime.Now - start;
 
            MessageBox.Show("Checked ListViewItems via CheckedIndices collection:\n" +
                "CheckedIteratorWaySlow " + tsCheckedIteratorWaySlow.TotalMilliseconds.ToString() + " milliseconds\n" +
                "CheckedIterator " + tsCheckedIterator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "CheckedEnumerator " + tsCheckedEnumerator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "Processed " + processed.ToString()
            );
        }
 
        private void ViaIndecesButton_Click(object sender, EventArgs e)
        {
            listView1.Items.Clear();
            FillListView();
 
            FetchViaIndeces();
 
            CheckAll();
            FetchViaIndeces();
        }
 
        void FetchViaManual()
        {
            processed = 0;
            int count;
            DateTime start;
            TimeSpan tsIteratorWaySlow;
            TimeSpan tsIterator;
            TimeSpan tsEnumerator;
 
            start = DateTime.Now;
            for (int i = 0; i < listView1.Items.Count; i++)
            {
                ListViewItem lvi = listView1.Items[i];
                if (lvi.Checked)
                {
                    ProcessListViewItem(lvi);
                }
            }
            tsIteratorWaySlow = DateTime.Now - start;
 
            start = DateTime.Now;
            count = listView1.Items.Count;
            for (int i = 0; i < count; i++)
            {
                ListViewItem lvi = listView1.Items[i];
                if (lvi.Checked)
                {
                    ProcessListViewItem(lvi);
                }
            }
            tsIterator = DateTime.Now - start;
 
            start = DateTime.Now;
            foreach (ListViewItem lvi in listView1.Items)
            {
                if (lvi.Checked)
                {
                    ProcessListViewItem(lvi);
                }
            }
            tsEnumerator = DateTime.Now - start;
 
            MessageBox.Show("Checked ListViewItems via Manual lookup:\n" +
                "IteratorWaySlow " + tsIteratorWaySlow.TotalMilliseconds.ToString() + " milliseconds\n" +
                "Iterator " + tsIterator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "Enumerator " + tsEnumerator.TotalMilliseconds.ToString() + " milliseconds\n" +
                "Processed " + processed.ToString()
            );
        }
 
        private void ViaManual_Click(object sender, EventArgs e)
        {
            listView1.Items.Clear();
            FillListView();
 
            FetchViaManual();
 
            CheckAll();
            FetchViaManual();
        }
    }
}

No comments: