Comment accélérer l’ajout d’éléments à un ListView?

J’ajoute quelques milliers (par exemple 53 709) éléments à un WinForms ListView.

Tentative 1 : 13,870 ms

 foreach (Object o in list) { ListViewItem item = new ListViewItem(); RefreshListViewItem(item, o); listView.Items.Add(item); } 

Cela fonctionne très mal. La première solution évidente consiste à appeler BeginUpdate/EndUpdate .

Tentative 2 : 3,106 ms

 listView.BeginUpdate(); foreach (Object o in list) { ListViewItem item = new ListViewItem(); RefreshListViewItem(item, o); listView.Items.Add(item); } listView.EndUpdate(); 

C’est mieux, mais toujours un ordre de grandeur trop lent. Séparons la création de ListViewItems de l’ajout de ListViewItems, nous trouvons donc le véritable coupable:

Tentative 3 : 2,631 ms

 var items = new List(); foreach (Object o in list) { ListViewItem item = new ListViewItem(); RefreshListViewItem(item, o); items.Add(item); } stopwatch.Start(); listView.BeginUpdate(); foreach (ListViewItem item in items) listView.Items.Add(item)); listView.EndUpdate(); stopwatch.Stop() 

Le véritable goulot d’étranglement consiste à append les éléments. Essayons de le convertir en AddRange plutôt qu’en foreach

Tentative 4: 2,182 ms

 listView.BeginUpdate(); listView.Items.AddRange(items.ToArray()); listView.EndUpdate(); 

Un peu mieux. Soyons sûr que le goulot d’étranglement n’est pas dans le ToArray()

Tentative 5: 2,132 ms

 ListViewItem[] arr = items.ToArray(); stopwatch.Start(); listView.BeginUpdate(); listView.Items.AddRange(arr); listView.EndUpdate(); stopwatch.Stop(); 

La limitation semble append des éléments à la vue de liste. Peut-être l’autre surcharge de AddRange , où nous ajoutons un ListView.ListViewItemCollection plutôt qu’un tableau

Tentative 6: 2,141 ms

 listView.BeginUpdate(); ListView.ListViewItemCollection lvic = new ListView.ListViewItemCollection(listView); lvic.AddRange(arr); listView.EndUpdate(); 

Eh bien, ce n’est pas mieux.

Maintenant il est temps de s’étirer:

  • Étape 1 – assurez-vous qu’aucune colonne n’est définie sur “auto-width” :

    entrer la description de l'image ici

    Vérifier

  • Étape 2 – assurez-vous que ListView n’essaie pas de sortinger les éléments chaque fois que j’en ajoute un:

    entrer la description de l'image ici

    Vérifier

  • Étape 3 – Demander un stackoverflow:

    entrer la description de l'image ici

    Vérifier

Note: Évidemment, cette ListView n’est pas en mode virtuel; puisque vous ne pouvez / ne pouvez pas “append” d’éléments à une vue de liste virtuelle (vous définissez la VirtualListSize ). Heureusement, ma question ne concerne pas une vue de liste en mode virtuel.

Y a-t-il quelque chose qui me manque qui pourrait expliquer l’ajout d’éléments à la listview étant si lente?


Chatter Bonus

Je sais que la classe Windows ListView peut faire mieux, car je peux écrire du code qui le fait en 394 ms :

 ListView1.Items.BeginUpdate; for i := 1 to 53709 do ListView1.Items.Add(); ListView1.Items.EndUpdate; 

qui, comparé au code C # équivalent 1,349 ms :

 listView.BeginUpdate(); for (int i = 1; i <= 53709; i++) listView.Items.Add(new ListViewItem()); listView.EndUpdate(); 

est un ordre de grandeur plus rapide.

Quelle propriété du wrapper WinForms ListView suis-je manquant?

J’ai jeté un coup d’oeil au code source de la vue liste et j’ai remarqué quelques petites choses qui peuvent ralentir la performance d’un facteur 4 ou plus:

dans ListView.cs, ListViewItemsCollection.AddRange appelle ListViewNativeItemCollection.AddRange , qui est l’endroit où j’ai commencé mon audit

ListViewNativeItemCollection.AddRange (de la ligne: 18120) a deux passages dans la collection entière de valeurs, un pour collecter tous les éléments vérifiés et les “restaurer” après l’appel de InsertItems (ils sont tous deux protégés par une vérification du owner.IsHandleCreated , propriétaire étant le ListView ) appelle alors BeginUpdate .

