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
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
J'ai donc testé le code suivant :
Le but est d'appeler
Concernant les détails de fonctionnement, je me suis servi d'une
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.
Après exécution de ce code en tant que benchmark, voici les résultats :
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.
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.