56 votes

Pourquoi la fonction de suppression d'ArrayList de Java semble-t-elle coûter si peu ?

J'ai une fonction qui manipule une très grande liste, dépassant environ 250 000 éléments. Pour la majorité de ces éléments, elle remplace simplement l'élément à la position x. Cependant, pour environ 5 % d'entre eux, elle doit les retirer de la liste.

L'utilisation d'une LinkedList semblait être la solution la plus évidente pour éviter les suppressions coûteuses. Cependant, l'accès à une liste de liens par index devient naturellement de plus en plus lent au fil du temps. Le prix à payer ici est celui des minutes (et beaucoup de minutes).

L'utilisation d'un Iterator sur cette LinkedList est également coûteuse, car il semble que j'aie besoin d'une copie séparée pour éviter les problèmes de concurrence de l'Iterator lors de la modification de cette liste. Le coût est ici de quelques minutes.

Cependant, c'est ici que mon esprit est un peu ébranlé. Si je change pour une ArrayList, cela fonctionne presque instantanément.

Pour une liste de 297515 éléments, supprimer 11958 éléments et modifier tout le reste prend 909ms. J'ai vérifié que la liste résultante fait bien 285557 éléments, comme prévu, et contient les informations mises à jour dont j'ai besoin.

Pourquoi c'est si rapide ? J'ai regardé la source de ArrayList dans le JDK6 et il semble qu'il utilise une fonction arraycopy comme prévu. J'aimerais comprendre pourquoi une ArrayList fonctionne si bien ici alors que le bon sens semble indiquer qu'un tableau pour cette tâche est une idée terrible, nécessitant le déplacement de plusieurs centaines de milliers d'éléments.

29voto

finnw Points 24592

J'ai effectué un benchmark, en essayant chacune des stratégies suivantes pour filtrer les éléments de la liste :

  • Copier les éléments recherchés dans une nouvelle liste
  • Utilisez Iterator.remove() pour supprimer les éléments indésirables d'un ArrayList
  • Utilisez Iterator.remove() pour supprimer les éléments indésirables d'un LinkedList
  • Compacter la liste en place (déplacer les éléments recherchés vers des positions inférieures)
  • Suppression par index ( List.remove(int) ) sur un ArrayList
  • Suppression par index ( List.remove(int) ) sur un LinkedList

Chaque fois que j'ai rempli la liste avec 100000 instances aléatoires de Point et utilisé une condition de filtrage (basée sur le code de hachage) qui accepterait 95 % des éléments et rejetterait les 5 % restants (la même proportion que celle indiquée dans la question, mais avec une liste plus petite car je n'ai pas eu le temps d'exécuter le test pour 250000 éléments).

Et les temps moyens (sur mon vieux MacBook Pro : Core 2 Duo, 2.2GHz, 3Gb RAM) étaient :

CopyIntoNewListWithIterator   :      4.24ms
CopyIntoNewListWithoutIterator:      3.57ms
FilterLinkedListInPlace       :      4.21ms
RandomRemoveByIndex           :    312.50ms
SequentialRemoveByIndex       :  33632.28ms
ShiftDown                     :      3.75ms

Ainsi, le retrait d'éléments par index d'un LinkedList était plus de 300 fois plus coûteux que de les retirer d'un système de gestion des déchets. ArrayList et probablement entre 6 000 et 10 000 fois plus cher que les autres méthodes (qui évitent la recherche linéaire et l'analyse des données). arraycopy )

Ici, il ne semble pas y avoir beaucoup de différence entre les quatre méthodes les plus rapides, mais j'ai réexécuté ces quatre méthodes avec une liste de 500 000 éléments et j'ai obtenu les résultats suivants :

CopyIntoNewListWithIterator   :     92.49ms
CopyIntoNewListWithoutIterator:     71.77ms
FilterLinkedListInPlace       :     15.73ms
ShiftDown                     :     11.86ms

Je suppose qu'avec une taille plus importante, la mémoire cache devient le facteur limitant, et que le coût de la création d'une seconde copie de la liste devient significatif.

Voici le code :

import java.awt.Point;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;

public class ListBenchmark {

