Cookies op Tweakers

Tweakers is onderdeel van DPG Media en maakt gebruik van cookies, JavaScript en vergelijkbare technologie om je onder andere een optimale gebruikerservaring te bieden. Ook kan Tweakers hierdoor het gedrag van bezoekers vastleggen en analyseren. Door gebruik te maken van deze website, of door op 'Cookies accepteren' te klikken, geef je toestemming voor het gebruik van cookies. Wil je meer informatie over cookies en hoe ze worden gebruikt? Bekijk dan ons cookiebeleid.

Meer informatie

Door Arjen van der Meijden

Lead Developer

Praktisch geheugenbeheer in Java bij Tweakers.net

Tweakers.net en Java

Tweakers.net heeft in de afgelopen maanden hard gewerkt aan het Tweakers 7.0-project. Een belangrijke doelstelling van dit project is het verbeteren van de specificatiefilters van de Pricewatch, die we ook op lijsten met nieuwsberichten, reviews, video's, enzovoort willen toepassen. Ook die content kan straks dus door de gebruiker gefilterd worden.

Onder meer om deze doelstelling te realiseren kozen we voor een uitbreiding van onze bestaande Pricewatch Engine. Een belangrijke taak van die engine is het snel aanleveren van gefilterde en gesorteerde informatie, inclusief statistieken, nodig om te bepalen welke keuzeopties bij de Pricewatch-lijsten worden aangeboden.

Toen we in 2009 de eerste versie van de engine ontwikkelden, ging het al om meer dan 150.000 producten, elk met een lijst specificaties. Het was al meteen duidelijk dat een php-applicatie niet snel genoeg was om de gegevens praktisch te verwerken. Daarom kozen we destijds voor een Java-applicatie. De door de gebruiker gekozen filters worden in php-code vertaald en doorgestuurd naar de Java-applicatie, die vervolgens een brok hapklare informatie aan de php-code teruggeeft.

Voor optimale prestaties laadt de engine alle gegevens in het ram en ook de bijbehorende zoekdatabase bevindt zich in het werkgeheugen. Alle gegevens samen nemen ruim 600MB in beslag.

Voor het Tweakers 7.0-project willen we echter naast de producten ook de nieuwsartikelen, reviews en andere content toevoegen. In onze MySQL-database nemen die gegevens al meer dan twee gigabyte in beslag en daarbij komt nog allerlei meta-informatie, zoals aanvullende relaties en zoektabellen. De hoeveelheid data neemt dus fors toe.

In dit artikel beschrijven we hoe we erin zijn geslaagd om het geheugengebruik binnen de perken te houden. De hier genoemde tips en trucs zijn primair gericht op Java, maar een groot deel van de ideeën is ook in andere talen toe te passen. Objecten worden immers in elke objectgeoriënteerde taal volgens vergelijkbare principes opgezet en ook datastructuren hebben doorgaans veel overeenkomsten.

Waarom geheugen besparen?

Tegenwoordig kun je al voor relatief weinig geld een 'simpele' dual-socketserver van 144GB werkgeheugen voorzien. Het loont dus niet direct om het geheugengebruik te minimaliseren. Een voor de hand liggende reden om het toch te doen, is dat het praktisch is om applicaties in een lokale ide met reële data te kunnen testen. Onze workstations beschikken over 8GB ram, waardoor een heap size van rond de 5GB wel ongeveer het maximum is. Met de ide, een geheugenprofiler, een mailclient, een browser en de applicatie zelf erbij is er af en toe 7,98GB geheugen in gebruik en dan wordt zo'n systeem behoorlijk traag.

Ook de prestaties zijn een reden voor efficiënt geheugengebruik. Kleinere objecten en compactere datastructuren nemen overal minder ruimte in, dus ook in de cpu-caches en -registers. Daarnaast is er minder bandbreedte nodig om de data van en naar de cpu's te verplaatsen. Minder geheugen gebruiken betekent verder dat de garbage collector minder hard hoeft te werken, zodat er minder en/of kortere collection pauses in de applicatie optreden en er meer cpu-tijd voor de applicatie overblijft.

Eenmaal geoptimaliseerde gegevens kunnen gedurende de hele levensduur van je applicatie een efficiënter gebruik opleveren. Wij konden bijvoorbeeld een aantal strings vervangen door byte-arrays, die niet alleen kleiner waren, maar ook efficiënter konden worden verwerkt. Uiteraard hangt de prestatiewinst sterk af van de gekozen optimalisatie en de omstandigheden.

Objecten in Java

Gegevens worden in Java met objecten beschreven. In deze objecten kunnen primitieven - zoals getallen, booleans en karakters - worden opgeslagen, ze kunnen naar andere objecten verwijzen en ze kunnen een combinatie van primitieven en verwijzingen bevatten.

Al die zaken kosten uiteraard geheugen. De precieze hoeveelheid hangt af van de gebruikte Java Virtual Machine. Er is bijvoorbeeld geen garantie dat de IBM JDK of de JRockit VM evenveel geheugen voor een object gebruikt als de Oracle JDK, hoewel er in de praktijk waarschijnlijk veel overeenkomsten zijn.

Met de hotspot-compiler van Oracle wordt voor elk object een header van twee referenties gebruikt, waaraan vervolgens de gewenste data wordt toegevoegd. Een referentie beslaat 4 bytes bij 32bits-systemen en 8 bytes bij een 64bits-VM. Met CompressedOops kunnen referenties bij een 64bits-VM in slechts 4 bytes worden gecodeerd. Aangezien CompressedOops sinds Oracles Java 7 standaard is ingeschakeld, gaan we bij de onderstaande berekeningen uit van referenties van 4 bytes.

De exacte geheugenlay-out van objecten in Oracles JVM is helaas niet eenvoudig te achterhalen en wordt ook niet gespecificeerd in de Java Language Specification. Daarin is alleen vastgelegd welke waarden moeten kunnen worden opgeslagen in een byte, short, int, float, long, double of char. Een boolean kost in de Oracle JVM 1 byte, maar in een andere JVM zouden bijvoorbeeld 8 booleans in 1 byte gegroepeerd kunnen worden.

Primitieve
BitsBytes
byte 8 1
short 16 2
int en float 32 4
long en double 64 8
char 16 2
boolean 1 - 8 1
referentie 32 of 64 4 of 8

De Oracle JVM slaat objecten in blokken van 8 bytes op, omdat de toegang en het beheer van de data zo efficiënter zijn dan wanneer met losse bytes wordt gewerkt. Omdat 8 bytes net genoeg is om de twee verplichte referenties in op te slaan, beslaat een object met een willekeurig veld dus altijd minimaal 16 bytes. Als objecten kleiner zijn dan een veelvoud van 8 bytes, blijft er altijd wat loze ruimte over, die 'padding' wordt genoemd.

Objectgroottes

Een bekend object in Java is de string, waarin een tekenreeks kan worden opgeslagen. Een string kan zijn interne array met chars delen met een andere string en bevat daarom een eigen beginpositie en lengte. Het char-array is een apart object, dat behalve de karakters ook een lengteparameter bevat.

Velden in een string
TypeLengte
objectheader 2x referentie 2x4 bytes
hash int 4 bytes
offset int 4 bytes
count int 4 bytes
value (char-array) referentie 4 bytes
Subtotaal 24 bytes
Velden in de char-array
objectheader 2x referentie 2x4 bytes
length int 4 bytes
char[0], char[1], ..., char[n] char n*2 bytes
padding van 2, 4 of 6 bytes
Subtotaal bij lege tekst 12 bytes
+ 4 bytes padding
Totaal voor lege tekst 40 bytes

Een lege string kost dus al 24+16, oftewel 40 bytes, overigens net als strings van 1 en 2 tekens. Uiteraard is de impact van de overhead kleiner bij langere strings.

Ook van andere veelgebruikte objecten kan het geen kwaad om te weten hoeveel data ze gebruiken. Een arraylist heeft bijvoorbeeld een object-array en een size-integer aan boord. Een lege arraylist kost dus in theorie 2*4+4+4=16 bytes plus 16 bytes voor de lege array, wat het totaal op 32 bytes brengt. Er zit echter een addertje onder het gras; een arraylist wordt standaard aangemaakt met een object-array van 10 elementen, die nog eens 2*4+4+10*4 bytes (+4 bytes padding) in beslag neemt; dat brengt het totaal op 72 bytes.

Voor hashmaps geldt een vergelijkbaar verhaal. Een hashmap bevat een array van entries, een size, een threshold, een modcount en een loadfactor. Daarnaast worden nog referenties naar de entryset, de keyset en de values bewaard. Dat kost dus 2*4+4*4+4+3*4=40 bytes. Daar komt nog een entry-array bij, die standaard 16 elementen groot is en dus 2*4+4+16*4=76 bytes omvat. Met padding betekent dat een grootte van 80 bytes. Een blanco hashmap beslaat dus 120 bytes en dan zit er nog geen data in.

Als je vooraf een grootte voor een hashmap opgeeft, wordt de capaciteit afgerond naar het eerstvolgende veelvoud van 2. De entry-objecten kosten weer 2*4 bytes voor de header en hebben daarnaast referenties naar de key, value en een eventuele next-entry. Daarnaast bevatten ze nog een kopie van de hashcode, wat een totaal van 24 bytes oplevert. Een met 5 elementen gevulde hashmap kost daardoor 40 + 12 + (4*8) + (24*5) = (+4 bytes padding) 208 bytes.

Elementen
Object[]
ArrayListLinkedListHashSet
10 56 bytes 72 bytes 264 bytes
356 bytes
100 416 bytes 432 bytes 2424 bytes 2980 bytes
1000 4016 bytes 4032 bytes 24024 bytes
28164 bytes

Met de voorgaande voorbeelden is waarschijnlijk wel duidelijk dat objecten in Java al gauw een stuk meer geheugen gebruiken dan je aan de hand van de opgeslagen data zou verwachten. Daarmee is de geheugenbehoefte van Java echter niet uitzonderlijk. Het objectmodel van PHP is bijvoorbeeld nog een stuk erger. Door de dynamische types van velden nemen 5 objecten met ieder 2 integers in een array maar liefst 4640 bytes in beslag.

In Java vergt dezelfde data de hierboven genoemde 208 bytes voor een hashmap, met daarnaast 5*16=80 bytes voor de integer-keys en 5*16=80 bytes voor de genoemde objecten. Dat brengt het totaal op 368 bytes en dat is al een stuk schappelijker.

Geheugen besparen: de basis

Om het geheugengebruik in Java te beperken zijn er twee mogelijkheden: de objecten moeten kleiner worden of het aantal objecten moet worden verlaagd. Alle trucs om geheugen te besparen zijn uiteindelijk terug te voeren op deze twee uitgangspunten. Soms kan het handiger zijn om het aantal objecten te vergroten, zolang dat maar betekent dat de objecten per stuk kleiner worden. Omgekeerd kan het ook nuttig zijn om grotere objecten te gebruiken, als het aantal objecten dan maar flink omlaag kan.

Verklein het aantal objecten

Elk object heeft minimaal 8 bytes nodig en meestal meer. Bovendien wordt minstens één referentie bijgehouden naar elk object dat in het geheugen actief blijft en ook dat kost geheugen. Door het aantal objecten te verkleinen wordt dus doorgaans geheugen bespaard.

Als een verzameling van 100.000 strings van gemiddeld 10 karakters, die dus 56 bytes per stuk kosten, kan worden teruggebracht tot 50.000 stuks, levert dat een besparing op van ruim 2,5MB. Soms kun je ook besparen door een groepje vaste objecten bij te houden, zodat je niet steeds nieuwe hoeft aan te maken, maar bestaande objecten kunt gebruiken.

Verklein de objecten zelf

Om geheugen te besparen ligt het voor de hand je objecten kleiner te maken. Door de afronding naar veelvouden van 8 bytes is dat echter niet altijd zinvol. Als een object 2 int-velden bevat en je kunt er 1 wegbezuinigen, dan levert dat je geen bit winst op. In beide gevallen is het object 16 bytes groot.

Als je echter zeker weet dat die 100.000 strings uit het eerdere voorbeeld uitsluitend tekens uit de ISO-8859-1-karakterset bevatten, dan weet je ook dat elk karakter maar 1 byte nodig heeft. De benodigde objecten vereisen dan nog maar een object van 2*4+4 (+4 bytes padding) = 16 bytes en een byte-array van 2*4+4+10 (+2 bytes padding) = 24 bytes, dus 40 bytes per stuk. Als je alleen de byte-arrays opslaat, zijn er zelfs maar 24 in plaats van de oorspronkelijke 56 bytes nodig. Dat levert in dit voorbeeld dus 1,5 of zelfs 3MB winst op.

Dat zijn nog geen enorme aantallen, maar het spreekt voor zich dat de winst bij grotere objecten en grotere aantallen objecten behoorlijk kan toenemen. Als de strings gemiddeld 200 karakters lang zijn, zouden we met onze byte-array-trucjes dus 20 tot 21MB kunnen besparen.

Tools om geheugen te analyseren

