Optimisation : Utilisation d'un pool d'objets

J'ai lu récemment une série d'articles1 2 3 4 très intéressants ayant pour sujet l'optimisation des programmes Java. La dernière partie parlait de la réutilisation des objets. Un exemple montrait qu'il était plus performant de vider un ArrayList que de créer une nouvelle instance :

for (int i = 0; i < 10; i++) {
    ArrayList list = new ArrayList();
    // Do some stuff...
}
ArrayList list = new ArrayList();
for (int i = 0; i < 10; i++) {
    // Do some stuff...
    list.clear();
}

Dans notre cas, appeler un constructeur est plus coûteux en temps processeur que de simplement supprimer tous les éléments de notre liste (la raison a été discutée sur comp.lang.java.programmer). Le code de droite est donc plus rapide à l'exécution.

J'ai alors eu l'idée de me lancer dans la création d'un pool d'objets. Dans le cas des ArrayList, le but serait de vider notre instance et de la conserver en attendant de la réutiliser plus tard.

J'ai donc testé le code suivant :
public class ArrayListPool<T> {

    private Queue<ArrayList<T>> pool;
    
    public ArrayListPool() {
        pool = new LinkedList<ArrayList<T>>();
    }
    
    public ArrayList<T> getArrayList() {
        synchronized (pool) {
            if (!pool.isEmpty()) {
                return (pool.poll());
            }
        }
        return (new ArrayList<T>());
    }
    
    public void releaseArrayList(ArrayList<T> list) {
        list.clear();
        synchronized (pool) {
            pool.offer(list);
        }
    }
    
    public void clear() {
        synchronized (pool) {
            pool.clear();
        }
    }
}

Le but est d'appeler getArrayList pour obtenir une instance d'un ArrayList, et de la passer en paramètre à la méthode releaseArrayList pour la remettre dans le pool et la réutiliser au besoin.

Concernant les détails de fonctionnement, je me suis servi d'une LinkedList pour stocker les objets à réutiliser. Cette implémentation de Queue est basée sur une liste chaînée, ainsi les insertion et les suppressions d'éléments dans le pool sont très rapides.

Enfin, l'utilisation de ce pool. À gauche, le code de base, qui servira de benchmark par la suite. À droite, le code modifié pour utiliser le pool.
ThreadGroup group2 = new ThreadGroup("Group 2");
for (int i = 0; i < threadCount; i++) {
    Thread t = new Thread(group2, "Thread " + i) {
        @Override
        public void run() {
            for (int i = 0; i < listCount; i++) {
                ArrayList<Object> list = new ArrayList<Object>();
                for (int j = 0; j < objectCount; j++) {
                    list.add(new Object());
                }
            }
        }
    };
    t.start();
}
final ArrayListPool<Object> pool = new ArrayListPool<Object>();
ThreadGroup group1 = new ThreadGroup("Group 1");
for (int i = 0; i < threadCount; i++) {
    Thread t = new Thread(group1, "Thread " + i) {
        @Override
        public void run() {
            for (int i = 0; i < listCount; i++) {
                ArrayList<Object> list = pool.getArrayList();
                for (int j = 0; j < objectCount; j++) {
                    list.add(new Object());
                }
                pool.releaseArrayList(list);
            }
        }
    };
    t.start();
}

Après exécution de ce code en tant que benchmark, voici les résultats :

threadCount listCount objectCount Temps avec pool (ms) Temps sans pool (ms)
100 100 100 65 82
10 100000 100 1961 2051
100 100000 100 17484 24950


Le temps d'exécution est légèrement réduit. En contrepartie, après analyse de l'application de test grâce au Profiler de NetBeans, la consommation mémoire est d'environ 2 Mo supplémentaires pour 100 objets en attente dans le pool. Cette solution est donc intéressante à mettre en place pour des objets dont l'instanciation est très fréquente. Dans le cas où les instanciations se font plus rares, le gain en temps CPU est négligeable, et la consommation mémoire est plus étalée dans le temps.

Permalink  |  Commentaires (2)


Comments:

Bonjour,

C'est une approche qui n'a plus vraiment de sens avec les jvm modernes.
Dans le micro-benchmark que tu présentes il me semble que la gestion des erreurs est omise par exemple, cette technique a donc un désavantage indéniable: la gestion de la mémoire est à réaliser par le développeur.
Autre problème avec cette approche: le code utilisant ton pool va aller de moins en moins vite plus tu auras de processor.
Pour plus d'infos sur cette problématique je vous conseilles 2 références:

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
http://blogs.azulsystems.com/cliff/

bonne journée,
Florent

Posted by opensourcereader on juin 01, 2008 at 12:42 PM CEST #

Bonjour Florent,

Cette approche n'a pas pour but d'être mise en application. La raison principale est qu'une telle demande d'objets n'est rencontrée que durant des benchmarks.

Le but principal était de mettre en avant le concept de réutilisation des objets, mais je ne pense pas que ce concept soit réellement réutilisable, sauf dans des cas très particuliers.

Cependant, les désavantages que tu présentes sont très intéressants, et méritent d'être considérés. J'essayerais de relancer le même benchmark sur une machine à plusieurs processeurs si j'arrive à en trouver une.

Bonne journée,
Vivien

Posted by viv on juin 01, 2008 at 02:10 PM CEST #

Post a Comment:
  • HTML Syntax: Allowed