    public static void main(String[] args) {
        Random rnd = new SecureRandom();
        Map<String, Long> timings = new TreeMap<String, Long>();
        for (int outerPass = 0; outerPass < 10; ++ outerPass) {
            List<FilterStrategy> strategies =
                Arrays.asList(new CopyIntoNewListWithIterator(),
                              new CopyIntoNewListWithoutIterator(),
                              new FilterLinkedListInPlace(),
                              new RandomRemoveByIndex(),
                              new SequentialRemoveByIndex(),
                              new ShiftDown());
            for (FilterStrategy strategy: strategies) {
                String strategyName = strategy.getClass().getSimpleName();
                for (int innerPass = 0; innerPass < 10; ++ innerPass) {
                    strategy.populate(rnd);
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        if (totalTime == null) totalTime = 0L;
                        timings.put(strategyName, totalTime - System.currentTimeMillis());
                    }
                    Collection<Point> filtered = strategy.filter();
                    if (outerPass >= 5 && innerPass >= 5) {
                        Long totalTime = timings.get(strategyName);
                        timings.put(strategy.getClass().getSimpleName(), totalTime + System.currentTimeMillis());
                    }
                    CHECKSUM += filtered.hashCode();
                    System.err.printf("%-30s %d %d %d%n", strategy.getClass().getSimpleName(), outerPass, innerPass, filtered.size());
                    strategy.clear();
                }
            }
        }
        for (Map.Entry<String, Long> e: timings.entrySet()) {
            System.err.printf("%-30s: %9.2fms%n", e.getKey(), e.getValue() * (1.0/25.0));
        }
    }

    public static volatile int CHECKSUM = 0;

    static void populate(Collection<Point> dst, Random rnd) {
        for (int i = 0; i < INITIAL_SIZE; ++ i) {
            dst.add(new Point(rnd.nextInt(), rnd.nextInt()));
        }
    }

    static boolean wanted(Point p) {
        return p.hashCode() % 20 != 0;
    }

    static abstract class FilterStrategy {
        abstract void clear();
        abstract Collection<Point> filter();
        abstract void populate(Random rnd);
    }

    static final int INITIAL_SIZE = 100000;

    private static class CopyIntoNewListWithIterator extends FilterStrategy {
        public CopyIntoNewListWithIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            ArrayList<Point> dst = new ArrayList<Point>(list.size());
            for (Point p: list) {
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class CopyIntoNewListWithoutIterator extends FilterStrategy {
        public CopyIntoNewListWithoutIterator() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            ArrayList<Point> dst = new ArrayList<Point>(inputSize);
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) dst.add(p);
            }
            return dst;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class FilterLinkedListInPlace extends FilterStrategy {
        public String toString() {
            return getClass().getSimpleName();
        }
        FilterLinkedListInPlace() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (Iterator<Point> it = list.iterator();
                 it.hasNext();
                 ) {
                Point p = it.next();
                if (! wanted(p)) it.remove();
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class RandomRemoveByIndex extends FilterStrategy {
        public RandomRemoveByIndex() {
            list = new ArrayList<Point>(INITIAL_SIZE);
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

    private static class SequentialRemoveByIndex extends FilterStrategy {
        public SequentialRemoveByIndex() {
            list = new LinkedList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            for (int i = 0; i < list.size();) {
                if (wanted(list.get(i))) {
                    ++ i;
                } else {
                    list.remove(i);
                }
            }
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final LinkedList<Point> list;
    }

    private static class ShiftDown extends FilterStrategy {
        public ShiftDown() {
            list = new ArrayList<Point>();
        }
        @Override
        void clear() {
            list.clear();
        }
        @Override
        Collection<Point> filter() {
            int inputSize = list.size();
            int outputSize = 0;
            for (int i = 0; i < inputSize; ++ i) {
                Point p = list.get(i);
                if (wanted(p)) {
                    list.set(outputSize++, p);
                }
            }
            list.subList(outputSize, inputSize).clear();
            return list;
        }
        @Override
        void populate(Random rnd) {
            ListBenchmark.populate(list, rnd);
        }
        private final ArrayList<Point> list;
    }

}

17voto

Howard Points 23487

La copie d'un tableau est une opération assez peu coûteuse. Elle est effectuée à un niveau très basique (c'est une méthode statique native de Java) et vous n'êtes pas encore dans le domaine où les performances deviennent vraiment importantes.

Dans votre exemple, vous copiez environ 12000 fois un tableau de taille 150000 (en moyenne). Cela ne prend pas beaucoup de temps. Je l'ai testé ici sur mon ordinateur portable et cela a pris moins de 500 ms.

Mise à jour J'ai utilisé le code suivant pour mesurer sur mon ordinateur portable (Intel P8400)

import java.util.Random;

public class PerformanceArrayCopy {

    public static void main(String[] args) {

        int[] lengths = new int[] { 10000, 50000, 125000, 250000 };
        int[] loops = new int[] { 1000, 5000, 10000, 20000 };

        for (int length : lengths) {
            for (int loop : loops) {

                Object[] list1 = new Object[length];
                Object[] list2 = new Object[length];

                for (int k = 0; k < 100; k++) {
                    System.arraycopy(list1, 0, list2, 0, list1.length);
                }

                int[] len = new int[loop];
                int[] ofs = new int[loop];

                Random rnd = new Random();
                for (int k = 0; k < loop; k++) {
                    len[k] = rnd.nextInt(length);
                    ofs[k] = rnd.nextInt(length - len[k]);
                }

                long n = System.nanoTime();
                for (int k = 0; k < loop; k++) {
                    System.arraycopy(list1, ofs[k], list2, ofs[k], len[k]);
                }
                n = System.nanoTime() - n;
                System.out.print("length: " + length);
                System.out.print("\tloop: " + loop);
                System.out.print("\truntime [ms]: " + n / 1000000);
                System.out.println();
            }
        }
    }
}

Quelques résultats :

length: 10000   loop: 10000 runtime [ms]: 47
length: 50000   loop: 10000 runtime [ms]: 228
length: 125000  loop: 10000 runtime [ms]: 575
length: 250000  loop: 10000 runtime [ms]: 1198

10voto

maple_shaft Points 7616

Je pense que la différence de performance est probablement due au fait que ArrayList supporte l'accès aléatoire alors que LinkedList ne le fait pas.

Si je veux obtenir (1000) d'une ArrayList, je spécifie un index spécifique pour y accéder, mais LinkedList ne supporte pas cela car elle est organisée par des références de nœuds.

Si j'appelle get(1000) de LinkedList, la liste entière sera itérée jusqu'à ce qu'elle trouve l'indice 1000, ce qui peut s'avérer excessivement coûteux si la LinkedList contient un grand nombre d'éléments.

6voto

dkamins Points 10565

Des résultats intéressants et inattendus. Ce n'est qu'une hypothèse, mais...

En moyenne, l'une des suppressions d'éléments de votre tableau nécessitera de déplacer la moitié de votre liste (tout ce qui suit) d'un élément en arrière. Si chaque élément est un pointeur 64 bits vers un objet (8 octets), cela signifie qu'il faut copier 125 000 éléments x 8 octets par pointeur = 1 Mo.

Un processeur moderne peut copier un bloc contigu de 1 Mo de RAM en RAM assez rapidement.

Par rapport au fait de boucler sur une liste liée pour chaque accès, ce qui nécessite des comparaisons, des branchements et d'autres activités peu favorables au CPU, la copie en RAM est rapide.

Vous devriez vraiment essayer d'évaluer les différentes opérations indépendamment et voir à quel point elles sont efficaces avec différentes implémentations de listes. Partagez vos résultats ici si vous le faites !

6voto

haylem Points 11504

Je passe volontairement sous silence certains détails d'implémentation, juste pour expliquer la différence fondamentale.

Pour supprimer le N-ième élément d'une liste de M éléments, l'implémentation de LinkedList se rendra jusqu'à cet élément, puis le supprimera simplement et mettra à jour les pointeurs des éléments N-1 et N+1 en conséquence. Cette deuxième opération est très simple, mais c'est le fait de se rendre jusqu'à cet élément qui vous fait perdre du temps.

En revanche, pour une ArrayList, le temps d'accès est instantané car elle est adossée à un tableau, c'est-à-dire à des espaces mémoire contigus. Vous pouvez sauter directement à la bonne adresse mémoire pour effectuer, en gros, les opérations suivantes :

  • réaffecter un nouveau tableau de M - 1 éléments
  • placer tout ce qui est compris entre 0 et N - 1 à l'indice 0 dans le tableau de la nouvelle liste de tableaux.
  • mettre tout ce qui est N + 1 à M à l'indice N dans le tableau de l'arraylist.

En y réfléchissant, vous remarquerez que vous pouvez même réutiliser le même tableau, car Java peut utiliser ArrayList avec des tailles pré-allouées, donc si vous supprimez des éléments, vous pouvez tout aussi bien sauter les étapes 1 et 2 et effectuer directement l'étape 3 et mettre à jour votre taille.

Les accès à la mémoire sont rapides, et la copie d'un morceau de mémoire est probablement suffisamment rapide sur le matériel moderne pour que le passage à la position N prenne trop de temps.

Toutefois, si vous utilisez votre LinkedList de manière à ce qu'elle vous permette de supprimer plusieurs éléments qui se suivent et de garder la trace de votre position, vous verrez un gain.

Mais il est clair que sur une longue liste, faire un simple remove(i) sera coûteux.


Pour ajouter un peu de sel et d'épice à cela :

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X