Er zijn diverse tools om het geheugengebruik van een draaiende applicatie te bekijken. Dit doen ze doorgaans door een heap dump te maken en die vervolgens te analyseren. Naast commerciële profilers als Yourkit, JProbe en JProfiler zijn er ook gratis tools; Oracle levert bij zijn JDK de tooltjes jmap en jhat, en Eclipse Memory Analyzer is in Eclipse te integreren. Het laatste programma kan vrij uitgebreide analyses van de heap dump maken, maar heeft momenteel nog geen ondersteuning voor CompressedOops, zodat de cijfers bij de objecten niet helemaal kloppen. Dat maakt echter niet veel uit bij het vinden van dubbele objecten, het volgen van referenties, het tellen van aantallen objecten, enzovoort.

Minder objecten gebruiken

De beste manier om geheugen te besparen is minder objecten te gebruiken. Vooral immutable objecten, zoals strings, zijn ideaal om te delen tussen verschillende objecten, waarbij maar één object nodig is voor een unieke waarde.

java.lang.String

Zoals we al eerder meldden, is de string in Java immutable. Dat houdt in dat de inhoud van een eenmaal aangemaakt object nooit meer wordt aangepast. Als er een bewerking op plaatsvindt, wordt er een nieuw object met een nieuwe inhoud gegenereerd; elke verwijzing naar de oude versie blijft echter ook geldig en geeft de oude, onveranderde waarde terug. Voor dezelfde tekst kan dus ook daadwerkelijk dezelfde string gebruikt worden, ongeacht of dat met diverse objecten, verschillende typen objecten of diverse threads gebeurt.

Aangezien de string een van de meest gebruikte data-objecten in Java is, hebben profilers vaak speciale routines om string-objecten naar inhoud te sorteren en om te zoeken naar dubbele of lege strings. Overigens kan Eclipse Memory Analyzer dat ook met andere objecten en zelfs met arrays.

Als je net als Tweakers.net met databases werkt, is de kans groot dat je voor elke tekst in elke record een nieuw string-object krijgt, ook als de tekst leeg is. In ons geval hebben we een slordige miljoen lege strings in de database: niet ingevulde url-velden, lege titels of beschrijvingen, enzovoort. Met 40 bytes per stuk is een miljoen lege strings goed voor zowat 40MB verspild geheugen.

Hiervoor zijn allerlei eenvoudige oplossingen te verzinnen. Je kunt bijvoorbeeld een utility-klasse voorzien van een methode die nagaat of een string leeg is en dan een referentie naar een vaste, lege standaardstring teruggeeft. Dat kan met zoiets:

class StringUtil {
  private static final String EMPTY_STRING = "";
  /**
   * Prevent copies of the empty string "".
   * @param input A potential copy of the empty string.
   * @return The reference to the singleton empty String
   *         or the original string if it was non-empty.
   */
  public static String getUniqueString(String input) {
    return (input != null && input.isEmpty() ? EMPTY_STRING : input);
  }
}

Als er veel dubbele strings zijn, zijn er twee manieren om het aantal objecten te reduceren. Bij een kleine, vaste verzameling korte teksten is het al gauw interessant om met een enum te werken. Strings worden dan in feite weggegooid zodra de bijbehorende enum in de bijbehorende vaste lijst is gevonden.

Aangezien een enum ook een klasse is, kun je er eventueel extra functionaliteit in stoppen. Je kunt bijvoorbeeld een enum voor http-statuscodes definiëren die naast de tekst ook de statuscode zelf bevat. Bovendien bestaan voor enums ook de speciale EnumSet- en EnumMap-klassen, waarmee nog extra werk en geheugen bespaard kunnen worden.

De tweede manier is om te werken met een StringInterner of canonicalizer. Hiermee worden twee gelijkwaardige strings door hetzelfde, unieke object gerepresenteerd.

Java biedt zelf de String.intern()-methode aan, maar deze kan beter niet voor dit doel gebruikt worden. De string wordt dan in de permgen opgeslagen en die heeft doorgaans een zeer beperkte grootte. In plaats daarvan kan simpelweg een HashMap <String, String> gebruikt worden. Ook Lucenes StringInterner (als je toch al Lucene gebruikt) en de Interner uit Googles Guava-bibliotheek zijn interessant.

class TrivialStringInterner {
  private final HashMap<String, String> stringCache = new HashMap<>();
  /**
   * Return the single unique instance for the given String-content.
* I.e. guarantee that this is true:
* (output1.equals(output2)) == (output1 == output2)
* @param input A potentially duplicate String. * @return The guaranteed singleton String (within this interner). */ public String intern(String input) { String output = stringCache.get(input); if(output == null) { stringCache.put(input); output = input; } return output; } }

Bij Tweakers.net wordt dit onder andere gebruikt voor de extensies van afbeeldingen. Dat zijn er maar heel weinig, in ons geval maar drie, maar we weten natuurlijk niet op voorhand of er niet toch nog eentje bijkomt. Met bijna 500.000 afbeeldingen in de database is het in elk geval al snel lonend om die te internen. Andere voorbeelden zijn plaatsnamen en landcodes.

Overigens is het internen of canonical maken van objecten ook mogelijk bij andere objecttypen. Bij elk type dat hergebruikt kan worden, kun je besparingen halen. Zo ontdekten we dat er voor onze 450.000 V&A-advertenties maar 60.000 unieke adressen werden gebruikt. Door adressen van de advertenties los te koppelen en ze uniek op te slaan, bespaarden we tientallen megabytes.

Let er wel op dat de datastructuur die je nodig hebt om de unieke objecten te vinden zelf ook geheugen kost. De hashmap voor de V&A-adressen neemt ruim 1,5MB in beslag; daar zouden nog aardig wat adresobjecten in gepast hebben.

Verder gebruikt de Oracle-JVM al unieke strings voor dezelfde tekst. Als in een stuk code tien keer dezelfde tekst staat, dan wordt binnen de applicatie ook steeds dezelfde string gebruikt. De Oracle-JVM roept namelijk voor elke hardcoded string in het programma zelf al String.intern() aan.

Primitieven gebruiken

Andere objecten die in de praktijk veel data opslokken, zijn de wrappers rond de primitieven. Ieder van deze objecten kost 16 bytes en bovendien 4 bytes aan referentie vanuit de plek waar ze gebruikt worden. Een object met acht primitieven van het byte-type neemt zo 168 echte bytes in beslag.

Mocht je toch primitieven in objecten opslaan, gebruik dan in geen geval de constructor, zoals new Boolean(boolVal), maar de factory-methode van Java zelf, zoals Boolean.valueOf(boolVal). Deze maakt handig gebruik van een aantal vaste caches om vaak gebruikte objecten te delen, zoals de booleans false en true, en de integerwaarden van -127 tot 128. Uiteraard kun je zelf ook objecten cachen als je ziet dat je ergens maar een beperkt aantal verschillende waarden hoeft te gebruiken.

Overigens kun je in uitzonderlijke situaties met een Long- of Double-wrapperobject juist geheugen besparen. De referenties ernaar kosten tenslotte maar 4 bytes, terwijl de primitieven zelf 8 bytes ruimte innemen.

Trove-collecties voor primitieven

Collecties in Java zijn erg handig en ze werken goed en snel. Ze hebben echter een nadeel; je kunt er geen primitieven in opslaan. Het enige alternatief is om met arrays van primitieven te werken en dat is niet erg praktisch. Als je een map van int naar int wil bijhouden, moet je Map<Integer, Integer> gebruiken. Met 100.000 key-valueparen komt een hashmap met de bijbehorende entry-objecten dan al op minstens 2,8MB en is er nog 3MB voor de integer-objecten nodig.

Gelukkig zijn er diverse bibliotheken te vinden die het mogelijk maken om direct met primitieven te werken. De compleetste die we zijn tegengekomen is Trove. Naast collecties voor primitieven biedt deze library ook hashtables die met 'open addressing' werken, waardoor er geen entry-objecten nodig zijn.

De TIntIntHashMap bevat een array met keys, een array met values, een byte-array met de status van velden, 8 floats en ints, en 2 booleans. De 3 arrays zijn wel standaard twee keer zo groot als het aantal opgeslagen objecten, vanwege de implicaties voor de prestaties die aan een kleinere array vastzitten. Voor onze 100.000 key-value paren is uiteindelijk 56 + (12+2*4*100.000) + (12+2*4*100.000) + (12+2*100.000) = 1,7MB nodig en omdat er geen 200.000 integer-objecten meer nodig zijn, ben je met die 1,7MB ook meteen klaar.

Naast de collecties voor primitieven kent Trove overigens ook een compacte THashMap en THashSet, waarbij vooral de laatste aanzienlijk kleiner is dan de Java-tegenhanger. De HashSet van Java is onder de motorkap namelijk gewoon een HashMap, die dus nog meer overhead heeft dan een 'echte' HashMap.

In onze code maken we tegenwoordig veelvuldig gebruik van de diverse primitieve-naar-object-arrays, zoals TByteObjectHashMap en TIntObjectHashMap, en we zijn ook fans geworden van de TIntArrayList en THashSet. Aangezien we deze vrij geleidelijk zijn gaan gebruiken, is het lastig te zeggen hoeveel geheugen we hiermee uiteindelijk hebben bespaard, maar met meer dan een miljoen van deze collecties in het geheugen bedraagt de totale besparing zeker enkele honderden megabytes.

Objecten kleiner maken

Naast het beperken van het aantal objecten kun je de objecten uiteraard ook kleiner maken. In bepaalde situaties kan het ook lonen om ze door speciale, veel kleinere objecten te vervangen.

Speciale Java-collecties gebruiken

Naast de Trove-collecties biedt Java collecties voor twee veel voorkomende situaties: collecties die leeg zijn en collecties die slechts 1 element bevatten. De Collections-klasse kent 6 methodes om zulke collecties compacter te maken.

De emptySet-, emptyList- en emptyMap-methodes hergebruiken hetzelfde object, waardoor ze het totale aantal bestaande objecten reduceren en effectief 0 bytes kosten. De singleton, de singletonList en de singletonMap zijn ook veel compacter dan hun normale tegenhangers en gebruiken per stuk 16 bytes.

In onze code hebben we meer dan 350.000 referenties naar emptyList en emptyMap, zodat we ten opzichte van de standaard-arraylists en -hashmaps respectievelijk 24 en 40MB geheugen besparen. Uiteraard kunnen die lege lijsten niet gevuld worden met nieuwe objecten, maar dat was in die gevallen voor ons toch niet nodig.

De singleton-versies gebruiken we zelfs nog vaker, respectievelijk 900.000, 600.000 en 100.000 maal. In vergelijking met de standaard-hashsets, -arraylists en -hashmaps besparen we zo respectievelijk 124, 32 en 12MB. Omdat we ook al veelvuldig de op maat gesneden THashSets en arraylists gebruiken, zijn de besparingen in de praktijk iets kleiner.

Als je toch meer dan 1 element in een collection wil stoppen, is een arraylist met weinig elementen - wij hanteren een grens van 10 stuks – bijna altijd sneller dan een HashSet of een THashSet. Daarnaast is een arraylist compacter. Als de verschillen in semantiek acceptabel zijn en er een collectie met een snelle contains-methode nodig is, is dat een prima manier om nog wat MB's te besparen.

Als voor een arraylist wordt gekozen, is de trimToSize()-methode ook erg nuttig. Deze verkleint de interne object-array tot precies de goede grootte. Voor alle op arrays gebaseerde collecties - zoals hashmap, hashset en arraylist - is het toch al verstandig om ze direct in de constructor de goede grootte mee te geven. Dat bespaart naast geheugen ook rekentijd, omdat de interne array niet vergroot hoeft te worden.

Strings comprimeren

Zoals gezegd zijn strings verantwoordelijk voor een groot deel van het geheugengebruik van veel applicaties. Het feit dat Java voor elk karakter 2 bytes gebruikt, maakt dat nog wat zichtbaarder. Het is echter niet altijd mogelijk of zinvol om te testen of een bepaalde tekst meermalen wordt gebruikt.

Bij lange teksten, zoals onze nieuwsberichten en reviews, weten we bijvoorbeeld al op voorhand dat de strings uniek zijn. Er valt dan niets te winnen met het internen van de objecten. Toch zijn de teksten die wij in het geheugen willen laden, samen goed voor 1,2GB aan objecten. Hier was dan ook een flinke besparing te realiseren.

Onze teksten zijn vrijwel uitsluitend in de ISO-8859-15-karakterset opgesteld, en ze worden ook zo naar jouw browser verstuurd. Dat betekent dat we de strings kunnen vervangen door een datatype dat 1 in plaats van 2 bytes per karakter gebruikt. Dat levert uiteraard een halvering van het benodigde geheugen op.

Dat vonden we echter nog niet genoeg, dus hebben we uitgezocht of datacompressie zinvol zou zijn. Omdat het vooral om korte teksten gaat en we niet te veel tijd aan het comprimeren en decomprimeren kwijt wilden zijn, was het rendement van compressie niet al te groot, maar we hebben toch de LZF-encoder in gebruik genomen.

LZF is een van de snelste compressie-algoritmes die momenteel beschikbaar zijn. Bovendien is er een snelle, puur in Java geschreven implementatie van beschikbaar, die ook nog eens onder een bruikbare licentie wordt aangeboden. Deze LZF-implementatie heeft voor de compressie van onze 600MB tekst ongeveer 4 seconden nodig. Het decomprimeren is zelfs al in 0,7 seconde gepiept.

LZF maakt overigens niet alle teksten korter. In dat geval bewaren we simpelweg de ongecomprimeerde byte-variant van de originele tekst, maar desondanks kunnen we nog ongeveer de helft op de geheugenbehoefte besparen. Uiteindelijk verzamelde onze StringCompressor de volgende statistieken.

