web-dev-qa-db-fra.com

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

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

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

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

Cependant, voici où mon esprit est un peu soufflé. Si je passe à une liste de tableaux, elle s'exécute presque instantanément.

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

Pourquoi est-ce si rapide? J'ai regardé la source d'ArrayList dans JDK6 et il semble utiliser 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 de déplacer plusieurs centaines de milliers d'éléments.

55
Ken

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

  • Copiez les éléments souhaité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 sur place (déplacer les éléments souhaités vers des positions plus basses)
  • Supprimer par index (List.remove(int)) sur un ArrayList
  • Supprimer par index (List.remove(int)) sur un LinkedList

Chaque fois que j'ai rempli la liste avec 100 000 instances aléatoires de Point et utilisé une condition de filtre (basée sur le code de hachage) qui accepterait 95% des éléments et rejetterait les 5% restants (la même proportion 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 ancien MacBook Pro: Core 2 Duo, 2,2 GHz, 3 Go de RAM) étaient les suivants:

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

Ainsi, la suppression d'éléments par index d'un LinkedList était plus de 300 fois plus chère que leur suppression d'un ArrayList, et probablement quelque part entre 6000 et 10000 fois plus cher que les autres méthodes (qui évitent la linéarité recherche et arraycopy)

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

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

Je suppose qu'avec la mémoire cache de plus grande taille devient le facteur limitant, donc le coût de création d'une deuxième copie de la liste devient important.

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;
    }

}
29
finnw

La copie de tableau est une opération plutôt peu coûteuse. Cela se fait à un niveau très basique (c'est une Java méthode statique native) et vous n'êtes pas encore dans la plage 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
17
Howard

Je pense que la différence de performance est probablement due à la différence qu'ArrayList prend en charge l'accès aléatoire là où LinkedList ne le fait pas.

Si je veux obtenir (1000) une ArrayList, je spécifie un index spécifique pour y accéder, mais LinkedList ne le prend pas en charge car il est organisé via les références Node.

Si j'appelle get (1000) de LinkedList, il itérera toute la liste jusqu'à ce qu'il trouve l'index 1000 et cela peut être exorbitant si vous avez un grand nombre d'éléments dans LinkedList.

10
maple_shaft

Je saute sur certains détails d'implémentation à dessein ici, juste pour expliquer la différence fondamentale.

Pour supprimer le N-ème élément d'une liste d'éléments M, l'implémentation LinkedList remontera 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 se mettre à cet élément qui vous coûte du temps.

Cependant, pour une liste de tableaux, le temps d'accès est instantané car il est soutenu par un tableau, ce qui signifie des espaces mémoire contigus. Vous pouvez accéder directement à la bonne adresse mémoire pour effectuer, de manière générale, les opérations suivantes:

  • réallouer un nouveau tableau de M - 1 éléments
  • mettre tout de 0 à N - 1 à l'index 0 dans le nouveau tableau de l'arrayliste
  • mettre tout N + 1 à M à l'indice N dans le tableau de l'arraylist.

En y pensant, vous remarquerez que vous pouvez même réutiliser le même tableau que Java peut utiliser ArrayList avec des tailles pré-allouées, donc si vous supprimez des éléments, vous pourriez aussi bien ignorer les étapes 1 et 2 et faites directement l'étape 3 et mettez à 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 du matériel moderne pour que le passage à la position N prenne trop de temps.

Cependant, si vous utilisez votre LinkedList de manière à vous permettre de supprimer plusieurs éléments qui se suivent et de garder une trace de votre position, vous constaterez un gain.

Mais clairement, sur une longue liste, faire un simple retrait (i) sera coûteux.


Pour ajouter une touche de sel et d'épices à ceci:

6
haylem

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

En moyenne, l'un de vos suppressions d'éléments de tableau nécessite de déplacer la moitié de votre liste (tout ce qui se trouve après) vers l'arrière d'un élément. Si chaque élément est un pointeur 64 bits vers un objet (8 octets), cela signifie 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 to RAM assez rapidement.

Par rapport au bouclage sur une liste chaînée pour chaque accès, ce qui nécessite des comparaisons et des branchements et d'autres activités hostiles au processeur, la copie RAM est rapide.

Vous devriez vraiment essayer de comparer les différentes opérations de manière indépendante et voir leur efficacité avec diverses implémentations de liste. Partagez vos résultats ici si vous le faites!

6
dkamins