Let op: Tweakers stopt per 2023 met Tweakblogs. In
dit artikel
leggen we uit waarom we hiervoor hebben gekozen.
De snelste manier om een array te prefix in php
Voor het ontwikkelen van een grote applicatie op mijn werk was ik benieuwd naar de performance van php met betrekking tot het prefixen van waarden in array. Je kunt wel zeggen dat dit valt onder premature optimalisation, maar met zicht op de verschillen denk ik wel dat het handig is er rekening mee te houden vanaf het begin.
De tests zijn gedaan door een array te vullen met 30.000 integers en deze te prefixen met de string 'prefix'. Voor deze testcase zijn dit de resultaten, met daaronder de code:
Duidelijk is dus dat het gebruik van de wazige constructie implode/explode het snelst is, maar wel wat extra geheugengebruik heeft t.o.v. de for-constructie. *!*
Array_walk is veruit de traagste functie met het meeste geheugengebruik, iets wat je opzich niet zou verwachten.
Qua geheugengebruik is de for-functie veruit superieur, en qua snelheid doet hij niet veel onder voor de explode/implode foreach met reference.
Tenzij je echt met enorme arrays gaat werken maken deze optimalisaties natuurlijk niet echt uit, maar wel verwonderlijk de verschillen te zien
*!* Na een opmerking van Erkens bleek dat niet de explode/implode-methode het snelst is, maar een foreach met een reference naar het object. De implode/explode volgt als tweede, en de for-loop is de nummer 3. Een iets andere conclusie dus
Hieronder de code, niet de mooiste, maar het was natuurlijk maar om te testen:
De tests zijn gedaan door een array te vullen met 30.000 integers en deze te prefixen met de string 'prefix'. Voor deze testcase zijn dit de resultaten, met daaronder de code:
| Test | Tijd | Extra geheugengebruik |
|---|---|---|
| Array_walk met method | 0.0266440 | 2,097,152B |
| Explode / implode | 0.0109251 | 786,432B |
| For-loop | 0.0115969 | 0B |
| For-loop met method | 0.0215051 | 0B |
| Foreach | 0.0135100 | 1,048,576B |
| Foreach met reference | 0.0100880 | 0B |
Duidelijk is dus dat het gebruik van de wazige constructie implode/explode het snelst is, maar wel wat extra geheugengebruik heeft t.o.v. de for-constructie. *!*
Array_walk is veruit de traagste functie met het meeste geheugengebruik, iets wat je opzich niet zou verwachten.
Qua geheugengebruik is de for-functie veruit superieur, en qua snelheid doet hij niet veel onder voor de explode/implode foreach met reference.
Tenzij je echt met enorme arrays gaat werken maken deze optimalisaties natuurlijk niet echt uit, maar wel verwonderlijk de verschillen te zien
*!* Na een opmerking van Erkens bleek dat niet de explode/implode-methode het snelst is, maar een foreach met een reference naar het object. De implode/explode volgt als tweede, en de for-loop is de nummer 3. Een iets andere conclusie dus
Hieronder de code, niet de mooiste, maar het was natuurlijk maar om te testen:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
| $aItems = array_fill(0, 30000, 1234); // create array with 30k items $sPre = 'prefix'; function test1(&$s, $k, $sPre) { $s = $sPre.$s; } function test4($s, $sPre) { return $sPre.$s; } // TEST 1 - Array_Walk to prefix $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); array_walk($aItems, 'test1', $sPre); echo '1: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (array_walk) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; // reset $aItems = array_fill(0, 30000, 1234); // create array with 30k items // TEST 2 - implode/explode to prefix $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); $aItems = explode('|', $sPre.implode('|'.$sPre, $aItems)); echo '<br />2: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (explode/implode) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; // reset $aItems = array_fill(0, 30000, 1234); // create array with 30k items // TEST 3 - for loop $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); $iCount = count($aItems); for($i = 0; $i < $iCount; $i++) { $aItems[$i] = $sPre.$aItems[$i]; } echo '<br />3: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (for) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; // reset $aItems = array_fill(0, 30000, 1234); // create array with 30k items // TEST 4 - for loop with method call $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); $iCount = count($aItems); for($i = 0; $i < $iCount; $i++) { $aItems[$i] = test4($aItems[$i], $sPre); } echo '<br />4: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (for - method) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; // reset $aItems = array_fill(0, 30000, 1234); // create array with 30k items // TEST 5 - foreach loop $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); $aNew = array(); foreach($aItems as $sKey => $iItem) { $aNew[$sKey] = $sPre.$iItem; } echo '<br />5: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (foreach) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; // reset $aItems = array_fill(0, 30000, 1234); // create array with 30k items // TEST 6 - foreach loop - own array - reference $iMemBefore = memory_get_usage(true); $iTmp = microtime(true); // // Note the & // // foreach($aItems as $sKey => &$iItem) { $iItem = $sPre.$iItem; } echo '<br />6: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (foreach - own array - reference) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B'; |
Reacties
Wat voor use-case heb je eigenlijk dat je zoiets als dit daadwerkelijk nodig hebt? Verder ben ik blij dat de methode die ik standaard genomen zou hebben (die for-loop) ook zo'n beetje het snelst en efficientst is 
[Reactie gewijzigd op maandag 10 januari 2011 15:46]
Mijn servertje geeft net eventjes andere resultaten:
1: 0.4804060 (array_walk) - Mem 2,359,296B
2: 0.0476279 (explode/implode) - Mem 1,048,576B
3: 0.1764009 (for) - Mem 0B
4: 0.5035262 (for - method) - Mem 262,144B
5: 0.1672239 (foreach) - Mem 1,835,008B
(Fedora 14, HTTPD2.2.17, PHP5.3.4, APC 3.1.6)
Ik verbaas mij er vooral over dat de explode de snelste is, alhoewel dit dus niet in elke usecase werkt. Optie 5 is trouwens een andere test, omdat je een nieuwe array gebruikt. Geen wonder dat de memory overhead daar het grootst is natuurlijk :-)
1: 0.4804060 (array_walk) - Mem 2,359,296B
2: 0.0476279 (explode/implode) - Mem 1,048,576B
3: 0.1764009 (for) - Mem 0B
4: 0.5035262 (for - method) - Mem 262,144B
5: 0.1672239 (foreach) - Mem 1,835,008B
(Fedora 14, HTTPD2.2.17, PHP5.3.4, APC 3.1.6)
Ik verbaas mij er vooral over dat de explode de snelste is, alhoewel dit dus niet in elke usecase werkt. Optie 5 is trouwens een andere test, omdat je een nieuwe array gebruikt. Geen wonder dat de memory overhead daar het grootst is natuurlijk :-)
De enige reden dat de foreach geheugen gebruikt en waarschijnlijk iets trager is, komt doordat een nieuwe array gevuld wordt. Wellicht interesant om te weten is wat er gebeurd als je daar de originele array gebruikt.
1: 0.0208831 (array_walk) - Mem 2,097,152B
2: 0.0096881 (explode/implode) - Mem 1,048,576B
3: 0.0107551 (for) - Mem 0B
4: 0.0206621 (for - method) - Mem 0B
5: 0.0140960 (foreach) - Mem 1,572,864B
2: 0.0096881 (explode/implode) - Mem 1,048,576B
3: 0.0107551 (for) - Mem 0B
4: 0.0206621 (for - method) - Mem 0B
5: 0.0140960 (foreach) - Mem 1,572,864B
Met een ordegrootte 30k heb ik natuurlijk niet te maken, maar ik zal even uitleggen waar ik deze methode voor wil gebruiken:ACM schreef op maandag 10 januari 2011 @ 15:45:
Wat voor use-case heb je eigenlijk dat je zoiets als dit daadwerkelijk nodig hebt? Verder ben ik blij dat de methode die ik standaard genomen zou hebben (die for-loop) ook zo'n beetje het snelst en efficientst is
- Een grote plaatjes/videosite met > 140k users en flink verkeer
- De viewcounters worden bijgehouden in memcache of APC user cache
- De keys worden opgebouwd alsvolgt: 'counter:view:image:<id>' met een integerwaarde.
- Er is een key/waarde-paar die bijhoudt welke id's er zijn veranderd (views).
Nu ik er nog eens over nadenk hoeft het opzich zo niet, maar kan ik een grote array in memcache gooien, maar omdat ik op het moment van testen nog niet echt een idee had hoe groot die array zou worden moest ik het limiet van memcache niet bereiken (1 MB). Zo te zien is de grootte van de array dan beperkt tot 30k items. Met een cronjob die redelijk vaak de gegevens wegschrijft naar de database moet dat natuurlijk wel lukken, zelfs op piekmomenten.
Dus ik denk zelfs dat deze test achteraf gezien nieteens echt terugkomt op de site, maar wel een keer goed om te testen natuurlijk
Zelf geniet de for-loop ook mijn voorkeur overigens.
Ik maak bij de foreach expres gebruik van een extra array omdat dat (iirc) sneller is dan de array die je loopt te manipuleren. Als ik het me goed herinner kan dat zelfs niet in een foreach..RSpliet schreef op maandag 10 januari 2011 @ 16:46:
Ik verbaas mij er vooral over dat de explode de snelste is, alhoewel dit dus niet in elke usecase werkt. Optie 5 is trouwens een andere test, omdat je een nieuwe array gebruikt. Geen wonder dat de memory overhead daar het grootst is natuurlijk :-)
Volgens mij wordt ie dan nog trager (als het al dezelfde output geeft), maar dat zou leuk zijn om nog even te testen inderdaad. Wellicht dat ik dat morgen nog even doeSalandur schreef op maandag 10 januari 2011 @ 16:48:
De enige reden dat de foreach geheugen gebruikt en waarschijnlijk iets trager is, komt doordat een nieuwe array gevuld wordt. Wellicht interesant om te weten is wat er gebeurd als je daar de originele array gebruikt.
[Reactie gewijzigd op maandag 10 januari 2011 17:14]
Onthoud dat bij zulke experimenten het van belang is dat je meegeeft op welk platform dit is en met welke processor. Aangezien bepaalde CPUs beter op bepaalde operaties zijn voorzien (lees: s390 met een circular buffer outperformed alle x86s in dat forloopje bijvoorbeeld). Je kan inderdaad niet een array loopen met foreach en deze tegerlijkertijd manipuleren, lijkt mij obvious waarom.
react@Phoenix1336: Ah my bad, in vele talen kan dat niet, probleem geval is als je een element toevoegt of verwijdert terwijl hij aan het loopen is. Ik gok dat het een design probleem is op het interpreter vs compiler niveau. Someone enlighten us why.
react@Phoenix1336: Ah my bad, in vele talen kan dat niet, probleem geval is als je een element toevoegt of verwijdert terwijl hij aan het loopen is. Ik gok dat het een design probleem is op het interpreter vs compiler niveau. Someone enlighten us why.
[Reactie gewijzigd op maandag 10 januari 2011 18:11]
De test is gedaan op een CentOS 5.5-bak met een dual Xeon configuratie met 4 GB ram. Typenummer weet ik zo even niet uit mijn hoofd 
Ehm, dat werkt gewoon hoor?analog_ schreef op maandag 10 januari 2011 @ 17:31:
. Je kan inderdaad niet een array loopen met foreach en deze tegerlijkertijd manipuleren, lijkt mij obvious waarom.
<?php
$aItems = array_fill(0, 30000, 1234); // create array with 30k items
$sPre = "prefix";
$iMemBefore = memory_get_usage(true);
$iTmp = microtime(true);
foreach($aItems as $sKey => $iItem) {
$aItems[$sKey] = $sPre.$iItem;
}
echo '<br />5: '. str_pad(round((microtime(true) - $iTmp), 7), 9,'0') . ' (foreach) - Mem '.number_format((memory_get_usage(true) - $iMemBefore)).'B';
echo "<pre>";
var_dump($aItems);
?>
Mijn devserver geeft dit:
1: 0.0499940 (array_walk) - Mem 2,097,152B
2: 0.0263059 (explode/implode) - Mem 1,310,720B
3: 0.0261610 (for) - Mem 0B
4: 0.0506029 (for - method) - Mem 0B
5: 0.0303259 (foreach) - Mem 524,288B
Ik heb meteen een extra test toegevoegd, welke ik nog wel eens wil gebruiken:
PHP:
6: 0.0254430 (foreach) - Mem 0B
Is bij mij ook meteen het snelste
Hoewel na een paar keer uitvoeren merk je dat er weinig verschil in zit. Overigens had ik ook op gegeven moment een negatief geheugenverbruik, dus de meetmethode is niet geheel handig zo.
1: 0.0499940 (array_walk) - Mem 2,097,152B
2: 0.0263059 (explode/implode) - Mem 1,310,720B
3: 0.0261610 (for) - Mem 0B
4: 0.0506029 (for - method) - Mem 0B
5: 0.0303259 (foreach) - Mem 524,288B
Ik heb meteen een extra test toegevoegd, welke ik nog wel eens wil gebruiken:
PHP:
1
2
3
| foreach($aItems as &$iItem) { $iItem = $sPre.$iItem; } |
6: 0.0254430 (foreach) - Mem 0B
Is bij mij ook meteen het snelste
Als je een array van 30k items opslaat in memcached gok ik dat je niet de meest efficiente aanpak hebt gekozenkrvabo schreef op maandag 10 januari 2011 @ 17:07:
Nu ik er nog eens over nadenk hoeft het opzich zo niet, maar kan ik een grote array in memcache gooien, maar omdat ik op het moment van testen nog niet echt een idee had hoe groot die array zou worden moest ik het limiet van memcache niet bereiken (1 MB). Zo te zien is de grootte van de array dan beperkt tot 30k items. Met een cronjob die redelijk vaak de gegevens wegschrijft naar de database moet dat natuurlijk wel lukken, zelfs op piekmomenten.
Afgezien daarvan zou ik je dan willen aanbevelen om goed te kijken naar je serializer. PHP's serializer is niet bepaald efficient in datagrootte, json levert veel kleinere strings op (en igbinary serialization nog weer kleiner). Bij de pecl memcached (met een d aan het eind) kan je dat instellen.
Klopt, het geheugengebruik is geloof ik (pin me er niet op vast) ook afhankelijk van andere php-pagina's die ondertussen verwerkt worden.Erkens schreef op maandag 10 januari 2011 @ 18:19:
PHP:
1 2 3 foreach($aItems as &$iItem) { $iItem = $sPre.$iItem; }
Is bij mij ook meteen het snelsteHoewel na een paar keer uitvoeren merk je dat er weinig verschil in zit. Overigens had ik ook op gegeven moment een negatief geheugenverbruik, dus de meetmethode is niet geheel handig zo.
Aangezien mijn testbak verder niets deed was het wel een prima indicator.
Het aanpassen van items in een foreach kan afaik alleen op deze manier, even testen nog:
PHP:
1
2
3
| foreach($aItems as $sKey => $iItem) { $aItems[$sKey] = $sPre.$iItem; } |
Deze manier heb ik net even getest op dezelfde bak en dan is ie ongeveer even snel als de foreach me extra array:
1: 0.0268769 (array_walk) - Mem 2,097,152B
2: 0.0111661 (explode/implode) - Mem 1,048,576B
3: 0.0108960 (for) - Mem 0B
4: 0.0208778 (for - method) - Mem 0B
5: 0.0139101 (foreach) - Mem 1,310,720B
6: 0.0140769 (foreach - own array) - Mem 2,359,296B
Het verschilt overigens per pageload, de laatste twee wisselen elkaar dan af welke sneller is. Deze bak was verder helemaal absoluut niets aan het doen, dus wel een indicatieve maatstaf zo.
Overigens is het met een reference inderdaad nog sneller, sneller dan welke methode dan ook! Opvallend. Dan moet ik even de blogpost nog aanpassen
6: 0.0100880 (foreach - own array) - Mem 0B
.
Ik sla ook geen 30k items op in memcache, althans dat is niet de bedoeling, maar een array ter grootte van ~30k items zou dus het maximale zijn voor memcache (1 MB limit voor elke value). Een view is natuurlijk al erg snel gemaakt, 1 persoon kan in principe elke seconde wel een view maken als hij een fotoalbum zit te bekijken. Volgens mij zaten we op enkele honderdduizenden tot > 1m pageviews per dag. Ik moet in ieder geval niet eens per uur de waardes naar de database schrijven, want dan zit ik dus al wel aan die limiet. Dat zal natuurlijk in de praktijk veel vaker gebeuren.ACM schreef op maandag 10 januari 2011 @ 18:34:
[...]
Als je een array van 30k items opslaat in memcached gok ik dat je niet de meest efficiente aanpak hebt gekozenAlthans, er zijn waarschijnlijk maar weinig use cases waarbij het zin heeft om 30k items te groeperen in je caching-laag.
Afgezien daarvan zou ik je dan willen aanbevelen om goed te kijken naar je serializer. PHP's serializer is niet bepaald efficient in datagrootte, json levert veel kleinere strings op (en igbinary serialization nog weer kleiner). Bij de pecl memcached (met een d aan het eind) kan je dat instellen.
Het doel is in ieder geval om voor zulke triviale data de database niet te vaak lastig te vallen.
Dat laatste wat je zegt is een goed idee. Volgens mij gebruik ik echter de 'memcache'-versie van php en niet 'memcached'. Dit moet ik even nazoeken. Bedankt voor de tip in ieder geval
[Reactie gewijzigd op maandag 10 januari 2011 18:55]
Houdt er wel rekening mee dat die tests geen atomaire operaties zijn, dus het kan best zijn dat de CPU even een context switch doet, waardoor je meetgegevens al niet meer betrouwbaar zijn. Wat je kunt doen is de test uitbreiden dat je elke onderlinge test 10000 keer uitvoert en dan het gemiddelde van elke test neemt.
Ah, we hebben weer een Power-of-Ten syndroompjeMeijuh schreef op maandag 10 januari 2011 @ 19:21:
Wat je kunt doen is de test uitbreiden dat je elke onderlinge test 10000 keer uitvoert en dan het gemiddelde van elke test neemt.
Programmers Need To Learn Statistics Or I Will Kill Them All
[Reactie gewijzigd op maandag 10 januari 2011 19:41]
10^x : #carebears is niet van toepassing in PHP omdat het scripted is, maw. de startup time is er altijd en daarin zijn we geïnteresseerd, niet wat er zou gebeuren als je het script viertien dagen door draaien.RobIII schreef op maandag 10 januari 2011 @ 19:39:
[...]
Ah, we hebben weer een Power-of-Ten syndroompje
Programmers Need To Learn Statistics Or I Will Kill Them All
Sowieso hoor je een oplossing als 'implode/explode' niet eens te proberen:
1) De oplossing is niet universeel bruikbaar, je implode character moet namelijk uniek zijn;
2) De oplossing kost een geheugen waar je U tegen zegt;
3) Je verliest de key.
array-walk en for loop met method hebben een extra (onoverzichtelijke) functie nodig, dus ik denk dat niemand dat als eerste oplossing bedenkt.
Daarnaast klinkt het geheel een beetje als een micro-optimisation. En als je alle onzinnige varianten wegstreept hou je dit over:
For-loop
0.0115969
Foreach
0.0135100
Foreach met reference
0.0100880
We hebben het hier blijkbaar over 30.000 iteraties. There is no way dat je zoveel iteraties in een web-request doet, tenminste ik kan me geen zinnige toepassing verzinnen. Laten we voor de lol zeggen dat we een achtergrond taak hebben die 3 miljoen iteraties moet doen. Dan hebben we het over een verschil in execute tijd van maar liefst 0.3 seconden in het geval dat je de "verkeerde" keuze had gemaakt.
Als het prefixen zo "lang" duurt waarom cache je het niet? Waarom zou je uberhaupt alle waarden in een array willen prefixen? Watvoor probleem wilde je oplossen?
Ps: Je foreach oplossing "zuipt" geheugen omdat je 2 arrays gebruikt. Beetje oneerlijk aangezien je dat bij alle andere voorbeelden niet doet. En als je echt memory freak bent, is unset ook je vriend.
Het is niet de bedoeling om hard over te komen, maar ik wil je alleen duidelijk maken dat elke seconde die je nog in deze manier van optimaliseren steekt een verloren seconde is.
1) De oplossing is niet universeel bruikbaar, je implode character moet namelijk uniek zijn;
2) De oplossing kost een geheugen waar je U tegen zegt;
3) Je verliest de key.
array-walk en for loop met method hebben een extra (onoverzichtelijke) functie nodig, dus ik denk dat niemand dat als eerste oplossing bedenkt.
Daarnaast klinkt het geheel een beetje als een micro-optimisation. En als je alle onzinnige varianten wegstreept hou je dit over:
For-loop
0.0115969
Foreach
0.0135100
Foreach met reference
0.0100880
We hebben het hier blijkbaar over 30.000 iteraties. There is no way dat je zoveel iteraties in een web-request doet, tenminste ik kan me geen zinnige toepassing verzinnen. Laten we voor de lol zeggen dat we een achtergrond taak hebben die 3 miljoen iteraties moet doen. Dan hebben we het over een verschil in execute tijd van maar liefst 0.3 seconden in het geval dat je de "verkeerde" keuze had gemaakt.
Als het prefixen zo "lang" duurt waarom cache je het niet? Waarom zou je uberhaupt alle waarden in een array willen prefixen? Watvoor probleem wilde je oplossen?
Ps: Je foreach oplossing "zuipt" geheugen omdat je 2 arrays gebruikt. Beetje oneerlijk aangezien je dat bij alle andere voorbeelden niet doet. En als je echt memory freak bent, is unset ook je vriend.
Het is niet de bedoeling om hard over te komen, maar ik wil je alleen duidelijk maken dat elke seconde die je nog in deze manier van optimaliseren steekt een verloren seconde is.
Een uniek karakter is snel genoeg gevonden, en aangezien mijn array bestaat uit enkel integers is elk alfa-teken goed. Als je kijkt naar de verschillende tests die hier o.a. in de comments staat zie je dat de implode/explode daadwerkelijk minder geheugen gebruikt dan een foreach met 2 arrays. De key verliezen was in mijn opzet geen probleem. Bij de eerste foreach had ik inderdaad ook deze key weg kunnen laten, maar deze tests zijn pas later toegevoegd omdat foreach me eigenlijk maar weinig kan deren als ik keys heb die ik kan gebruiken.ReenL schreef op maandag 10 januari 2011 @ 20:29:
Sowieso hoor je een oplossing als 'implode/explode' niet eens te proberen:
1) De oplossing is niet universeel bruikbaar, je implode character moet namelijk uniek zijn;
2) De oplossing kost een geheugen waar je U tegen zegt;
3) Je verliest de key.
Overigens was array_walk mijn eerste gedachte hierin omdat ik dacht dat het wellicht mogelijk was om het zonder function te doen, daarom ben ik gaan testenarray-walk en for loop met method hebben een extra (onoverzichtelijke) functie nodig, dus ik denk dat niemand dat als eerste oplossing bedenkt.
Dat staat ook in de tekst er boven. Vele malen kleine optimalisaties uitvoeren kan veel schelen. Ik zeg niet dat je achteraf al je loops aan moet gaan passen, maar als je hier vanaf het begin af aan rekening mee houdt dan heb je zonder extra moeite wel iets extra snelheid.Daarnaast klinkt het geheel een beetje als een micro-optimisation. En als je alle onzinnige varianten wegstreept hou je dit over:
Het gaat in principe over 1 array met 30k elementen. Normaal zul je het niet snel tegenkomen, maar in een achtergrondproces kan dat vrij simpel. Nu zeg je dat het 0.3 seconden per request scheelt, maar als je 5x dezelfde soort optimalisatie kunt doen dan scheelt het uiteindelijk best veel.We hebben het hier blijkbaar over 30.000 iteraties. There is no way dat je zoveel iteraties in een web-request doet, tenminste ik kan me geen zinnige toepassing verzinnen. Laten we voor de lol zeggen dat we een achtergrond taak hebben die 3 miljoen iteraties moet doen. Dan hebben we het over een verschil in execute tijd van maar liefst 0.3 seconden in het geval dat je de "verkeerde" keuze had gemaakt.
Dan moet ik overigens wel zeggen dat het op een dual xeon bak is getest die niets stond te doen. Als je dit soort optimalisaties doet op je shared hosting kan het best wel wat schelen.
Zoals ik al zei: ja, het geneuk in de marge, maar daar gaat het niet om. Dit is puur indicatief.
Zoals ik hierboven in een comment had geplaatst hield ik een lijst bij van x-aantal id's die zijn veranderd. Die id's worden geprefixt met een string die samen met het id leidt tot de _key_ van een memcached waarde. Het is dus al gecached.Als het prefixen zo "lang" duurt waarom cache je het niet? Waarom zou je uberhaupt alle waarden in een array willen prefixen? Watvoor probleem wilde je oplossen?
Memcache kan een maximum van 1 MB per waarde aan, dus hoe korter de waarde per element in de array, hoe meer er inpast. En aangezien de prefix overal hetzelfde is was dit dus een uitstekende reden om het te prefixen
De reden dat ik twee arrays heb gebruikt bij de eerste for-each test is dat ik zelf _nooit_ waardes van een array met een foreach aanpas. Dit omdat de waarde dan later in de loop anders kan zijn dan ik had verwacht en onduidelijk is waar de waarde vandaan komt (voor mij). Ik was overigens ook van mening dat het niet mogelijk was om de waarde aan te passen, maar door direct gebruik te maken van $aItems[$sKey] of de reference is het inderdaad wel mogelijk. Het blijft iets wat ik nooit zelf zou doen though.Ps: Je foreach oplossing "zuipt" geheugen omdat je 2 arrays gebruikt. Beetje oneerlijk aangezien je dat bij alle andere voorbeelden niet doet. En als je echt memory freak bent, is unset ook je vriend.
Overigens gebruik ik expres geen unset om te kijken hoeveel extra geheugen wordt gebruikt.
En toch maak ik dat lekker zelf uitHet is niet de bedoeling om hard over te komen, maar ik wil je alleen duidelijk maken dat elke seconde die je nog in deze manier van optimaliseren steekt een verloren seconde is.
Zoals ik al zei is het gewoon een kwestie van benchmarken, en zeker op een high-performance site kunnen al die kleine rotoptimalisaties een enorm verschil opleveren. Dit is dan ook geen optimalisatie achteraf, maar juist vooraf. Voor mij is duidelijk geworden wat een snellere methode is om iets te doen waardoor ik ook in het vervolg sneller voor de ene oplossing zal kiezen dan voor de ander.
Een beetje extra kennis is nooit weg, ook al is het maar een kleine winst
[Reactie gewijzigd op maandag 10 januari 2011 21:07]
Als je zoveel tijd besteed aan zo'n issue, heb je dan niet de verkeerde taal gekozen?
Desalniettemin blijft het geneuk in de marge. Je hebt grote kans dat e.e.a. uiteindelijk naar memcached sturen veel langer duurt dan wat je hier allemaal doet. En dat het dus uiteindelijk op de gehele procedure nog geen procent scheelt.krvabo schreef op maandag 10 januari 2011 @ 21:05:
Zoals ik al zei: ja, het geneuk in de marge, maar daar gaat het niet om. Dit is puur indicatief.
Zelf zou ik beginnen met de meest leesbare en natuurlijke variant. En dat is - voor mij - de for-loop of zelfs de foreach-variant.
Als je waarden van 1MB gebruikt in je cache... Dan betekent dat dat je later in het gebruiken van die records ook die 1MB data moet ophalen, unserializen en waarschijnlijk filteren. Je moet echt heel erg opletten met dat soort dingen, want het kan best zijn dat je alle winst van je cache daar compleet mee verliest.Memcache kan een maximum van 1 MB per waarde aan, dus hoe korter de waarde per element in de array, hoe meer er inpast. En aangezien de prefix overal hetzelfde is was dit dus een uitstekende reden om het te prefixen
Als je van die 30k elementen bijv 100 (steeds andere) nodig hebt, dan kan je wellicht beter alle 30k los cachen en met een multi-get de 100 keys die je nodig hebt opvragen.
Ik vind inderdaad ook dat een foreach niet de array hoort aan te passen. Zelfs met een gewone for-loop zou ik dat in dit soort contexten niet zo gauw doen.De reden dat ik twee arrays heb gebruikt bij de eerste for-each test is dat ik zelf _nooit_ waardes van een array met een foreach aanpas.
Je moet altijd naar je kritieke pad(en) kijken. Helaas zijn die met php wat lastig te achterhalen, maar het kan zomaar zijn dat het eigenlijk helemaal niet zoveel uitmaakt. 0.01% winst is ook voor een hele grote site nog steeds 0.01%Zoals ik al zei is het gewoon een kwestie van benchmarken, en zeker op een high-performance site kunnen al die kleine rotoptimalisaties een enorm verschil opleveren.
Als je code in een van je kritieke paden kunt versnellen, waardoor je ineens 5% winst boekt, dan is het natuurlijk absoluut het overwegen waard.
Als je toch meerdere "correcte" en leesbare varianten kent, dan kan je inderdaad net zo goed de meest efficiente pakkenDit is dan ook geen optimalisatie achteraf, maar juist vooraf. Voor mij is duidelijk geworden wat een snellere methode is om iets te doen waardoor ik ook in het vervolg sneller voor de ene oplossing zal kiezen dan voor de ander.
Prima, maar nu ben je appels met peren aan het vergelijken omdat je het bij de andere oplossingen niet doet.onduidelijk is waar de waarde vandaan komt
0.3 seconden op 3miljoen entries. En achtergrond taken kijken niet op 1,5 seconden.Nu zeg je dat het 0.3 seconden per request scheelt, maar als je 5x dezelfde soort optimalisatie kunt doen dan scheelt het uiteindelijk best veel.
Waarom zijn die key's geprefixed? En waarom doe je alles in het eerste loopje prefixen en waarschijnlijk in een volgend loopje uitlezen als het in één loopje kan?Die id's worden geprefixt met een string die samen met het id leidt tot de _key_ van een memcached waarde. Het is dus al gecached.
Oké als je het dan toch zo geweldig vind en houd van scheve vergelijkingen:
http://phpbench.com/
Kun je, jezelf nu bezig houden met het echte optimalisatie werk
Moch, het begon met een testje tussen twee manieren (array_walk, en de implode/explode) om eens voor de gein te kijken wat er nu sneller was en was echt niet bedoeld als full-blown benchmark. Het was nog een half uurtje tot het einde van de werkdag en heb na de testjes even alles op dit blog gezet om de test ook met andere mensen te delen die het wellicht ook wel grappig vonden om te zien dat een compleet implausibele en 'lelijke' oplossing sneller werkt dan de mooie for-loop.Olaf van der Spek schreef op maandag 10 januari 2011 @ 21:09:
Als je zoveel tijd besteed aan zo'n issue, heb je dan niet de verkeerde taal gekozen?
Het schrijven van het blog duurde al langer dan het schrijven van de test..
Het sturen naar memcache is inderdaad redelijk traag omdat het zelfs voor een lokale cache een tcp/ip-verbinding over tcp opzet. Ik geloof dat facebook ook wel udp in memcache heeft gebouwd, maar geen idee of dat zich al een weg terug heeft gevonden in memcache.ACM schreef op maandag 10 januari 2011 @ 21:37:
[...]
Desalniettemin blijft het geneuk in de marge. Je hebt grote kans dat e.e.a. uiteindelijk naar memcached sturen veel langer duurt dan wat je hier allemaal doet. En dat het dus uiteindelijk op de gehele procedure nog geen procent scheelt.
Zelf zou ik beginnen met de meest leesbare en natuurlijke variant. En dat is - voor mij - de for-loop of zelfs de foreach-variant.
Normaal houd ik me echt niet bezig met zulke tests, maar omdat ik verschillende oplossingen kon bedenken wilde ik het toch eens graag weten of de implode/explode zoveel trager zou zijn. Niet dus
Het punt is nu net dat die 30k stond voor de grootte van de array die bijhoudt wat de keys zijn die ik moet ophalen met multi-getAls je waarden van 1MB gebruikt in je cache... Dan betekent dat dat je later in het gebruiken van die records ook die 1MB data moet ophalen, unserializen en waarschijnlijk filteren. Je moet echt heel erg opletten met dat soort dingen, want het kan best zijn dat je alle winst van je cache daar compleet mee verliest.
Als je van die 30k elementen bijv 100 (steeds andere) nodig hebt, dan kan je wellicht beter alle 30k los cachen en met een multi-get de 100 keys die je nodig hebt opvragen.
True, maar het is wel zo dat 1.000.000x 0.1% meer uitmaakt dan 100x 0.1%.[...]
Je moet altijd naar je kritieke pad(en) kijken. Helaas zijn die met php wat lastig te achterhalen, maar het kan zomaar zijn dat het eigenlijk helemaal niet zoveel uitmaakt. 0.01% winst is ook voor een hele grote site nog steeds 0.01%
Precies. De reden dat ik deze blogpost heb gedaan is ook meer omdat ik het raar vond dat een rare oplossing als de implode/explode snéller is dan een nette for-loop. Dat had ik niet verwacht en wilde juist testen hoeveel langzamer / intensiever het zou zijn. Een onverwacht resultaat is voor mij altijd wel interessant.Als je toch meerdere "correcte" en leesbare varianten kent, dan kan je inderdaad net zo goed de meest efficiente pakken
En daarom heb ik nadat ik deze blogpost heb gemaakt en er op ben gewezen dat je wel waarden in een foreach kunt aanpassen de vergelijking heb toegevoegd en daadwerkelijk mijn conclusie heb aangepast.ReenL schreef op maandag 10 januari 2011 @ 21:50:
Prima, maar nu ben je appels met peren aan het vergelijken omdat je het bij de andere oplossingen niet doet.
nee natuurlijk niet, maar die extra anderhalve seconde heb je wel extra load op je systeem waardoor het laden van andere pagina's weer trager gaat. En natuurlijk is een array van miljoenen records gekkenwerk, het ging me meer om het idee dat het niet slecht is om vooraf rekening te houden met de prestaties van minder moeilijke algoritmes. Als je vooraf weet dat een bepaalde oplossing beter is (sneller, minder geheugen), ga je dan alsnog een trage functie gebruiken? Nee toch?0.3 seconden op 3miljoen entries. En achtergrond taken kijken niet op 1,5 seconden.
Dat heb ik al eerder uitgelegd: zodat de source array minder groot kan worden en in memcache zou passen. Dit is echter puur theoretisch en niet voor live gebruik. Als ik de prefix in de array zou plaatsen zou ik nog maar < 8k elementen in de array kunnen plaatsen ipv de ~30k nu. Puur theoretisch dus. En nee, hij wordt niet opnieuw uitgelezen maar gebruikt in een multi-get naar memcache gestuurd.Waarom zijn die key's geprefixed? En waarom doe je alles in het eerste loopje prefixen en waarschijnlijk in een volgend loopje uitlezen als het in één loopje kan?
Right, want wanneer je een test doet voor micro-optimalisatie ben je natuurlijk nooit bezig met grotere optimalisaties. Zoals ik al meerdere keren heb geplaatst is dit puur een theoretisch indicatieve test en houd ik me normaal echt niet bezig met dit soort gemierenneukOké als je het dan toch zo geweldig vind en houd van scheve vergelijkingen:
http://phpbench.com/
Kun je, jezelf nu bezig houden met het echte optimalisatie werk.
Logs van dit soort views ruikt bijna naar een mongoDB oplossing, waar je met een map/reduce script redelijk makkelijk statistieken uit kunt trekken. Nu valt dat wel een beetje buiten de scope van deze blog maar toch....
Hmm, deze jongen vindt naar mijn idee gewoon het warm water uit hier... Elk persoon die iets of wat met wetenschap bezig is of is geweest, vindt dit basis. Bah, 5 minuten weggegooidRobIII schreef op maandag 10 januari 2011 @ 19:39:
[...]
Ah, we hebben weer een Power-of-Ten syndroompje
Programmers Need To Learn Statistics Or I Will Kill Them All
Ik zal die link vanmiddag wel eens doorlezen, maar het klopt toch wel tot op zekere hoogte wat ik zeg? Als je de test vaker uitvoert wordt het resultaat betrouwbaarder.stits schreef op dinsdag 11 januari 2011 @ 01:19:
[...]
Hmm, deze jongen vindt naar mijn idee gewoon het warm water uit hier... Elk persoon die iets of wat met wetenschap bezig is of is geweest, vindt dit basis. Bah, 5 minuten weggegooid
En je denkt dat ik niet een paar keer op f5 heb gedrukt? Dit is een representatieve sample. De prestatie van de methoden zijn toch zeer afhankelijk van je testcase, je hardware, je software, bezetting van hardware, etc. Het gaat dus meer om de verschillen tussen de tests dan de daadwerkelijke tijden zelf.
Een (gewogen) gemiddelde maken van deze waarden zou zo goed als niets toevoegen omdat de tijden nauwelijks zullen verschillen dan de waarden die ik hier heb neergezet
Een (gewogen) gemiddelde maken van deze waarden zou zo goed als niets toevoegen omdat de tijden nauwelijks zullen verschillen dan de waarden die ik hier heb neergezet
Hoezo denk je dat heb je dat getest?Een (gewogen) gemiddelde maken van deze waarden zou zo goed als niets toevoegen omdat de tijden nauwelijks zullen verschillen dan de waarden die ik hier heb neergezet
Ja dat zeg ik dus ook. Maar ik zeg ook dat als je de test vaker zou uitvoeren dat je test dan betrouwbaarder wordt en daar is RobIII het niet mee eens blijkbaar.En je denkt dat ik niet een paar keer op f5 heb gedrukt? Dit is een representatieve sample. De prestatie van de methoden zijn toch zeer afhankelijk van je testcase, je hardware, je software, bezetting van hardware, etc. Het gaat dus meer om de verschillen tussen de tests dan de daadwerkelijke tijden zelf.
Ik heb nu ook die link doorgelezen van RobIII en ik snap dat als je niet de standaardafwijking en de condities bij je test vermeld het meermalig uitvoeren van een test net zo weinig zegt als het eenmalig uitvoeren van een test.
[Reactie gewijzigd op dinsdag 11 januari 2011 19:19]
Reageren is niet meer mogelijk