Type string
Aantal
Grootte
Aangeboden strings 821.450 1.263.011.216
Waarvan leeg 190.636

0

Waarvan niet comprimeerbaar 105.356

10.586.802

Waarvan wel comprimeerbaar 525.458 394.556.723
Totaal gebruikt geheugen na verkleining 425.013.432

Door de aangeboden strings te vervangen door objecten met een byte-array en die waar mogelijk te comprimeren, hebben we het geheugengebruik dus met ongeveer 800MB gereduceerd. Dat is een winst van ruim 66 procent.

Voor wie nog meer wil besparen, zijn er nog alternatieven. Bij lange teksten zullen de meeste general-purpose-algoritmen behoorlijke reducties opleveren. Met de korte teksten die wij in de aanbieding hebben, moet echter goed opgelet worden; Huffman tables en dictionaries worden normaliter in de gecomprimeerde tekst opgeslagen en dat kost weer extra ruimte. Een van de weinige algoritmes die hiermee efficiënt overweg kunnen, is FemToZip. We hebben ook geëxperimenteerd met een eigen compressiesysteem dat een externe dictionary en Huffman-table bouwde.

Beide methodes leverden opnieuw een halvering van het geheugengebruik op. Helaas bleken de methodes veel complexer; ze vereisen goed gekozen trainingsdata en veel vooronderzoek. Bovendien waren beide algoritmes aanzienlijk trager dan LZF; FemToZip had ruim 200 seconden nodig voor het comprimeren en het decomprimeren duurde ook nog altijd 15 seconden. In vergelijking met de prestaties van LZF vonden we dat niet acceptabel.

821.450

Objecten vervangen of opdelen

Het juiste type gebruiken

In een aantal gevallen kan data als tekst, maar ook als een specifiek datatype worden opgeslagen. Voorbeelden zijn ip-adressen, datums en vaste keuzelijsten. Het vervangen van vaste keuzes door enums of met behulp van interning is al genoemd. Daar wordt vooral winst geboekt omdat het aantal objecten kan worden gereduceerd.

Bij de keus tussen een string en een ander datatype is het vooral van belang om te onderzoeken welk type compacter is. Ook speelt een rol dat specifieke datatypes vaak handige bewerkingen op de data mogelijk maken.

Een Inet4Address neemt in Java slechts 24 bytes in, terwijl een stringversie van een ip-adres (bij 12 tekens) in totaal 24+40=64bytes kost. In ons geval ging het om 450.000 adressen en dus om een besparing van ruim 17MB. Voor het converteren van strings naar InetAddress-objecten is de InetAddresses-klasse van Guava erg praktisch. Deze voert namelijk geen dns-look-up op een gegeven ip-adres uit, terwijl de vergelijkbare methode van InetAddress dat soms wel doet.

Vergelijkbare winsten zijn te halen door netjes een Integer, Date, Long, Double en zo verder te gebruiken. Let er wel op dat bij ieder van die objecten een hoeveelheid cpu-tijd nodig is om ze van en naar strings te converteren. Als de desbetreffende objecten al als primitieve worden aangeboden, is de keus uiteraard eenvoudiger.

Objecten opdelen

In sommige gevallen bevatten veelgebruikte objecten velden die maar af en toe gevuld zijn. In ons geval werd voor Vraag & Aanbod-advertenties eerst één klasse gebruikt. Voor dienstenadvertenties zijn er echter vijf aanvullende velden, zoals de begin- en eindtijd, die daardoor ook bij productadvertenties werden opgeslagen.

Aangezien we veel meer productadvertenties dan dienstenadvertenties hebben, kostten die extra velden 16 bytes per object. Met onze 450.000 advertenties is dat bijna 7MB. Door de advertentieobjecten te verdelen in ServiceAdvertisements en ProductAdvertisements, konden we die velden in de meeste gevallen weglaten en zo dus weer een paar megabyte besparen.

Een andere manier van opdelen is het verhuizen van velden naar een los object. Bij een advertentie kan opgegeven worden of het product voor een vaste prijs, door middel van een veiling, via ruil of tegen elk aannemelijk bod wordt aangeboden. We schreven daarom een VaPrice-interface met aparte klassen per type betaling; zo hoeven we specifieke velden alleen te alloceren en te vullen als ze ook echt gebruikt worden.

Sterker nog, er zijn bijvoorbeeld ruim 200.000 advertenties met een concrete vraagprijs, maar daarbij worden minder dan 1700 unieke bedragen gebruikt. De meeste bezoekers kiezen uit een vrij klein aantal prijzen; bedragen als 20, 25 of 50 euro komen vaak voor. Bovendien hoefden we maar één object aan de waarde 't.e.a.b.' te wijden, in plaats van 30.000.

Conclusie

Onze totale geheugenbesparing ten opzichte van een 'naïeve' implementatie loopt door de besproken technieken waarschijnlijk richting de 2 gigabyte. Dat is genoeg om ruimschoots binnen de eerder genoemde maximale heap size van 4GB te blijven.

Daarmee is echter niet gezegd dat iedereen maar meteen moet beginnen met dit soort optimalisaties. De meeste genoemde ideeën kosten extra rekentijd bij het aanmaken van objecten, verbloemen complexiteit of doen aannames die strijdig kunnen zijn met defensive coding-principes. Erger nog, ze kunnen bij verkeerd gebruik juist extra geheugen kosten.

Tijdens de ontwikkeling van onze Java-code bleek echter dat de besproken methoden en technieken niet in de standaard-'toolkit' van onze programmeurs zaten. Dat is niet zo vreemd; dergelijke optimalisaties maken doorgaans geen deel uit van een standaard-Java-cursus. Ook in de praktijk is het niet gebruikelijk dat er meer dan een paar duizend objecten in het geheugen worden gehouden.

Het is precies daarom dat we dit artikel hebben gepubliceerd; andere Java-programmeurs hoeven zo het wiel niet opnieuw uit te vinden. Nogmaals, het blind in elke applicatie toepassen van deze technieken is niet de bedoeling. Gebruik altijd een goede geheugenprofiler om uit te zoeken welke optimalisaties werkelijk de moeite waard zijn en probeer ook de nadelen van je optimalisatie te vinden. Dat scheelt je niet alleen veel werk, maar voorkomt ook dat je jezelf met nodeloos complexe code allerlei nieuwe problemen op de hals haalt. Zoals vaak wordt gezegd: "Premature optimization is the root of all evil."

Wat vind je van dit artikel?

Geef je mening in het Geachte Redactie-forum.

Reacties (138)

Wijzig sortering
Leuk artikel, toch stel ik mij hier en daar wat vragen bij de optimalisaties.

Een aantal stukken lijken me zelfs counter productief te werken. Zeker als je weet hoeveel de jvm begint te optimaliseren na een aantal runs.
De stukken over de Strings bijvoorbeeld. Het comprimeren van grote teksten akkoord. Maar de byte arrays?
Java heeft een zeer complex en snel systeem als het over strings gaat, hier is immers al een object pool aanwezig. Het enigste wat je niet mag dien is een new String("") deze zal altijd een verse instantie terug geven. In de andere gevallen krijg je zaken uit de object pool terug :

Assert.assertSame(String.valueOf(""), String.valueOf(""));
Assert.assertSame(String.valueOf("test"), "test");
Assert.assertSame(new String(""), String.valueOf(""));

De eerste twee asserts zullen succesfull zijn,, de laatste zal uiteraard falen.
(assert same vergelijkt memory adresses)

Een aantal stukken van de string optimalisatie zijn dus reeds opgelost door de JVM.
Uiteraard kan je voor grote teksten wel stukken optimaliseren... Dus afhangende van waar bepaalde zaken zijn toegepast kan dit nuttig zijn of juist overhead veroorzaken

Verder lijkt het gebruik van primitive types in principe beter, maar aangezien java een aantal dingen regelt voor jou met pointers enzovoort. Is er een groot verschil tussen het gebruik van een int en een Integer bijvoorbeeld. Een method call met een int zal een pass by value gebruiken, welke opnieuw een int zal aanmaken voor die methode, indien je een Integer doorgeeft zal de orginele versie hebruikt worden, dus naar gelang het gebruik is de een performanter dan het ander, hier laten we zelfs dan nog het autoboxing stuk achterwege in de redenatie.

Verder zie ik heel veel terugkomen over optimalisatie en hashmaps en krijg ik het gevoel dat er wat veel word geleden on het NIH syndroom (not invented here : http://en.wikipedia.org/wiki/Not_invented_here) Voor veel van die zaken zou ik overwegen een cache provider of een in memory database te gebruiken. En dus de optimalisatie door iemand anders te laten doen...

Gezien de resultaten van de profiles zullen jullie er inderdaad wel in geslaagd zijn om wat megs vrij te maken, al verwacht toch een stevige extra inspanning van jullie cpu met de nieuwe code. Ook zal na x aantal tijd na een hoop interne jvm optimalisaties de winst niet zoveel zijn als verwacht.

M'n mening is natuurlijk gebaseerd op het artikel en zonder de code effectief te bekijken blijft het moeilijk om te zien hoe het effectief uitdraait...
Strings die uit een database komen via een jdbc-driver, etc zijn per definitie niet geinterned. En aangezien de string-interner van Java zelf met permanent geheugen werkt en niet op de heap alloceert wil je daar ook geen 3GB aan tekst in gooien...

Overigens gebruiken wij alleen string-interning op plekken waar relatief weinig unieke strings vaak gebruikt worden. Het kost uiteraard meer cpu-tijd om dat te realiseren, maar volgens de huidige JVM-specificaties zou daar nooit automatisch geheugen bespaard worden en ik betwijfel of men bij Oracle wel ooit zo ver wil gaan om alle strings automatisch te internen.
Dat is namelijk heel duur en ik kan me niet voorstellen dat men de performance-kosten daarvan in de JVM zo goed kan optimaliseren dat het altijd winst oplevert.

Integer vs int is ook (vrijwel) altijd in het voordeel van de tweede... de referentie is (in dit verhaal) altijd even groot als de int zelf, dus zelfs als je de referentie doorpaast en nooit nieuwe objecten maakt win je niks. Zodra je het object echter moet gaan gebruiken moet je vervolgens die referentie gaan volgen om de juiste waarde eruit te vissen... bij de int had je 'm al bij de hand.

En voor wat betreft in-memory databases... we komen juist niet weg met standaard SQL en ook Lucene kan niet het type queries dat wij erop loslaten helemaal aan. Dus dan ben je alsnog veel tijd bezig met daar een oplossing voor te vinden en/of moet je (onderscheidende) features gaan schrappen.

Overigens gebruiken we ook veel externe bibliotheken, zoals Spring, de genoemde LZF, Lucene en Trove.
Nog belangrijker is misschien wel dat de meeste van deze optimalisaties eigenlijk geen ander algoritme vereisten. Of je nou een TObjectIntHashMap<Article> of een HashMap<Article, Integer> gebruikt, je code blijft vrijwel hetzelfde werken.
Hmmm,


Je kan tegenwoordig heel veel met SQL, ook complexe zaken en ook snel met de juiste modellering en optimalisaties. Toegeven, je moet dan wel een database gebruiken die de wat rijkere features van SQL geheel of gedeeltelijk ondersteund. AL dan niet in eigen dialect.

Ik ben dus wel benieuwd naar een voorbeeld model en de data extractie opdracht om te zien of dit echt buiten de mogelijkheden van SQL valt.
Het is een typisch techneuten artikel wat inhoudelijk wel hout snijdt maar de beargumentatie waarom voor bepaalde architecturen wordt gekozen is nogal zwakjes.
Waarom zou je een hele database inhoud in het geheugen willen inladen?
Daar hebben ze in-memory databases voor uitgevonden.
Die hele argumentatie staat dan ook domweg niet in het artikel omdat dat niet relevant was voor dit artikel... Dit artikel is in 2012 gemaakt nadat die beslissing al lang en breed in 2008 was genomen.

Maar de argumentatie is simpel; op het moment dat we hiermee begonnen was er geen enkele databaseomegving die het soort resultaten dat wij wilden - bij het type queries dat wij deden - kon opleveren. Denk daarbij vooral aan het aanleveren van facetten in de pricewatch: ondanks dat we de eerste 50 resultaten tonen hoeveel waren er in totaal en welke overige filters zouden ook nog steeds resultaten opleveren.
Solr en Lucene kunnen nu met facetten, maar in 2008 was Solr nog niet zo enorm ver en Lucene had toen nog niet de facet-package.

SQL kan sowieso dat soort queries niet efficient verwerken; je krijgt hele complexe statements en/of heel veel temporary tables. En dan is het nog maar de vraag of je met group-by's en aanverwanten wel de benodigde facet-statistieken kunt krijgen.
Elke query-taal die dus nog minder dan SQL kan valt dus sowieso af... Vergeet vooral niet dat de product-specificaties behoorlijk complex te definieren zijn.

Later hebben we ook nog dingen toegevoegd zoals het kunnen genereren van dynamisch samengestelde specificaties (zoals de "prijs per gigabyte"-spec bij harde schijven). Met een OO model kan je de complexiteit daarvan heel netjes wegstoppen op een plek waar dat goed past. Bij een off-the-shelve omgeving moet je dat soort dingen doorgaans er een beetje inhacken of zelf achteraf nog wat mee doen.
Op het moment dat je dan alsnog wilt kunnen filteren en sorteren op de uitkomst van die specificatie, wordt het best pittig... Wat het dan extra lastig maakt is dat productprijzen in theorie ieder moment kunnen wijzigen en daardoor je met name uitgebreide de-normalizaties (zoals bij Solr zou gebeuren) heel vaak moet herhalen.

Een andere feature is bijvoorbeeld het kunnen formatteren van specificaties. Als je een hoeveelheid geheugen wilt kunnen vergelijken ivm sorteren of filteren wil je natuurlijk dat 524288 (=512KB) kleiner is dan 1048576 (=1MB), maar wel dat de gebruiker de tekst 512KB en 1MB zal zien (en er op kan zoeken met tekst-search).

Er zijn vast producten die off-the-shelve ongeveer kunnen bieden wat wij van origine met onze omgeving deden, maar doordat het toch in een OO-model zat zijn we daar uiteraard gebruik van gaan maken. Zoals het toevoegen van die dynamisch opgestelde specificaties of het aanleveren en kunnen doorzoeken van geformatteerde specificatieteksten.

Wij zien het daardoor nog steeds als een product-(zoek/filter/informatie-)service, maar dan wel een die veel meer omhelst dan domweg een database waar de php-laag met ingewikkelde queries in moet proberen te zoeken... en dan nog alles aan formattering, koppelingen (bijv door een referentie naar een Apple-merk object te plaatsen bij het 2e apple-product in de serialized-output) etc zelf te moeten uitvogelen. Dat was juist een deel van de zwakte van de php-code die we gingen vervangen ;)
Ik begrijp dat we het hier hebben over een complex systeem, maar hoe doen reuzen zoals google dit (software-matig) dan? Ook zij hebben enorm complex gelinkte user-gegevens, die allemaal op ieder moment kunnen wijzigen en op verschillende systemen uitgesmeerd staan. Hebben zij al deze gegevens ook verspreid in een soort "shared memory" staan, of komt er bij het laden van google-mailtje wel degelijk ergens een query aan te pas?
Misschien wel een open deur: http://research.google.com/archive/mapreduce.html :+
Als je niet van lezen houd zijn er op youtube vast menig filmpje over te vinden. Anders is er deze http://research.google.com/roundtable/MR.html
Jullie hebben dus omdat jullie unieke requirements hadden een versimpelde versie van een OO database kernel gebouwd met eigen optimalisaties. Op zich zal menig hardcore Java programmeur zijn/haar vingers bij aflikken.
Dit zou ook met standaard onderdelen opgelost kunnen worden. Nog steeds dat het een grote uitdaging zou zijn geweest en dat de hardware requirements waarschijnlijk groter waren geweest maar qua ontwikkelkosten zou het volgens mij wel wat minder zijn geweest en qua onderhoud al helemaal. Wat jullie gebouwd hebben is vele malen moeilijker overdraagbaar.

