Cookies op Tweakers

Tweakers maakt gebruik van cookies, onder andere om de website te analyseren, het gebruiksgemak te vergroten en advertenties te tonen. Door gebruik te maken van deze website, of door op 'Ga verder' te klikken, geef je toestemming voor het gebruik van cookies. Je kunt ook een cookievrije versie van de website bezoeken met minder functionaliteit. Wil je meer informatie over cookies en hoe ze worden gebruikt, bekijk dan ons cookiebeleid.

Meer informatie

Door , , 138 reacties, 132.224 views •

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."


Door Arjen van der Meijden

- Lead Developer

In oktober 2001 begonnen met als voornaamste taak het technisch beheer van het forum. Daarna doorgegroeid tot senior developer en softwarearchitect. Nu lead developer, met een leidinggevende taak binnen het team van programmeurs en systeembeheerders van Tweakers.

Reacties (138)

Reactiefilter:-11380136+1125+232+33
Moderatie-faq Wijzig weergave
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?
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
~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.
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.



LG G4 Battlefield Hardline Samsung Galaxy S6 Edge Microsoft Windows 10 Samsung Galaxy S6 HTC One (M9) Grand Theft Auto V Apple iPad Air 2

© 1998 - 2015 de Persgroep Online Services B.V. Tweakers vormt samen met o.a. Autotrack en Carsom.nl de Persgroep Online Services B.V. Hosting door True