39 votes

Implementer la compression en mémoire pour les objets en Java

Nous avons ce cas d'utilisation où nous aimerions compresser et stocker des objets (en mémoire) et les décompresser au besoin.

Les données que nous voulons compresser sont assez variées, allant de vecteurs de flottants à des chaînes de caractères en passant par des dates.

Est-ce que quelqu'un pourrait suggérer une bonne technique de compression pour cela ?

Nous recherchons la facilité de compression et la vitesse de décompression comme facteurs les plus importants.

Merci.

59voto

WhiteFang34 Points 28652

Si vous souhaitez compresser les instances de MyObject, vous pourriez les faire implémenter Serializable puis diffuser les objets dans un tableau d'octets compressé, comme ceci :

ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
ObjectOutputStream objectOut = new ObjectOutputStream(gzipOut);
objectOut.writeObject(myObj1);
objectOut.writeObject(myObj2);
objectOut.close();
byte[] bytes = baos.toByteArray();

Ensuite, pour décompresser votre byte[] en objets :

ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
GZIPInputStream gzipIn = new GZIPInputStream(bais);
ObjectInputStream objectIn = new ObjectInputStream(gzipIn);
MyObject myObj1 = (MyObject) objectIn.readObject();
MyObject myObj2 = (MyObject) objectIn.readObject();
objectIn.close();

11voto

Peter Lawrey Points 229686

Tout comme les réponses précédentes, je vous suggère d'utiliser DeflatorOutputStream et InflatorInputStream car ils sont plus simples / plus rapides / plus petits que les alternatives. La raison pour laquelle c'est plus petit, c'est qu'il se contente de compresser alors que les alternatives ajoutent des extensions de format de fichier comme des contrôles CRC et des en-têtes.

Si la taille est importante, vous pourriez apprécier une sérialisation simple de votre part. Cela est dû au fait que ObjectOutputStream a un surcoût significatif, rendant les petits objets beaucoup plus grands. (Cela s'améliore pour les grands objets surtout lorsqu'ils sont compressés)

Par exemple, un Integer prend 81 octets, et la compression n'aidera pas beaucoup pour un si petit nombre d'octets. Il est possible de réduire considérablement cela.

8voto

Jonas Kongslund Points 2262

Une proposition pourrait être d'utiliser une combinaison des flux suivants:

5voto

Martin Kersten Points 645

La compression des objets sérialisés en Java n'est généralement pas très bonne...

Tout d'abord, il faut comprendre qu'un objet Java contient beaucoup d'informations supplémentaires inutiles. Si vous avez des millions d'objets, vous avez cet surcoût des millions de fois.

Par exemple, prenons une liste doublement chaînée. Chaque élément a un pointeur précédent et suivant + vous stockez une valeur longue (horodatage) + un octet pour le type d'interaction et deux entiers pour les identifiants d'utilisateur. Comme nous utilisons la compression de pointeurs, nous sommes à 6 octets * 2 + 8 + 4 * 2 = 28 octets. Java ajoute 8 octets + 12 octets pour le padding. Cela fait 48 octets par élément.

Créons maintenant 10 millions de listes avec 20 éléments chacune (séries temporelles d'événements de clic des utilisateurs des trois dernières années (nous voulons trouver des motifs)).

Nous avons donc 200 millions * 48 octets d'éléments = 10 Go de mémoire (ok ce n'est pas énorme).

Ok, en plus la collecte des ordures nous étouffe et les surcoûts à l'intérieur de la JDK s'envolent, nous finissons avec 10 Go de mémoire.

Maintenant, utilisons notre propre stockage de mémoire / d'objets. Nous le stockons sous forme de tableau de données en colonnes où chaque objet est en fait une seule ligne. Ainsi, nous avons 200 millions de lignes dans une collection d'horodatage, précédent, suivant, userIdA et userIdB.