Ik ben wel eens vaker tegengekomen dat een programmeur een eigen variant van een database (kernel) maakt omdat er sprake is van unieke requirements. In dit geval ging het over in principe een dubbelgelinkte lijst van arrays die je als polymorfe objecten zou kunnen beschouwen. Gemaakt in C++/Assembler en het was daarmee mogelijk om berekeningen van afhankelijkheden terug te brengen van dagen naar uren. Nadeel was wel dat het een single user database was en dat de lijst gedeeltelijk in virtueel geheugen terecht kwam.
Maar het principe van een database met polymorfe objecten is volgens mij vele malen krachtiger dan het (ouderwetse) relationele datamodel met gefixeerde aantallen kolommen.
Het gaat helaas niet alleen om het in-memory laden van de database, maar vooral om de complexe relaties tussen data (die sneller zijn wanneer ze in geheugen worden bewaard) en complexe operaties daarop.
Zeer interessant artikel! Als programmeur is het altijd goed om te weten hoe dingen echt werken, en wat de echte gevolgen zijn van bepaalde design decisions. Separates the boys from the men. ;)

However: hardware is cheap, programmers are expensive.
Behalve dat het een interessant academisch uitstapje is, lijkt het me onwaarschijnlijk dat de tijd besteed aan deze optimalisaties inc. het resulterende onderhoud goedkoper uitkomt dan een RAM upgrade voor alle dev workstations. Hoe kijken jullie daar naar?
Met het "hardware is cheap"-paradigma vergeet je dat je dan vrij snel het risico loopt dat je tegen de grenzen van verticale schaling aanloopt. Op het moment dat je dit soort dingen horizontaal moet gaan schalen krijg je ineens allerlei hele ingewikkelde problemen die je met diezelfde dure programmeurs moet op zien te lossen.

Een ander belangrijk aspect is dat het inderdaad heel goedkoop is geworden om meer geheugen te gebruiken, maar met de huidige multi-core omgevingen is de relatieve geheugenbandbreedte per cpu-core eigenlijk nauwelijks gestegen en ook cpu-caches (per core) blijven redelijk gelijk. Dus door meer geheugen te gebruiken - hoe goedkoop die dimms ook zijn - loop je alsnog het risico dat je moeilijk op te lossen performanceproblemen introduceert.
Uiteraard zijn dit soort optimalisaties ook niet zomaar zonder problemen en kosten ze in een aantal gevallen meer cpu-power, vooral bij het initialiseren van je omgeving. Dat laatste doen we echter normaliter maximaal 1x per iteratie terwijl die code wel vervolgens honderden keren per minuut gebruikt wordt.

Het aardige is dat hier niet eens zo heel veel manuren in gestoken is. De meeste wijzigingen waren vooral een kwestie van een interface herimplementeren hooguit met hier en daar wat aangepaste calls (lang leve refactor in je IDE) en/of vrij kleine toevoegingen aan bestaande code (zoals een string-interner inzetten of een utility die bij een aantal veel gebruikte arraylists kijkt of een singletonList niet handiger was).
Zo'n utility schrijven is uiteindelijk ook relatief weinig werk, vaak niet meer dan een paar uur, inclusief unit-tests en een aantal strategisch gekozen implementaties (je begon tenslotte met een geheugen-profiel waar je die strategische plekken al uit kon afleiden).

Je moet uiteraard net als bij cpu-optimalisaties wel goed kijken waar winst te halen valt, een goede (geheugen)profiler is hiermee dan ook onmisbaar. Als je ziet dat je ergens 5 arraylists gebruikt, dan is het niet zo boeiend om daar singletonList's van te maken, maar als je ziet dat er ergens 250.000 arraylists zijn, waarvan er 200.000 geen elementen hebben en nog eens 40.000 1 element... dan loont het wellicht wel de moeite daar wat tijd in te steken.
Net als bij cpu-optimalisaties gelden hier ook gewoon "80/20-regels".
However: hardware is cheap, programmers are expensive.
Slechte mentaliteit. Zonder dat ik het hele artikel heb gelezen wordt er eigenlijk goedgepraat dat je baggercode mag produceren omdat hardware zo goedkoop is en je slechte code maar fixt met meer hardware. Dit zijn nou typisch de argumenten waar de "sysadmin vs developer" discussies uit ontstaan.

Overgens ben ik het wel met je eens dat de argumenten die aangevoerd worden om deze operatie te doen een beetje frappant zijn. Niettemin is het een interessant artikel (en toch het Tweaker gehalte wat hoger te houden) maar als je er objectief naar kijkt heb ik ook zo m'n vraagtekens.
@Silentsnake: geheel ermee eens dat dit een slechte mentaliteit is. Ik heb vaak genoeg met lede ogen gezien (als senior engineer, architect) dat er maar wat aangekloot wordt, want 'daar gooien we toch wel hardware tegenaan' om dan achteraf te horen 'goh, hoe kan dat nou dat 'ie niet sneller gaat?'.
En dat het zo gaat komt niet in de laatste plaats omdat mensen met een 'business bril' op (en beslissingsbevoegdheid) denken dat je alles op de makkelijke en snelle manier kunt oplossen.

Let wel: ik heb het dan over projecten waar tonnen tot miljoenen geïnvesteerd worden, waarbij bedrijven serieuze risico's nemen en waarbij achteraf dus nog een flink kostenplaatje erbij komt om applicaties toch op het gewenste prestatieniveau te krijgen. Om nog maar te zwijgen over het verlies aan arbeidsproductiviteit als je duizenden gebruikers hebt die het moeten doen met een trage applicatie.

Het artikel van codinghorror.com stelt overigens juist dat je er met hardware niet alleen komt, en dat je ook (in bepaalde gevallen) optimizations aan je code moet gaan doorvoeren.

@Cergorach: ik zie in de praktijk en vooral bij grotere bedrijven dat er genoeg code is > 10 jaar oud (want geheel herschrijven wordt niet vaak gedaan, imho terecht).
In de tussentijd is er voor tientallen miljoenen (geen geintje) aan hardware tegenaan gesmeten zonder snelheidswinst (ook geen geintje). Ik betwijfel ten zeerste of dit investeringsregime in almaar snellere hardware echt goedkoper is dan de investering in betere geschreven software en optimizations.
Ik heb het al regelmatig gezien dat software traag blijft omdat 80% van de tijd in slecht geheugengebruik zit, slecht bedachte algoritmes of (ook een leuke) O/R mappers met n+1 problemen. Dit zijn allemaal quick wins die veel winst opleveren en prima afgezet kunnen worden tegen hoge cyclische hardwareinvesteringen.
Vaak gaat het namelijk niet om micro-optimizations maar software die gewoon extreem slordig en onbedachtzaam geschreven is, en dus veel ruimte laat voor snelle en eenvoudige verbeteringen.

Ik ben tegen premature optimizations, maar eis van 'mijn' ontwikkelaars wel dat ze performance in hun achterhoofd hebben en er rekening mee wordt gehouden dat software later te optimaliseren moet zijn. Dat kan prima zonder significante investeringen en is in mijn ogen- een kwestie van je werk goed doen.
Dit is een goed artikel, en wat mij betreft mogen er meer volgen.


edit: typo, extra uitleg

[Reactie gewijzigd door Bram® op 8 mei 2012 15:53]

Het blijft natuurlijk altijd "the right tool for the job". Hardware er tegen aan gooien terwijl dit niet helpt is natuurlijk niet handig terwijl een quick win software aanpassing die juist wel performance verbetering zou leveren wel weer handig is.

Het probleem is vaak dat je of een budget man heb of een software man of een hardware man die ieder hun eigen oplossing het beste vinden zonder buiten hun box te kijken.

Als er een memory leak zit in bepaalde software en er wordt gesproken over tonnen om het enigszins op te lossen (en dan werd niet eens gegarandeerd dat het geheel word opgelost), terwijl even extra geheugen inprikken en een scriptje die er voor zorgt dat de server een nette reboot krijgt buiten kantoor tijden er voor zorgde dat de klanten er geen last meer van hadden. Wat wil je dan? Natuurlijk is het niet netjes en wellicht is het oplossen van de memory leak wenselijk, maar als je data en daarmee je geheugen lek de komende jaren niet het mogelijk plaatsbare geheugen voorbij groeien dan is er niets aan de hand. Er zijn imho hele grote verschillen tussen hoe het hoort en hoe het werkt.

Je loopt dan natuurlijk tegen de lamp als de business besluit om 24/7 te draaien met de applicatie en iemand vergeet even te melden dat de server 's nachts wordt gereboot ivm. een memory leak. Of dat de backup van 23:00 tot 06:00 draait en dat systemen tijdens die periode niet beschikbaar zijn. Dat dergelijke issues niet bij de beslissende partij terecht komen is niet (alleen) de schuld van de beslissende partij, maar van de partijen die er tussen zitten. Managers die niet begrijpen dan wel onthouden wat hun medewerkers vertellen en IT medewerkers die hun managers niet duidelijk of correct inlichten.

Ik word er inderdaad ook niet vrolijk van als een manager besluit dat als ze een dual core processor in gaan zetten de applicatie wel sneller moet draaien omdat ze dan twee cores kunnen gebruiken. Zonder zich af te vragen of de software wel geschikt is om op meerdere cores te werken en of dat een performance verbetering teweeg brengt... Of de manager die P4s wil inzetten omdat ze zo snel zijn (hoge kloksnelheid)...

Dergelijke problemen hebben niet zo zeer te maken met de gekozen oplossing, maar met de kwaliteit van het personeel dat er werkt (op alle niveaus).
'Bagger' code is natuurlijk subjectief, als het resultaten levert dat binnen de specs valt dan is er natuurlijk weinig aan de hand. Teveel ITers (niet alleen een developer issue) vergeten dat het doel van software is niet de software zelf, maar de toegevoegde waarde voor de corebusiness en dat afgewogen tegen de kosten.

Code opleveren is niet een keuze tussen 'goede' code of 'slechte' code alsof deze beide evenveel kosten om te produceren. 'Goede' code kost extra tijd, extra tijd kost extra geld. Je kan natuurlijk ook zeggen, neem dan een 'goede' programmeur aan, maar deze kost ook meer dan een 'slechte'. En vaak zie je aan de buitenkant of CV niet of je een 'goede' of een 'slechte' te pakken heb.