ListView.InsertItems (de la ligne: 12952), premier appel, a un autre cheminement de la liste entière, puis ArrayList.AddRange est appelé (probablement un autre passage), puis un autre passage après. Menant à

ListView.InsertItems (à partir de la ligne: 12952), deuxième appel (via EndUpdate ), un autre passage à travers lequel ils sont ajoutés à un HashTable , et un Debug.Assert(!listItemsTable.ContainsKey(ItemId)) le ralentira davantage en mode débogage. Si le handle n’est pas créé, il ajoute les éléments à une ArrayList , listItemsArray mais if (IsHandleCreated) , il appelle

ListView.InsertItemsNative (de la ligne: 3848) passe à travers la liste où il est réellement ajouté à la vue de liste native. un Debug.Assert(this.Items.Contains(li) ralentira également les performances en mode débogage).

Il y a BEAUCOUP de passages supplémentaires à travers toute la liste des éléments du contrôle .net avant même qu’il n’arrive réellement à insérer les éléments dans la vue de liste native. Certains passages sont protégés par des vérifications sur le Handle en cours de création. Si vous pouvez append des éléments avant la création du handle, cela peut vous faire gagner du temps. La méthode OnHandleCreated prend le listItemsArray et appelle directement InsertItemsNative sans le listItemsArray supplémentaire.

Vous pouvez lire le code ListView dans la source de référence vous-même et jeter un coup d’oeil, peut-être que j’ai raté quelque chose.

Dans le numéro de mars 2006 de MSDN Magazine, il y avait un article intitulé Winning Forms: Practical Tips for Boosting The Performance of Windows Forms Apps .

Cet article contient notamment des conseils pour améliorer les performances de ListViews. Il semble indiquer que c’est plus rapide d’append des éléments avant que le descripteur ne soit créé, mais que vous payerez un prix lorsque le contrôle est rendu. Peut-être que l’application des optimisations de rendu mentionnées dans les commentaires et l’ajout des éléments avant la création du descripteur donneront le meilleur des deux mondes.

Edit: Testé cette hypothèse de différentes manières, et tout en ajoutant les éléments avant de créer le handle est plus rapide, il est exponentiellement plus lent quand il va créer le handle. J’ai joué en essayant de le piéger pour créer le handle, puis en quelque sorte le faire appeler InsertItemsNative sans passer par toutes les passes supplémentaires, mais hélas j’ai été contrecarré. La seule chose que je pourrais penser serait de créer votre Win32 ListView dans un projet c ++, de le bourrer avec des éléments et d’utiliser le hooking pour capturer le message CreateWindow envoyé par ListView lors de la création de son handle et de renvoyer une référence à win32. ListView au lieu d’une nouvelle fenêtre .. mais qui sait ce que le côté affecte il y aurait … un gourou Win32 aurait besoin de parler de cette idée folle 🙂

J’ai utilisé ce code:

 ResultsListView.BeginUpdate(); ResultsListView.ListViewItemSorter = null; ResultsListView.Items.Clear(); //here we add items to listview //adding item sorter back ResultsListView.ListViewItemSorter = lvwColumnSorter; ResultsListView.Sort(); ResultsListView.EndUpdate(); 

J’ai également défini GenerateMember sur false pour chaque colonne.

Lien vers le sortingeur de vue de liste personnalisée: http://www.codeproject.com/Articles/5332/ListView-Column-Sorter

J’ai le même problème. Ensuite, j’ai trouvé que le sorter rendait si lent. Rendez la sortingeuse nulle

 this.listViewAbnormalList.ListViewItemSorter = null; 

alors quand cliquez sur sortingeur, sur la méthode ListView_ColumnClick , le rendre

  lv.ListViewItemSorter = new ListViewColumnSorter() 

Enfin, après avoir été sortingé, rendre le sorter nouveau nul

  ((System.Windows.Forms.ListView)sender).Sort(); lv.ListViewItemSorter = null; 

Créez tous vos ListViewItems FIRST , puis ajoutez-les tout à la fois à ListView .

Par exemple:

  var theListView = new ListView(); var items = new ListViewItem[ 53709 ]; for ( int i = 0 ; i < items.Length; ++i ) { items[ i ] = new ListViewItem( i.ToString() ); } theListView.Items.AddRange( items );