Précédent et suivant pointent désormais vers des identifiants de ligne et deviennent 4 octets (ou 5 octets si nous dépassons 4 milliards d'entrées (peu probable)).

Nous avons donc 8 + 4 + 4 + 4 + 4 => 24 * 200 millions = 4,8 Go + aucun problème de GC.

Comme la colonne d'horodatage stocke les horodatages de manière min-max et que nos horodatages se situent tous dans les trois dernières années, nous n'avons besoin que de 5 octets pour stocker chacun des horodatages. Comme les pointeurs sont maintenant stockés de manière relative (+ et -) et que les séries de clics sont temporellement étroitement liées, nous avons besoin en moyenne de 2 octets pour le précédent et le suivant et pour les identifiants d'utilisateur, nous utilisons un dictionnaire car les séries de clics concernent environ 500 000 utilisateurs, nous avons seulement besoin de trois octets chacun.

Ainsi, nous avons maintenant 5 + 2 + 2 + 3 + 3 => 15 * 200 millions => 3 Go + Dictionnaire de 4 * 500k * 4 = 8 Mo = 3 Go + 8 Mo. Ça change de 10 Go, non ?

Mais nous n'avons pas fini. Comme nous n'avons plus d'objets mais des lignes et des données, nous stockons chaque série sous forme de ligne de tableau et utilisons des colonnes spéciales étant des collections de tableaux qui stockent en réalité 5 valeurs et un pointeur vers les cinq valeurs suivantes + un pointeur précédent.

Nous avons donc 10 millions de listes avec 20 entrées chacune (puisque nous avons un surcoût), nous avons par liste 20 * (5 + 3 + 3) + 4 * 6 (ajoutons un surcoût d'éléments partiellement remplis) => 20 * 11 + 5 * 6 => 250 * 10 millions => 2,5 Go + nous pouvons accéder plus rapidement aux tableaux qu'en parcourant les éléments.

Mais ce n'est pas fini... les horodatages sont maintenant stockés de manière relative ne nécessitant que 3 octets par entrée + 5 à la première entrée. -> nous économisons beaucoup plus 20 * 9 + 2 + 5 * 6 => 212 * 10 millions => 2,12 Go. Et maintenant, en stockant le tout en mémoire en utilisant gzip, nous obtenons 1 Go car nous pouvons stocker tout linéairement en premier en stockant la longueur du tableau, tous les horodatages, tous les identifiants d'utilisateur ce qui rend très probable qu'il y ait des motifs dans les bits à compresser. Comme nous utilisons un dictionnaire, nous le trions selon la probabilité de chaque identifiant d'utilisateur de faire partie d'une série.

Et comme tout est un tableau, vous pouvez désérialiser tout à presque vitesse de lecture donc 1 Go sur un SSD moderne coûte 2 secondes pour charger. Essayez cela avec la sérialisation / désérialisation et vous pourrez entendre l'administrateur pleurer.

Donc avant de compresser des données sérialisées, stockez-les dans des tableaux, vérifiez chaque colonne / propriété si elle peut être compressée logiquement. Et enfin, amusez-vous avec cela.

Et rappelez-vous qu'1 To (ECC) coûte 10k aujourd'hui. Ce n'est rien. Et 1 To SSD coûte 340 euros. Ne perdez donc pas votre temps sur ce problème à moins que vous n'ayez vraiment besoin de le faire.

2voto

gabuzo Points 3181

Il existe différents algorithmes de compression implémentés dans le JDK. Consultez le [java.util.zip](http://download.oracle.com/javase/6/docs/api/java/util/zip/package-summary.html) pour connaître tous les algorithmes implémentés. Cependant, il n'est peut-être pas judicieux de compresser toutes vos données. Par exemple, un tableau vide sérialisé peut faire plusieurs dizaines d'octets car le nom de la classe sous-jacente figure dans le flux de données sérialisées. De plus, la plupart des algorithmes de compression sont conçus pour supprimer la redondance des blocs de données volumineux. Sur de petits à moyens objets Java, vous aurez probablement très peu, voire aucun gain du tout.

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