Natuurlijk kan efficiënt/snel code een goede investering zijn, maar dat moet natuurlijk wel even in context worden bekeken, want in veel gevallen hebben we nog maar weinig aan code dat 10 jaar geleden is geschreven. In de tussentijd zijn de techniek qua software en hardware enorm voorruit gegaan zodat nieuwe technieken worden gebruikt en het oude code in de prullenbak kan...

Bedenk eens wat een werkplek (dus salaris plus overige kosten) van een 'goede' programmeur kost en hoeveel tijd er aan een dergelijke optimalisatie wordt besteed, test trajecten (want nieuw code kan weer nieuwe fouten bevatten), etc. Zet dit eens uit tegenover de kosten van hardware voor de komende tien jaar...
Ik ben het echt niet eens met jouw reactie. Er moet altijd goede code geschreven worden TENZIJ je de code 1 keer schrijft en er nooit meer onderhoud op hoeft te plegen. Normale trajecten hebben wel onderhoud en onderzoek heeft uitgewezen dat van de totale kosten van een systeem in die 10 jaar 20% in de nieuw bouw heeft gezeten en 80% in het onderhoud.

Goede code maakt systemen beter onderhoudbaar en dus goedkoper op de lange termijn.
Dat ligt dan inderdaad wat voor traject je heb, wat de extra kosten zijn voor 'goede' code en of het reëel is dat er inderdaad zoveel aan onderhoud nodig is.

Laten we stellen dat het bagger project 200k kost en daarna over 10 jaar 80k per jaar.
Laten we stellen dat het goede project 800k kost en daarna over 10 jaar 60k per jaar.

Das natuurlijk kort door de bocht, maar de situatie kan zo prima voorkomen.

Laten we stellen dat het goede project 400k kost en daarna over 10 jaar 40k per jaar.

Kijk nu heb je 200k bespaart! Maar wat nu als ze na 5 jaar de stekker er al uit trekken omdat het hele pakket van scratch moet worden gebouwd met andere techniek en andere specs (door markt werking of wetgeving). Om nog maar niet te spreken over het feit of je het extra geld voor 'goede' code wel heb liggen of als je een behoorlijk rendement heb op je eigen investeringen.

Vaak lopen projecten aan tegen issues met schaalbaarheid, limieten van de techniek (software/hardware) toen der tijd of gewoon algehele bedrijfsvoering die verandert.

Als je bv. naar Tweakers.net kijkt hadden ze 10 jaar geleden helemaal geen geoptimaliseerde java engine nodig, waarom zouden ze 10 jaar geleden de php hebben geoptimaliseerd terwijl ze twee jaar geleden (gedeeltelijke) zijn overgestapt naar Java?

In mijn ervaring is dat een 10% performance verbetering van je huidige software vaak 90% van je tijd kost (wellicht lichtelijk overdreven, maar you get the point hoop ik). Terwijl een andere oplossing (software en/of hardware) vaak veel goedkoper is en je meer flexibiliteit oplevert.
Sterker nog, de functionaliteit van de pricewatch was in die tijd zodanig veranderd dat de optimalisaties die we wel degelijk doorvoerden alsnog niet voldoende winst opleverden... De java her-implementatie lostte het eenvoudiger op. Bovendien was ie ook nog veel uitbreidbaarder, waardoor we nog veel meer met de pricewatch konden gaan doen daarna.

Overigens proberen we wel degelijk onderhoudbare code op te leveren. Die code is doorgaans ook beter te testen, levert "vanzelf" betere prestaties op en is in kleinere delen later uit te breiden of aan te passen.
We hebben niet de illusie dat de code nu "klaar" is en nooit meer aangeraakt zal worden, maar tegelijk willen we bij de volgende keer dat we het aan moeten raken wel liefst zo min mogelijk hoeven te veranderen :)
Lees het bronartikel ook; Betere code is niet altijd sneller, en snellere code is niet altijd beter.

De kreet "Premature optimization is the root of all evil" is niet zomaar een kreet; Performance optimalisatie kan tot complexere of slechter leesbare code leiden. Dat verhoogd de beheerslast, en de kans op bugs.

Stel dat jij met een ingewikkelde constructie waar niemand iets van snapt, 15% geheugen bespaard, en een lastige bug introduceert, heb je de software dan beter of slechter gemaakt?


Even voor de duidelijkheid; Ik wil niet beweren dat het bovenstaande van toepassing is op tweakers.net. Ze zetten al jaren een fantastisch en stabiel product neer, dus ik ben volledig bereid aan te namen dat ze het beter weten dan ik. ;) - Ik ben natuurlijk wel benieuwd naar hun overwegingen en bevindingen.
Ik ben dan ook zeer benieuwd hoeveel manuren er is besteed aan deze optimalisatie, hoeveel sneller deze optimalisatie is en of de snelheidsverbetering ook niet met snellere hardware opgelost kan worden.

Natuurlijk is Tweakers, Tweakers niet als ze de boel niet zelf tweaken ;-)

Maar vanuit een business perspectief en kosten plaatje vraag ik met toch af of dit nuttig en kosten effectief is. Of groeit de dataset zo snel dat deze de komende jaren niet kan worden bijgehouden door hardware verbeteringen of worden de kosten van hardware zo hoog dat dit alsnog een goede investering is?
Helemaal mee eens. Gedurende het hele artikel moest ik aan dit artikel op thedailywtf.com denken, waar een stagiair afvraagt of vijf maanden ontwikkeltijd opweegt tegen een geheugenbankje van een paar tientjes.
Als ik he zo inschat heb ik er misschien een week of twee aan besteedt. Je wordt er vrij snel handig in en dan zijn nieuwe besparingen vinden eenvoudiger. Bovendien zijn dit ook het soort optimalisaties en ideetjes die ik wel leuk vind om bij wijze van hobby uit te vogelen (zoals een performancevergelijking van LZF vs Zlib) en dus hooguit dan nog implementatietijd "van de zaak" kosten :P (en de meeste tijd zit doorgaans in de research).

Maar de besparingen zijn dan - zoals ik in een andere posting aanhaalde - cummulatief ook wel al gauw in de orde van grootte 6-10GB. En als je het over heaps van 12-16GB gaat hebben in plaats van 4-8GB, loop je ineens tegen grenzen aan van workstations (de gene die wij nu hebben kunnen niet meer dan 16GB kwijt), waardoor het niet zomaar meer een gevalletje van "een dimmetje erbij prikken" is.
Of het dan alsnog goedkoper is om dikkere workstations te nemen, op dedicated testservers te deployen, een hele andere koers te gaan varen of toch dit soort relatief eenvoudige wijzigingen door te voeren is uiteraard moeilijk voor- en achteraf in te schatten.

[Reactie gewijzigd door ACM op 8 mei 2012 18:03]

40-80 uur is inderdaad vrij weinig voor dergelijke verbeteringen, wellicht kom ik gewoon te weinig developers van jou kaliber tegen :-)

Zitten jullie al op heaps van 12-16GB? En zo niet, wanneer verwacht jullie daar wel op te zitten?
Deze omgeving kan momenteel nog goed in een 4GB heap draaien, maar dat is dan uiteraard wel met marginale request-load.
Op de productieservers verwacht ik dat we 'm wel op 12GB gaan zetten, domweg "omdat het kan" en omdat de JVM dan gewoon ook tijdens interne herindexatie goed ruim in zijn geheugen zit: we willen tenslotte geen OOM krijgen als er toevallig wat meer gebruikers-requests zijn en een groot stuk data opnieuw opgebouwd wordt en er tijdelijk twee versies in leven zijn.
Ik bekijk het van een andere kant:
Voor mij is de snelheid van een website die ik veel bezoek een heel groot deel van de userexperience (liever een lelijke website die snel is, dan een fancy langzame website). Als een website langzaam laad irriteer ik me daar snel aan. Ik ben dan een gebruiker die zich daar erg bewust van is, maar dit geldt voor eigenlijk iedereen (en vooral bij ervaren gebruikers) op een onderbewust niveau.

Tweakers had de afgelopen 24u 3,75 miljoen pageviews. Als deze optimalisaties gemiddeld 0,1sec winst opleveren per pagina, dan is dus 375.000 seconden per dag dat gebruikers minder zitten te wachten. Dat zijn meer dan 100u per dag dat gebruikers minder hoeven te wachten. Dat lijkt me al snel de moeite waard.

Verder wordt er in het artikel en de commentaren ook vermeld dat deze optimalisaties niet in één ronde zijn doorgevoerd, maar dat dit een proces is geweest wat heeft gelopen tijdens de normale ontwikkelvoortgang. Het is dus waarschijnlijk dat veel problemen zijn aangepakt op het moment dan men ze bij reguliere werkzaamheden toch tegen kwam. De extra tijd die dit kost kan daardoor dus heel beperkt zijn en dat blijkt ook uit de reactie hieronder van ACM.

Al met al vind ik de focus van t.net op snelheid heel goed.
Het is leuk om te zien wat voor hoepels jullie door hebben moeten springen om Java toch nog een beetje werbaar te houden in een high traffic larger data set situatie. Je kunt je dan ook af vragen of Java wel de juiste oplossing is voor zo'n situatie. Zelf werd ik niet zo lang geleden gevraagd om een large data high traffic situatie te verbeteren van ~5 seconde response times naar >1 seconde, en dat met een data base met meer dan honderd miljoen entries die per dag een kleine 10 miljoen nieuwe entries er bij krijgt... Totale grote van de database is zo rond de 100GB. Even een Java truckje uithalen zo als hier genoemd kan simpel weg niet omdat Java daar simpel weg niet geschikt voor is dankzij al het actieve memory management en zo. Onze oplossing is een in memory database waar je dus wel een machine met net even wat meer geheugen nodig hebt maar goed dat is nog niet zo'n ramp voor het geld hoef je het niet te laten. Natuurlijk is alles ook op de disks beschikbaar en zal er niet meer data verloren gaan dan het geval was toen het geheel nog op Oracle draaide.

In middels zijn we zo ver dat we de eerste resultaten binnen krijgen van de eerste testers, en ze hebben er nog al wat moeite mee omdat ze niet zien dat de nieuwe data set al op het scherm staat. Men is gewend dat zo iets wel even duurt en kan het simpel weg nog niet geheel bevatten dat met een volledige database de resultaten in minder dan 1 seconde al op het scherm staan.

Ik begrijp dat Java voor jullie de het en een is simpel weg omdat dat nu eenmaal jullie standaard taal is maar als ik kijk naar de hoeveelheid werk die er verzet moet worden om de prestaties en het geheugen gebruik binnen de perken te houden dan vraag ik me af of het wel zin heeft. Het argument dat een workstation maar 8GB geheugen heeft is natturlijk leuk maar niet echt zinnig omdat de prijs van geheugen praktisch 0 is en testen met een volledige data set zeker als je een in memory database gebruikt simpel weg zelden nodig is en dus beperkt kan worden tot het testen op een test omgeving dan wel een centrale development omgeving. Ook hier een leuke tip bouw een development server gelijk aan of zo dicht mogelijk bij je productie omgeving. Daar maak je je build en test je s'avonds het geheel, je hebt dan helemaal geen workstation nodig en kunt simpel weg de hele dag testen met een build van de avond er voor. Je hoeft echt niet eleke developer van een volledige omgeving te voorzien dat is in de meeste gevallen simpel weg niet realistisch nog haalbaar. Hoe verder we richting big data gaan hoe onmogelijker dat zal worden dus als je slim ben sla je dat er nu al uit voor je straks met een meute klagende developers zit die roepen dat ze zo niet kunnen werken.
Er zijn uiteraard verschillende wegen die naar Rome leiden, waarbij onze keus voor een in-house gemaakte java applicatie er 1 is. Gegeven dat we al een basis hadden voor de huidige pricewatch en gezien onze huidige dataset en de groei van deze dataset, gecombineerd met de kennis van de devvers en de beschikbare tijd (dit is natuurlijk niet het enige werk wat voor Tweakers 7 verzet moet worden), was dit de beste keuze.

Uiteraard is het mogelijk om een dataset die een paar keer groter is beter met een andere taal/tool te benaderen, maar dat moet vervolgens wel binnen de mogelijkheden vallen, oftewel, de situatie moet er naar zijn.

Wat mij opvalt is dat je zegt dat je zelf niet voor java gekozen hebt, maar voor iets (schijnbaar) beters, maar ik kan niet goed opmaken uit je verhaal wat dat dan is?
+2Anoniem: 23636
@Rob Coops9 mei 2012 22:48
Totale grote van de database is zo rond de 100GB. Even een Java truckje uithalen zo als hier genoemd kan simpel weg niet omdat Java daar simpel weg niet geschikt voor is dankzij al het actieve memory management en zo.
De (standaard) hotspot JVM niet, maar de Zing JVM van Azul Systems kan wel met dergelijke heaps werken, waarbij GC-delays onder de 10 ms blijven.
Ik vind het werkstation-argument ook dun. De andere argumenten lijken mij details. Ik ben wel benieuwd hoeveel tijd dit onderzoek plus de verbeteringen hebben gekost, en hoeveel ermee bespaard is (uitbreidingen nu en in de toekomst). Ik snap dat de focus hier op het technische gedeelte ligt, wat erg interessant leesvoer is overigens. Maar ik ben wel benieuwd naar een iets uitgediepte rationale achter de keuze.

