Jon Skeet a récemment soulevé un sujet de programmation intéressant sur son blog: "Il y a un trou dans mon abstraction, chère Liza, chère Liza" (italiques ajoutés):
J'ai un ensemble - un
HashSet
, en fait. Je souhaite supprimer certains éléments de celui-ci… et de nombreux éléments peuvent bien ne pas exister. En fait, dans notre cas de test, aucun des éléments de la collection «suppressions» ne sera dans l'ensemble d'origine. Cela semble - et est en effet - extrêmement facile à coder. Après tout, nous devonsSet<T>.removeAll
nous aider, non?Nous spécifions la taille de l'ensemble «source» et la taille de la collection «suppressions» sur la ligne de commande, et construisons les deux. L'ensemble source contient uniquement des entiers non négatifs; l'ensemble des suppressions ne contient que des entiers négatifs. Nous mesurons le temps nécessaire pour supprimer tous les éléments à l'aide de
System.currentTimeMillis()
, ce qui n'est pas le chronomètre le plus précis au monde, mais qui est plus que suffisant dans ce cas, comme vous le verrez. Voici le code:import java.util.*; public class Test { public static void main(String[] args) { int sourceSize = Integer.parseInt(args[0]); int removalsSize = Integer.parseInt(args[1]); Set<Integer> source = new HashSet<Integer>(); Collection<Integer> removals = new ArrayList<Integer>(); for (int i = 0; i < sourceSize; i++) { source.add(i); } for (int i = 1; i <= removalsSize; i++) { removals.add(-i); } long start = System.currentTimeMillis(); source.removeAll(removals); long end = System.currentTimeMillis(); System.out.println("Time taken: " + (end - start) + "ms"); } }
Commençons par lui donner une tâche facile: un ensemble source de 100 éléments et 100 à supprimer:
c:UsersJonTest>java Test 100 100 Time taken: 1ms
D'accord, nous ne nous attendions pas à ce que ce soit lent… clairement, nous pouvons accélérer les choses un peu. Que diriez-vous d'une source d'un million d'articles et de 300 000 articles à supprimer?
c:UsersJonTest>java Test 1000000 300000 Time taken: 38ms
Hmm. Cela semble encore assez rapide. Maintenant, je sens que j'ai été un peu cruel, lui demandant de faire tout ce retrait. Facilitons un peu la tâche - 300 000 éléments sources et 300 000 suppressions:
c:UsersJonTest>java Test 300000 300000 Time taken: 178131ms
Pardon? Près de trois minutes ? Yikes! Il devrait sûrement être plus facile de supprimer des éléments d'une collection plus petite que celle que nous avons gérée en 38 ms?
Quelqu'un peut-il expliquer pourquoi cela se produit? Pourquoi la HashSet<T>.removeAll
méthode est-elle si lente?
la source
Réponses:
Le comportement est (quelque peu) documenté dans le javadoc :
Ce que cela signifie en pratique, lorsque vous appelez
source.removeAll(removals);
:si la
removals
collection est d'une taille inférieure àsource
, laremove
méthode deHashSet
est appelée, ce qui est rapide.si la
removals
collection est de taille égale ou supérieure àsource
, alorsremovals.contains
est appelée, ce qui est lent pour une ArrayList.Solution rapide:
Notez qu'il existe un bogue ouvert très similaire à ce que vous décrivez. L'essentiel semble être que c'est probablement un mauvais choix mais qu'il ne peut pas être changé car il est documenté dans le javadoc.
Pour référence, voici le code de
removeAll
(dans Java 8 - je n'ai pas vérifié les autres versions):la source
ArrayList#contains
c'était le coupable. Un regard sur le code de aAbstractSet#removeAll
donné le reste de la réponse.