De Java-engine van een van onze applicaties bevat een gefacetteerde zoekmachine die zijn data in-memory houdt om snel resultaten te kunnen leveren. De indexen van die zoekmachine bestaat uit een aantal Maps en Sets gevuld met Integers. Dat blijkt echter niet zo’n heel efficiënte manier te zijn als je kijkt naar het geheugengebruik.
Om te testen hoe veel geheugen een Set met Integers gebruikt heb ik deze testcode die een Set met 100.000 integers samenstelt. Door de hoeveelheid gebruikt geheugen daarvoor en daarna te vergelijken valt te meten hoe veel geheugen die Set kost:
package memtest; import java.util.HashSet; import java.util.Set; public class MemTest { public static long getUsedMem() { Runtime runtime = Runtime.getRuntime(); return runtime.totalMemory() - runtime.freeMemory(); } public static void main(String[] args) { long initialMem = getUsedMem(); Set set = new HashSet(); for (int i = 1; i <= 100000; i++) { set.add(i); } long finalMem = getUsedMem(); System.out.println("Extra mem: " + (finalMem - initialMem)); } }
Dit leverde bij mij na een aantal keer draaien de volgende output:
Extra mem: 6655960 Extra mem: 6655960 Extra mem: 6655944 Extra mem: 6727368 Extra mem: 6742968 Extra mem: 6658232
Een Set met 100.000 Integers kost in dit geval dus ongeveer 6MB oftewel gemiddeld 66 bytes per stuk. Hoe komt dat? Een integer is toch maar 4 bytes?
Het korte antwoord is dat als je een Set vult met integers, deze automatisch worden ‘geboxed’ naar Integer objecten en dat een object meer geheugen kost dan een ‘primitive’ integer. Bij een geringe hoeveelheid items in zo’n Set zul je dit niet snel merken maar in ons geval hebben we tientallen sets met tienduizenden integers. Dan loopt het geheugengebruik al snel op.
Een oplossing hiervoor is geen gebruik te maken van een Set (die kan namelijk alleen met objecten overweg) maar bijvoorbeeld gebruik te maken van Trove. Die biedt een keur aan Collections klassen die onder water gebruik maken van primitieve variabelen. Dat is niet alleen sneller maar kost ook nog eens minder geheugen.
Om dit te testen passen we bovenstaande code aan naar het volgende:
package memtest; import gnu.trove.set.TIntSet; import gnu.trove.set.hash.TIntHashSet; public class MemTestTrove { public static long getUsedMem() { Runtime runtime = Runtime.getRuntime(); return runtime.totalMemory() - runtime.freeMemory(); } public static void main(String[] args) { long initialMem = getUsedMem(); TIntSet set = new TIntHashSet(); for (int i = 1; i <= 100000; i++) { set.add(i); } long finalMem = getUsedMem(); System.out.println("Extra mem: " + (finalMem - initialMem)); } }
Na deze code een aantal keer te draaien blijkt er nog minder dan een derde van de hoeveelheid geheugen nodig is en ook verrassend constant is:
Extra mem: 2136208 Extra mem: 2136208 Extra mem: 2136208 Extra mem: 2136208 Extra mem: 2136208 Extra mem: 2136208
Dat scheelt! Code die sneller draait en minder geheugen kost. Maar ja; dat levert wel een extra dependency op. En een ‘rare’ interface want een TSet is niet hetzelfde als een Set uit de standaard API. In ons geval konden we de publieke API ‘schoon’ houden door daar gebruik te blijven maken van de standaard Set interface maar onder water worden de Trove classes gebruikt.