Rob's opmerkingen over de test-omgevingen kloppen helemaal. Wij testen hier ook een kopie van ons productieplatform met een beperkte dataset. Pas als we het gaan testen in een staging-omgeving, is de dataset net zo groot als op productie. De staging-omgeving is slechts enkel uitgevoerd, dus daarvoor hoef je maar 1 keer te portemonnee te trekken.
De besparingen zijn allemaal los en gedurende diverse iteraties uitgevoerd en het idee van dit artikel ontstond pas ruim na dat de meeste optimalisaties door waren gevoerd. Dus de documentatie is wat lastig daardoor, vooral ook omdat e.e.a. dan latere uitbreidingen ook kleiner hield.

Natuurlijk is het werkstation dun, maar niet de enige. Sowieso is het gewoon interessant om dit soort dingen te optimaliseren of is het soms eigenlijk meer een bijkomstigheid van een andere optimalisatie.

Ik denk dat als we alle huidige (test)data zonder deze optimalisaties zouden inladen, dat we dan zo'n 6-10GB meer data zouden gebruiken... Wat dat betreft is het werkstation-argument wel heel handig, want je loopt veel sneller tegen inefficiente structuren aan met OOM's dan wanneer je 20GB ram zou (kunnen) alloceren :P
Het werkstation-argument wordt m.i. een beetje onderschat nu.
Voor ontwikkelaars is het echt wel een toegevoegde waarde als ontwikkelingen direct kunnen gebeuren op een realistische omgeving. Zo kan de performantie-impact van wijzigingen beter worden ingeschat, en kunnen bugs beter worden gereproduceerd, ...

Verder gaat zoiets meestal gepaard met een goed gestroomlijnde procedure (en kennis ervan bij de teamleden) om te deployen, een nieuwe omgeving op te zetten enz. wat volgens mij zeer belangrijk is voor de efficiëntie en zelfredzaamheid van de teamleden.
Kan niet anders zeggen dat dit mij een veel practischere oplossing lijkt dan wat tweakers.net gedaan heeft. Maar 'tweakers' doen vaak dingen omdat het kan, dus misschien is dat hier ook wel het geval.

Maar kijkend naar investering lijkt me een in memory database server veel goedkoper dan het ontwikkelen van de in het artikel geschreven code.

Maar toch ook interessant artikel om te lezen.
in het artikel wordt juist een in memory database beschreven. het is alleen niet een COTS oplossing, maar een op maat oplossing voor de specifieke toepassing van Tweakers.net.

Ik vraag me af hoeveel van de mensen die hier roepen dat JAVA traag is, er zelf recentelijk op enterprise niveau mee gewerkt hebben. Er zijn namelijk genoeg toepassingen te bedenken waar java sneller en practischer is dan iets als C of C++, denk bv aan runtime optimalisaties (die kunnen per definitie niet in een volledig gecompileerd platform), of cross platform beschikbaarheid (bv ontwikkelaars die op mac/linux/win ontwikkelen).
omdat Java daar simpel weg niet geschikt voor is dankzij al het actieve memory management en zo.
'geschikt' is subjectief maar ook daar zijn zat mogelijkheden voor.

Ook zie ik niet helemaal hoe de programmeertaal betrekking heeft op de keuze van in memory database

Alweer wat oudere presentatie mbt tot latency in java:
LMAX - How to Do 100K TPS at Less than 1ms Latency:
http://www.infoq.com/presentations/LMAX
0Anoniem: 151857
@Rob Coops8 mei 2012 12:56
~5 seconde response times naar >1 seconde
Nou, dan was je zeker snel klaar ;)
*grapje*
Leuk artikel, maar niet zo heel nuttig voor de gemiddelde Java applicatie, omdat die de data gewoonlijk uit de database haalt. Indexeren kan je dan bijv met Lucene doen en dan ben je al eigenlijk klaar. Mocht de database te traag worden, dan zou je bijvoorbeeld MongoDB kunnen gebruiken.
Desondanks wel nuttig, het geeft wel aan dat een simpel lusje om strings aan elkaar te plakken al gauw redelijk wat data verstookt, iets wat veel ontwikkelaars al snel vergeten. Als daarna echter de garbage collector afgaat is het probleem ook weer verholpen...

Was het niet handiger geweest om alleen de nieuwe artikelen in het geheugen te houden, van bijvoorbeeld de laatste maand ofzo? Ik kan me voorstellen dat de oude artikelen zelden of nooit bekeken worden. Hierdoor hoef je ze al niet eens in het geheugen te laden. Om relevante artikelen te bepalen zou je dan alleen wat steekwoorden in het geheugen bij te hoeven houden, en klaar ben je. of nog mooier: de top 8000 artikelen. Artikelen die worden opgevraagd uit de cache krijgen een nieuwe timestamp, en de oudste artikelen vallen er zo vanzelf uit...

Naja, er zal ongetwijfeld wel goed over nagedacht zijn, maar ik kan me zo voorstellen dat dit niet altijd zo door kan blijven gaan... Op en gegeven moment loop je toch uit je geheugenruimte, ondanks al je compressie.
Het probleem is hierbij dat MySQL en MongoDB de complexe filtering niet efficient kunnen verwerken. Althans, niet in combinatie met het ophalen van facet-informatie en een goede schatting van het aantal items dat in totaal nog aan je filters voldoet.

Daar zou je op zich wel Lucene of Solr voor kunnen gebruiken, maar wij hebben uiteraard nog net dat beetje extra complexiteit wat Lucene/Solr ook niet zomaar praktisch maakt. Bovendien moet je voor een Lucene-index (of MongoDB-collection) alles tot op het laagste niveau denormalizeren in losse "documenten".
Als er dus relatief veel van die afgeleide data verandert, ben je continu met hele complexe bijwerk-operaties bezig.
Door deze applicatie hebben we e.e.a. kunnen vereenvoudigen omdat we met referenties naar objecten kunnen werken en daardoor niet steeds de inhoud ervan hoeven te dupliceren.

Qua ram-geheugen heb ik hieronder al een opmerking geplaatst. 10 jaar aan verzamelde gegevens past in een 4GB heap... dus we gaan er van uit dat we nog wel even mee kunnen, ook al groeit het tegenwoordig wel wat harder dan in het begin :)

Voor wat betreft de garbage collector: vergeet niet dat geheugen gebruiken ook impact op je rekentijd heeft, niet enkel op de absolute hoeveelheid RAM die gebruikt wordt. En bovendien is die absolute hoeveelheid ook eindig (dat is zelfs een van je opmerkingen over onze opzet ;) )

[Reactie gewijzigd door ACM op 8 mei 2012 09:19]

Misschien eens kijken naar ElasticSearch of soortgelijk, kun je een stuk meer mee.
Volgens mij kan daar nauwelijks meer mee dan met Lucene of Solr, het grootste voordeel is dat het een stuk eenvoudiger is om die databases schaalbaar in te zetten. Maar met onze relatief compacte dataset (het past tenslotte nog binnen 4GB ;) ) is dat wat overbodig om verticaal over meerdere machines uit te smeren.
Er zijn andere (commerciële) oplossingen beschikbaar vandaag die de problemen die je aanhaalt zeer efficiënt kunnen oplossen (filtering met facets + counts en eenvoudige en ogenblikkelijke updates)
De oplossing die wij ontwikkelen en commercialiseren lijkt immers te voldoen aan de eisen die je stelt en alle nodige features aan te bieden (en meer). (Stuur een DM indien je meer info wil, 'k wil hier niet "spammen")

Kan je een inschatting geven van de hoeveelheid tijd/werk die in deze optimalisaties gestoken is? (Die mythische manmaanden enzo :) )
Volgens mij werkt een database ook met zo'n cache.
Erg interessant artikel, met veel plezier gelezen hoewel ik nog wat meer simpele codevoorbeeldjes ook leuk had gevonden. Waarom is er eigenlijk voor Java gekozen? Dat staat niet echt bekend als een heel snelle taal. Wat mij betreft mag dit iets nieuws worden op Tweakers, zulke artikeltjes over praktische implementaties!
Waarom is er eigenlijk voor Java gekozen? Dat staat niet echt bekend als een heel snelle taal.
Niet bekend staan als snel en niet snel zijn, zijn 2 verschillende dingen. Java heeft inderdaad die reputatie, en in het verleden had java ook wat performance problemen. Maar tegenwoordig(al een tijdje eigenlijk) is java erg rap. Wat betreft performance is er geen reden om niet voor java te kiezen eigenlijk.
Het is nog altijd stukken langzamer dan andere talen eigenlijk.
Maargoed, het is een keer in Java geschreven, en programma's herschrijven in een andere taal heeft ook zo zijn nadelen :P

Zelf ben ik meer thuis in de c/c++/c# wereld.
Als ik nu naar c# kijk zie ik een groot aantal overeenkomsten.
Iets wat ik niet ben tegen gekomen is het verschil tussen class en struct, dat ook op dezelfde manier in c++ zo werkt qua geheugen.
Als je veel data in objecten opslaat is het verstandig om een struct te gebruiken ipv een class.
Ik kwam er zelf een keer achter toen ik een plaatje had gemaakt, en de een kleuren class had gebruikt. Ipv de 8MB die hij zou moeten gebruiken kwam ik uit op zo'n 70MB, ook al een vrij goede geheugen optimalisatie :)
Een class heeft namelijk een aantal pointers zoals this, die extra ruimte in nemen. Gek genoeg kun je wel this gebruiken in c# bij een struct die in principe geen extra pointers heeft.

Weet iemand of je dit zelfde trucje ook in Java kan doen?
Het is nog altijd stukken langzamer dan andere talen eigenlijk.
Hoe kom je daar nu weer bij. Java is 1 van de snelste platformen. Check bijvoorbeeld http://shootout.alioth.debian.org/

De 'traagheid' van java was meer een perseptie van de verkeerde architectuur keuze mbt de GUI. De platformonafhankelijkheidseis zorgde ervoor dat er nooit gebruik gemaakt werd van native gui componenten, maar dat deze volledig zelf gerenderd werden. Dit zorgde ervoor dat in die tijd de GUI merkbaar trager reageerde dan native applicaties.

Verder kent java niet het concept 'structs', de vraag is echter of bij Java hetzelfde probleem speelt. Zoals ACM al aangeeft is de overhead slechts 8 bytes per instantie. Dat is die 'extra ruimte' die ingenomen wordt naast de in het object zelf opgeslagen data.
Dat is dus een naieve vergelijking van talen. In de praktijk is een taal als C++ door zijn compile-time templates vele malen sneller dan C en zeker dan Java. Je kunt in een taal als C++ veelal het werk van performance gewvoelige inner-loops naar compiletime verplaatsen. Het resultaat: compiler doet er uren over in plaats van seconden en de inner loop functie wordt een order of magnitude sneller.
Los daarvan zijn talen als C, C++, Objective-C in de handen van een performance bewuste programeur veel CPU-cache vriendelijker in ghet gebruik van grotere data structuren in memory. Tenslotte, en daar komen we de laatste tijd veel meer achter, werkt het threaded concurency model van Java eigenlijk helemaal niet zo goed voor het concurent afhandelen van vele IO intensieve taken.

Zelfs een single-threaded JavaScript (of all languages !) op NodeJs out-performed een dikke multi-core multi-threaded Java based server als het om veel KCCons gaat in combinatie met database acties.

Met C++ en boost::asio bereik je (met flink wat meer moeite) een multi-core capable variant van het NodeJs concurent-IO model, die met de bijkomde voordelen van C++ op het gebied van compile-time calculatie bij elkaar resulteerd in veel meer performance op de zelfde hardware OF veel minder hardware voor de zelfde performance.

Ik snap best dat tweakers uit hystorische overwegingen, en het vermoedelijk nog redelijk beperkte aantal KCCons nog steeds Java gebruikt, maar het idee dat Java vooral voor hogere KCore nummers één van de snelste platformen zou zijn is gewoon pertinent niet waar. Zelfs JavaScript is voor dit soort toepassingen een order of magnitude schaalbaarder dan Java, en C++ maakt daar, afhankelijk van data structuren en de mogelijkheid om performance critische delen naar compiletime te verplaatsen nog eens één of twee orders of magnitude er bij.

Voor specifieke toepassingen kan dat, weet ik uit ervaring, gewoon betekenen dat je bijvoorbeeld een kleine farm van 4 servers met elk twee 2.5Ghz quadcores met Java wel eens heel goed zou kunnen vervangen door twee single-processor single-core 500Mhz 'appliance' apparaatje met C++ en boost::asio.

Ja het is een uitzondering waarbij zowel het aantal connecties, de database interacties, de data structuur als ook de uitgevoerde algoritme en de mogelijkheden van compile-time claculatie een rol speelde, en ja, de appliances waren eigenlijk alleen maar bedoeld om aan te tonen hoe schaalbaar de oplossing was omdat het niet mogelijk was om op een andere manier de schaalbaarheid te testen, dus de uitendelijke oplossing is toch op twee dikke servers overgedimensioneerd, maar dat soort performance verschillen zijn dus gewoon mogelijk, en overdimensionering heeft ook zo z'n voordelen i.v.m. de potentiele DOS bestandheid van de applicatie.
Dat is dus een naieve vergelijking van talen. In de praktijk is een taal als C++ door zijn compile-time templates vele malen sneller dan C en zeker dan Java.
Het begon zo goed en vervolgens doe je precies het zelfde. ;(

compile time optimalisaties en specifiek meta template programming maken maar klein gedeelte van het spectrum van optimalisaties en ook maar toepasbaar op een kleine subset van problemen - ook kunnen andere talen(ook java) via andere wegen prima het zelfde of nagenoeg het zelfde bereiken. C++ Template zijn wel elegant en makkelijk beschikbaar andere kant heeft het ook impact op leesbaarheid. Zelf vind ik de error berichten heel obscure maar dat ligt niet fundamenteel aan c++ templates maar niet te min praktisch wel heel irritant.

Overigens kan met meta circular evaluation http://en.wikipedia.org/wiki/Meta-circular_evaluator überhaupt leuke truuks uithalen. :*) Is ook menig mooie paper over geschreven.

Anyway, de kunde en kennis van de programmeur, als wel gekozen algoritme heeft veel meer invloed op de performance dan de gemiddeld gebruikte taal. Die marge is ook veel kleiner dan de ruimte die zit in het performance gat van jouw voorbeeld - dat neigt toch echt naar een probleem tussen het toetsenbord en de stoel dan wel de beschikbaarheid of kennis van het executie omgeving.
Tenslotte, en daar komen we de laatste tijd veel meer achter, werkt het threaded concurency model van Java eigenlijk helemaal niet zo goed voor het concurent afhandelen van vele IO intensieve taken.
Ik heb nog geen model gezien die niet te implementeren was onder java en c++ (als je voorbeelden heb hoor ik die graag) Mocht je op het hele "add locks until it works (tm)" principe doelen niemand met ook maar het minste verstand van concurrent programmeren heeft daar ooit in gelooft (dat ook in 2012 nog steeds niet iedereen de memo heeft gehad is dan ook zeker bedroevend.) Wat betreft green threads/erlang processes/actors et al moet je ook echt afvragen of het handig is om een sceduler op dat niveau te implementeren, elke soort van introductie van een hiërarchie van scedulers introduceer je overhead. De rede waarom deze aanpak toch goed kan uitpakken is omdat er automatisch een hoeveelheid resources gealloceerd word per thread en bij een bepaalde hoeveelheid thread de koek op is. Een (logische) andere oplossing is de hoeveelheid(door het OS) per thread gealloceerde resources te verlagen en ja dat werkt en loop je ook tegen de beperkingen van de OS scedulers, in de linux kernel is dit gefixed of deels gefixed(er is iig een patch voor) bij microsoft hebben ze de memo nog niet gehad(ik heb nog niet naar de laaste versie gekeken.) Het enige verschil tussen user threads/fibers/green threads is dan het userspace vs kernelspace. En daarmee ook terug naar het topic komen: dat is pas een **** memory layout/geheugebeheer/datastructuur 8)7
Ook hier zijn ideen over om het op te lossen:
Another approach taken in experimental operating systems is to have a single address space for all software, and rely on the programming language's virtual machine to make sure that arbitrary memory cannot be accessed
http://en.wikipedia.org/wiki/Userspace

Voor de mensen die ik kwijt ben - sorry heb helaas op dit moment geen tijd om er een echt uitgebreid stuk over te schrijven.

@pibara - Misschien dat het handig om afkortingen als KCCons en KCore uit te schrijven ook met google erbij kom je(ik iig) niet ver.
Met KCCons doel ik op de metric van het aantal concurrent connecties. Het aantal Kilo (1000) concurrent connecties. Zodra dat getal een beetje hoog begint te worden dan trekt Java dat beduidend minder goed dan JavaScript op NodeJs. Zal niet zo zeer aan de taal liggen maar meer aan de modelkeuze die bij NodeJs ondanks de taal toch een sucses bleek te zijn terwijl je met java naar het lijkt nog niet echt op het volledig asynchrone model over kunt stappen. In ieder geval niet zonder al je bestaande frameworks en infrastructuur aan de kant te zetten. TMP in C begint vaak gewoon waar NodeJs ophoudt. Door je IO/pools bottlenecks uit de weg te ruimen komt heel vaak een CPU bottleneck naar boven. Verschuiven van werk naar compile time in bescheide vorm is overigens veel breder inzetbaar dan veel mensen denken. Het uitfactoren van een enkel ifje binnen een lus kan soms al een enorme boost opleveren. Als tenslotte nog spraken is van grote in memory data structuren dan kan de directe controlle over memory layout in best veel gevallen een enorme invloed hebben op het aantal cache hits van je CPU. In Java en javascript heb je die controle niet en mis je dus die mogelijkheid. Je zult deze drie issues niet vaak alle drie samen tegen komen waardoor de winsten die je bij elkaar opgeteld kunt halen minder groot zijn,maar frankly het wegnemen van de eerste van deze drie bottlenecks halveert vaak in zn eentje al het benodigde ijzer dus is al de moeite waard.

[edit] Vert.X ziet er veel belovend uit trouwens voor wat betreft het NodeJs verhaal.

[Reactie gewijzigd door pibara op 10 mei 2012 13:02]

En buiten NodeJs heb je nu ook nog:

- luvit ( using luajit ), 2x performance en veel minder geheugen gebruik
link: http://luvit.io/

- Node.Native, a c++11 versie die natuurlijk weer sneller is....
link: https://github.com/d5/node.native/#readme

Simpele vergelijking op : http://www.devthought.com...ttp-hello-world-showdown/

Natuurlijk gebruiken ze allemaal de libuv library, die voor de eventafhandeling zorgt.

Allemaal leuke ontwikkelingen....
Verder kent java niet het concept 'structs', de vraag is echter of bij Java hetzelfde probleem speelt. Zoals ACM al aangeeft is de overhead slechts 8 bytes per instantie. Dat is die 'extra ruimte' die ingenomen wordt naast de in het object zelf opgeslagen data.
Oke, die overhead heb je dus niet bij structs die toepasbaar zijn in de c dialecten.
Maar die 8 bytes klinken niet zo veel zo, maar eigenlijk is het best wel wat als je veel data hebt.
Een foto van 12MP zou dan namelijk al 96MB overhead hebben bijvoorbeeld. Dat vind ik nogal wat.
Als je een object per pixel zou nemen, of een object per kleurcomponent, dan zou je gelijk hebben. Het is echter in dat geval veel zinvoller om gewoon een bytearray te gebruiken, en dat is eigenlijk ook wat uit dit artikel volgt. Dan heb je in het geheel geen overhead meer.
Klopt, maar mijn implementatie was als volgt:
RGBColour
XYZColour
LABColour

Door vervolgens een generic bitmap klasse te gebruiken heb je veel voordelen. De bitmap kan uit ieder kleuren patroon bestaan met bijbehorende functies handig bij de hand. En de bitmap heeft ook basis functies, zoals van x,y coordinaten naar een gewoon array, aangezien jagged of 2D arrays weer trager zijn :)

Op die manier heb ik toch een mooie object oriented oplossing, met veel performance :)
@wootah :? http://en.wikipedia.org/wiki/Flyweight_pattern

(op een of andere manier onder de verkeerde post gekomen)

[Reactie gewijzigd door Mr_Light op 8 mei 2012 19:37]

@Mr_Light
RGB, CIE-XYZ en CIE-LAB zijn 3 zeer verschillende kleur ruimtes en ik wilde ze daarom ook specifiek implementeren op die manier. Er zit heel wat wiskunde en regeltjes achter, op deze manier houd ik het duidelijk voor iedereen.

Het enige wat de structs met elkaar delen zijn 3 variabelen eigenlijk. Met een struct in c# sla je alleen die 3 variabelen op in het geheugen. De functies en methoden in een struct worden maar een keer in het geheugen geplaatst, net als bij een class. Je hebt dus verwaarloosbare overhead als je een aantal grote foto's inlaad ;)

Ik had het inderdaad zoals Janoz kunnen doen. Dezelfde manier word ook toegepast in .net bitmap en (wpf) writeablebitmap als je op een "unsafe" manier gaat programmeren. Unsafe is echter een achterlijke benaming, het wil alleen zeggen dat je direct toegang hebt tot pointers. Aangezien alle bitmap toestanden in .net eigenlijk GDI+ zijn (unmanaged) is unsafe de enige high performance manier om iets te schrijven/lezen.

Nadeel is, er ontstaat snel verwarring met zo'n array :+
Met een simpele rechttoe rechtaan implementatie behoud je het overzicht, en krijg je minder snel vreemde dingen op je scherm te zien met wat bewerkingen. :)
Is mijn manier langzamer? Ja, ietsjes, aangezien je een verwijzing (pointer) dieper moet per pixel in theorie. Is dat merkbaar? De impact is minimaal kan ik je vertellen ;)
Ik heb namelijk alles uitvoerig getest dmv benchmarks en profiling voordat ik alles definitief implementeerde :)

[Reactie gewijzigd door wootah op 8 mei 2012 19:53]

Nadeel is, er ontstaat snel verwarring met zo'n array
Als OO programmeur wrap je die array toch als private variable en noem je hem bv PixelManager of ImageGrid met een methode get(RGB,x,y) of getRGB(x,y) welke bij aanroep zo'n object maakt en teruggeeft. ben je even ver en heb je ook meteen de memory layout ge-encapsuleerd: zo kan je er voor kiezen om te optimaliseren richting concurrency (bv vanwegen cache line contention) of bijna gegarandeerde prefech vanwegen sequential layout + sequential call -> altijd hete caches.

c++, c# of java maakt hier niet zo veel uit. Unsafe is om boundschecking te omzeilen welke je ook in java hebt echter een goede JIT(of eerder) zou deze al moeten omzeilen.

overigens vind ik het altijd vaag als mensen c/c++/c# groeperen - afgezien van de naam heeft c# net zo veel te maken met c en/of c++ als java.
Heb je per pixel een mogelijk andere kleur implementatie? Zo nee, dan kun je natuurlijk nog steeds een bytearray gebruiken. Het enige wat je dan hoeft te weten is hoeveel bytes er per pixel gebruikt worden en wat de breedte van het plaatje is. De manier waarop de kleur geencode is kun je vervolgens als een helper/handler toevoegen.
struct in C is niet hetzelfde als struct in C++. struct in C++ is een klasse en in C heb je geen klasse's. Het verschill tussen struct en een class in C++ is dat members in een class standaard private is (of juist public), tenzij expliciet aangegeven. Voor de rest is een struct en class hetzelfde in C++.

Allemaal leuk en aardig als die optimalisatie, maar mijn ervaring (welke ik waardevol vind), is eerst te concentreren op je doel. Heb je je doel bereikt, pas dan kun je concentreren op optimalisatie. Als je je doel hebt bereikt kun je ook een hardcore geek inhuren, zodat jij verder je doel verbetert.
Als ik naar die shootout kijk, was C++ zowel qua geheugengebruik als qua snelheid een betere keus geweest.
Jup, bovendien heb je nog de kans om ansi c te gebruiken, die af en toe nog net ff wat sneller is :D
C++ is best wel een hippe taal, en ik programmeer er graag in. Met C++11 is het helemaal genieten, maar de compilers ondersteunen het allemaal nog niet helemaal |:(

En ja, fortran is nog steeds koning... maar dat is volgens mij alleen voor echte high performance code aan te raden :+
Die rangorde gaat alleen op voor computationally expensive loads.

http://beza1e1.tuxen.de/articles/faster_than_C.html geeft aardige overview met breed begrijpbare voorbeelden

voor andere loads waar concurrency een rol speeld zijn best wat aanpakken die gewoon niet werken bij die talen omdat het ultra traag of idioot moeilijk wordt om te implementeren. denk bv aan runtime optimalisaties als lock elision wat allerlei deuren opent.
Whether you use Java or C is not always as important as the algorithm you use. Being smarter can make more difference.
http://java.dzone.com/articles/java-can-be-significantly

[Reactie gewijzigd door Mr_Light op 10 mei 2012 04:39]

Om een praktijkvoorbeeld van je stelling te geven; wie het wil proberen kan eens naar JDownloader kijken.
De 0.x versie is verschrikkelijk log; gebruikt heel veel resources en start ontzettend traag op.
De nieuwe beta van 2.0 echter is voor een heel groot deel herschreven (daarom mist er ook nog veel functionaliteit en worden er dagelijks bugs geplet), en is veuuuuul sneller en lichter dan zijn voorganger.
We kunnen ervan uitgaan dat Java een taal is die gewoon makkelijk slecht gebruikt kan worden. Daar zal dat idee uit voortkomen. Als je weet wat je ermee kan en moet doen, lijkt het me een krachtige oplossing.
Maar uiteindelijk gaat het erom dat je de dingen in moet zetten waar ze voor bedoeld zijn.

[Reactie gewijzigd door Buggle op 8 mei 2012 10:56]

Java is een belachelijk snelle taal, doet in nagenoeg alles behalve low-level number crunching niet onder voor een 'native' taal. Zeker qua intensief geheugengebruik en concurrent applicaties - zoals in deze toepassing - is Java een veel betere keuze dan bijv. C++. Dat Java niet bekend staat als een snelle taal is iets van tien jaar geleden, toen het idd nog niet zo snel was. Ook zijn Java GUI applicaties niet bepaald responsief, maar dat komt waarschijnlijk vanwege een te zware architectuur oid.
Niet om een hele programmeertaal oorlog te beginnen, maar: nieuws: Xamarin ontwikkelt port van Android naar C#
En dan is mono nog langzamer dan microsoft .net :D
Door de hedendaagse verbeteringen van processorkracht maakt het steeds minder uit blijkbaar.

edit: oké oké het is inderdaad een andere vm, maar die is wel grotendeels gebaseerd op de java vm als ik het zo begrijp.

Destijds is voor java gekozen, met een aantal verschillende redenen lijkt me. Zo moest het sowieso sneller zijn dan php, dat is niet zo moeilijk gelukkig :+
Maar ik denk dat ook qua toekomst is gekezen, java had vooral rond die tijd waarschijnlijk een veel duidelijker toekomst beeld dan mono, en als bedrijf zet je daar op in lijkt me :)

[Reactie gewijzigd door wootah op 8 mei 2012 12:08]

Let wel dat in het artikel dat je aanhaalt niet de snelheid of traagheid van Java ter discussie staat, maar juist die van de Dalvik VM. Het lijkt me sterk dat Tweakers deze VM gebruikt voor hun Java-processen. ;)
Informatief en goed artikel. Ik kan zelf ook nog wel aanraden om, mocht je bvb toch in een krappe memory omgeving werken bvb nog op 32bit productiemachines (triest, ik weet het) moeten werken waar je af en toe toch grotere hoeveelheden gegevens in memory nodig hebt en niet op voorhand weet hoeveel, kan je beter gebruik maken van een linkedlist, kost extra geheugen qua datastructuren maar er moet niet 1 blok continuous gealloceerd worden.

Als je dan bvb een blok van 100mb hebt, en je moet plots een dubbel zo grote arraylist hebben gaat hij intern een dubbel zo groot blok alloceren en dan de bestaande data overkopieren waardoor er tijdelijk 300mb in use is, of je in het ergste geval een out of memory kan krijgen.
Een linked list kost niet alleen extra geheugen, maar is ook langzamer om te doorlopen. In vergelijking met een array vele malen langzamer zelfs. Wanneer je dit gewoon nodig hebt omdat je programma anders helemaal niet draait is de trade-off uiteraard acceptabel, maar in het algemeen zal een trip naar de hardwareboer toch een betere investering zijn.

Een alternatief is niet een standaard linked list te gebruiken maar één die in chunks werkt -- dus niet elk elementje zijn eigen node, maar arrays in een node. Hiermee reduceer je de overhead van de list maar hou je de mogelijkheid extra elementen toe te voegen zonder te moeten kopiëren. Dit vergt wel codeerwerk als nog niet iemand een library hiervoor gemaakt heeft in jouw taal, natuurlijk.

De extreme vorm hiervan is maar één zo'n chunk hebben en geen list. 200 MB array up-front vragen en die gebruiken tot het op is. Hiermee zet je geheugenbeheer buiten spel en het is dus ook nog eens lekker snel. Nadeel: dit werkt niet goed als je heel veel verschillende soorten data hebt (je kunt niet voor alles 200 MB gaan reserveren), het programmeert niet fijn omdat je zelf het gebruik bij moet houden en ingebouwde limieten (zelfs als ze niet hard-coded zijn) zijn natuurlijk de pest om te beheren en onderhouden ("er is nog geheugen over, waarom doet het programma het niet?"/"ik wil alleen maar een bestand van één regeltje openen, waarom doet het programma het niet?")
Als je in een (Java) de iterator gebruikt (of de for-each loop), dan is loop-en over de LinkedList net zo efficient (nl O(n)) als over een array (of ArrayList). Als je iedere keer get(index) gebruikt dan is het inderdaad O(n2)
LinkedList is waarschijnlijk in de praktijk iets minder efficient ivm het steeds moeten volgen en bijwerken van referenties naar objecten. Terwijl de ArrayList-iteratie een array-pointer heeft en een integer die geincrement wordt.
De ArrayList is waarschijnlijk net iets cache-vriendelijker ivm het netjes op een rij staan van de benodigde referenties, waardoor het opvragen van de referentie naar een volgend element meer kans heeft om (bijv door een pre-fetch) al in het cache-geheugen van de cpu te zitten.

Daarentegen is iterator.remove() weer veel goedkoper bij de LinkedList omdat er enkel maar twee Nodes aangepast hoeven te worden, ipv een arraycopy om het verwijderde element te overschrijven... maar het is in de praktijk nog best lastig goed te voorspellen welke van de twee het snelst zal zijn in zo'n situatie. Vergeet niet dat de ene O(n) de andere niet is (bijvoorbeeld omdat het in het ene geval n * 1ms is en in het andere geval n * 100ms).

Het is allemaal in dit geval nogal theoretisch geneuzel en het is goed mogelijk dat de overhead van beide iterators zo marginaal is vergeleken met je algoritme, dat je beter daar op kan concentreren :)

Ik zou dan ook vooral een List-implementatie kiezen die het best bij je requirements past (bijv. efficiente random-access of juist goedkope iteratie-deletes).
En als beperkt geheugengebruik een eis is, dan is een ArrayList die verder niet meer aangepast wordt en direct met de goede grootte was geconstrueerd de beste keuze :P

't Is overigens wat mij betreft de eerste keuze voor een List, tenzij je toevallig de andere eigenschappen van een LinkedList goed kunt gebruiken (bijv omdat je 'm als een Deque, Queue of Stack wilt gebruiken).
Het is zelfs tegenwoordig zo, dat zelfs met relatief kleine lijst grotes array lists sneller zijn bij random insert en delete operaties dan linked lists. Dat komt voornamelijk door de caches in de cpu's. Als je gehele arraylist makkelijk in de cache past, dan is het invoegen van een element ergens random in de lijst sneller ondanks dat de helft van de array opgeschoven moet worden dan met een linkedlist, waarbij om de goede node te vinden al misschien vele memory operaties gedaan moeten worden omdat alle nodes random in het geheugen verspreid staan. Elke geheugen actie kost in de orde van 100 keer zoveel tijd dan een normale cache hit. Daarom dat tegenwoordig in bijna alle gevallen een array list beter performed dan linked lists.
Leuk artikel, maar zoals elhopo ook aangeeft toch mijn bedenkingen naar lange termijn visie...

Hoe is daarmee rekening gehouden?
Deze omgeving bevat nu artikelen en producten die in de afgelopen 10 jaar zijn verzameld... en dat binnen een geheugengebruik dat nu goed in het RAM van een eenvoudige workstation past (ik gebruik zelf een jvm met 4GB heap). Dus we verwachten dat het nog wel even duurt voor het onpraktisch wordt om voor dit doel RAM-geheugen te gebruiken.

Wel willen we tzt nog een stuk gegevens toevoegen wat wel aanzienlijk meer is dan die paar GB, dat komt waarschijnlijk in een normale on-disk lucene-index voor het doorzoeken en verder de reeds bestaande database als bron voor de objecten zelf en wellicht een eenvoudige cache-laag die slechts de hot-items in geheugen houdt.

Puur qua interfaces en opzet zou dat allemaal vrij goed binnen de huidige omgeving moeten passen. Maar er zitten vast nog haken en ogen aan om door te voeren :P
Ik vraag me af hoe nuttig het is om optimalisaties te doen speciaal voor ontwikkelomgevingen? Waarom zou je niet 1/10de of 1/5de van de data kunnen gebruiken voor de ontwikkeling? Of geef iedereen een snelle SSD en benader vanaf daar de data :p

Zelf heb ik dit soort dingen (weliswaar met een stuk minder data) opgelost met Lucene Solr maar daar had ieder document inderdaad dezelfde structuur, ik kan me voorstellen dat het lastiger wordt als ieder document er anders uit ziet.

Misschien wel leuk om deze back-end te open-sourcen? O-)

[Reactie gewijzigd door Ramon op 8 mei 2012 10:20]

Je kan je natuurlijk afvragen of een Count dan nog nuttig is. Vraag het 1 x per dag uit en ververs het een paar keer per dag oid zonder het voor elke request te doen.

Dit is een geval van: Deze data hoeft niet perse 100% te kloppen, als er 2 of 3 producten meer in staan dan de count laat zien maakt voor de gebruiker niet uit.
Helaas, zo makkelijk kom je er niet van af. Als je een filter aanbiedt dan verwachten gebruikers ook dat ie iets nuttigs doet en dat ie doet wat ie suggereert.

Je zou dus sowieso al geen filter moeten tonen dat na activatie alle producten uitsluit (oftewel, als de count = 0, dan moet ie sowieso afvallen).
En om duidelijker te maken hoe nuttig een filter is wilden we sowieso graag cijfertjes erbij gaan tonen om aan te geven hoeveel resultaten er over blijven, maar als je dat doet... dan verwachten gebruikers natuurlijk wel dat ze ook daadwerkelijk kloppen. Bij 100 of 110 zal een gebruiker het nog niet zo erg vinden, maar op het moment dat het 1 of 7 is, dan wordt het een ander verhaal.

Maar de accuraatheid in het algemene, ongefilterde geval is inderdaad verder wel op te lossen. Waar het moeilijker wordt, is op het moment dat er een filter geactiveerd wordt. Stel dat het prijsfilter van >= 0 naar >= 100 wordt gesleept, dan verwacht een gebruiker natuurlijk wel dat van de overige filters dan "nuttigheidsindicatie" (die aantallen) worden bijgewerkt om hem opnieuw te helpen in te schatten hoe zinvol het gaat zijn om er een tweede (of derde, of vierde...) filter bij te activeren.

En je kan inderdaad afvragen of dat allemaal wel nodig is... maar wij hebben het dus voor elkaar gekregen dat het ruim snel genoeg precies de correcte cijfertjes oplevert. Daarbij kunnen we zelfs de filters sorteren op de populariteit van de erbij horende content (zodat de top-10 dat direct in beeld staat hopelijk wat nuttiger is, ipv alfabetische sortering), ook als een deel is afgevallen door andere filters.
Met de huidige filters in de pricewatch is eigenlijk niet te zien wat een combinatie van opties je gaat opleveren, met de nieuwe kan je daar veel eenvoudiger inzicht in krijgen. En dat maakt 'm net dat beetje nuttiger en gebruiksvriendelijker :)

Kortom, het is goed mogelijk dat het allemaal niet echt nodig is voor de gebruiker. Maar als het wel kan, dat de gebruiker er wel degelijk baat bij heeft. En het kon blijkbaar ;)

Al deze geheugen-ideeen waren daar overigens niet strict noodzakelijk voor, dat was eigenlijk meer bijzaak. Denk dan aan: "het is wel nuttig qua cpu-performance, ondanks dat het niet nodig is" (de trove-containers besparen enorm op autoboxing, de strings spugen we meestal toch in byte-vorm uit, etc), "het stelt de eenvoudige verticale schalingsplafonds weer jaren uit", maar ook: "omdat het kan", "het is leuk om te onderzoeken", "je leert er de taal en werking van de jvm beter van kennen", etc :)

[Reactie gewijzigd door ACM op 9 mei 2012 08:10]

Je laatste alinea snap ik helemaal. :)

Wat ik zelf enorm interresant vind is de afweging tussen het per request actueel zijn van de gegevens en het werk dat dit oplevert of het niet helemaal actueel zijn van deze gegevens en of dit voldoende is.
Ik nam altijd aan dat de empty string sowieso geinterned was in Java. Maar dat is dus niet zo? Waarom niet eigenlijk?
Nee, alle strings die je in code definieert zijn allemaal interned. Maar alles wat nieuw gemaakt wordt, ongeacht de lengte van de string, niet.

Het zou natuurlijk kunnen dat er ergens een JVM is die wel de lege String altijd interned, maar ik gok dat er dan wel hier en daar een applicatie stuk gaat die per ongeluk een == ipv equals-vergelijking doet en zich dan ineens anders gaat gedragen.
+1Anoniem: 84766
8 mei 2012 10:06
Interessant artikel. Ik weet niet of Java inmiddels ook structs ondersteunt, maar hier kan ook nog een winst gehaald worden bij slim gebruik. Het voordeel is dat een struct in zijn geheel op de stack wordt aangemaakt, waarbij een object op de heap wordt aangemaakt met een reference naar het object op de stack.
't Staat de JVM vrij om met (cpu-)stacks of heaps te werken natuurlijk. Als ie voor zichzelf kan bewijzen dat een object daadwerkelijk niet de scope van een methode verlaat zou ie prima mogen besluiten dan een stukje stack ervoor te gebruiken.

Overigens heeft de heap van java weer als voordeel dat het met de garbage collector een aantal handige performance-trucjes kan uithalen. De precieze uitleg is me ondertussen alweer ontschoten, maar het is in een aantal gevallen mogelijk dat objecten die een heel kort leven hebben uiteindelijk bij een bepaalde simpele check al niet gekopieerd (naar een ander deel van de heap) worden en daardoor ook niet expliciet gefreed hoeven te worden. Dan wordt vervolgens een heel stuk geheugen als geheel leeggegooid en daarmee wordt dan het weggooien van kleine objectjes ineens heel goedkoop.

Op dit item kan niet meer gereageerd worden.


Nintendo Switch (OLED model) Apple iPhone 13 LG G1 Google Pixel 6 Call of Duty: Vanguard Samsung Galaxy S22 Garmin fēnix 7 Nintendo Switch Lite

Tweakers vormt samen met Hardware Info, AutoTrack, Gaspedaal.nl, Nationale Vacaturebank, Intermediair en Independer DPG Online Services B.V.
Alle rechten voorbehouden © 1998 - 2022 Hosting door True