De snelste manier om een array te prefix in php

Door krvabo op maandag 10 januari 2011 15:36 - Reacties (25)
Categorie: Dev, Views: 7.443

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:

TestTijdExtra geheugengebruik
Array_walk met method0.02664402,097,152B
Explode / implode0.0109251786,432B
For-loop0.01159690B
For-loop met method0.02150510B
Foreach0.01351001,048,576B
Foreach met reference0.01008800B


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


Door Tweakers user ACM, 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 :)

[Reactie gewijzigd op maandag 10 januari 2011 15:46]


Door Tweakers user RSpliet, maandag 10 januari 2011 16: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 :-)

Door Tweakers user Salandur, 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.

Door Tweakers user -RetroX-, maandag 10 januari 2011 16:58

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

Door Tweakers user krvabo, maandag 10 januari 2011 17:07

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 :)
Met een ordegrootte 30k heb ik natuurlijk niet te maken, maar ik zal even uitleggen waar ik deze methode voor wil gebruiken:
- 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.
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 :-)
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..
Salandur 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.
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 doe :)

[Reactie gewijzigd op maandag 10 januari 2011 17:14]


Door Tweakers user analog_, maandag 10 januari 2011 17:31

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.

[Reactie gewijzigd op maandag 10 januari 2011 18:11]


Door Tweakers user krvabo, maandag 10 januari 2011 17:35

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 :)

Door Tweakers user Phoenix1337, maandag 10 januari 2011 17:50

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.
Ehm, dat werkt gewoon hoor?

<?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);
?>

Door Tweakers user Erkens, maandag 10 januari 2011 18:19

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:
1
2
3
foreach($aItems as &$iItem) {
  $iItem = $sPre.$iItem;
}



6: 0.0254430 (foreach) - Mem 0B


Is bij mij ook meteen het snelste :o 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.

Door Tweakers user ACM, maandag 10 januari 2011 18:34

krvabo 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.
Als je een array van 30k items opslaat in memcached gok ik dat je niet de meest efficiente aanpak hebt gekozen ;) Althans, 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.

Door Tweakers user krvabo, maandag 10 januari 2011 18:51

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 snelste :o 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.
Klopt, het geheugengebruik is geloof ik (pin me er niet op vast) ook afhankelijk van andere php-pagina's die ondertussen verwerkt worden.
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 :P


6: 0.0100880 (foreach - own array) - Mem 0B

.
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 gekozen ;) Althans, 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.
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.

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]


Door Tweakers user Meijuh, maandag 10 januari 2011 19:21

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.

Door Tweakers user RobIII, maandag 10 januari 2011 19:39

Meijuh 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.
Ah, we hebben weer een Power-of-Ten syndroompje :Y)
Programmers Need To Learn Statistics Or I Will Kill Them All

[Reactie gewijzigd op maandag 10 januari 2011 19:41]


Door Tweakers user analog_, maandag 10 januari 2011 20:27

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.

Door Tweakers user ReenL, 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.

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.

Door Tweakers user krvabo, maandag 10 januari 2011 21:05

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.
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.
array-walk en for loop met method hebben een extra (onoverzichtelijke) functie nodig, dus ik denk dat niemand dat als eerste oplossing bedenkt.
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 testen ;)
Daarnaast klinkt het geheel een beetje als een micro-optimisation. En als je alle onzinnige varianten wegstreept hou je dit over:
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.
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.
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.

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.
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?
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.
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 ;)
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.
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.

Overigens gebruik ik expres geen unset om te kijken hoeveel extra geheugen wordt gebruikt.
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.
En toch maak ik dat lekker zelf uit ;)
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]


Door Tweakers user Olaf van der Spek, maandag 10 januari 2011 21:09

Als je zoveel tijd besteed aan zo'n issue, heb je dan niet de verkeerde taal gekozen?

Door Tweakers user ACM, maandag 10 januari 2011 21:37

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.
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.
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 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.
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.
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.
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.
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% :P

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.
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.
Als je toch meerdere "correcte" en leesbare varianten kent, dan kan je inderdaad net zo goed de meest efficiente pakken :)

Door Tweakers user ReenL, maandag 10 januari 2011 21:50

onduidelijk is waar de waarde vandaan komt
Prima, maar nu ben je appels met peren aan het vergelijken omdat je het bij de andere oplossingen niet doet.
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.
0.3 seconden op 3miljoen entries. En achtergrond taken kijken niet op 1,5 seconden.
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.
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?

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 ;).

Door Tweakers user krvabo, maandag 10 januari 2011 22:54

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

Het schrijven van het blog duurde al langer dan het schrijven van de test..
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.
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.

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 :)
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.

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.
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-get ;) Ik moet ergens weten welke elementen dat zijn, maar zoals ik al zei is het efficienter om een multidimensionale array bij te houden met id en viewcount dan dit.
[...]

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% :P
True, maar het is wel zo dat 1.000.000x 0.1% meer uitmaakt dan 100x 0.1%. :+
Als je toch meerdere "correcte" en leesbare varianten kent, dan kan je inderdaad net zo goed de meest efficiente pakken :)
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.
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.
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.
0.3 seconden op 3miljoen entries. En achtergrond taken kijken niet op 1,5 seconden.
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?
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?
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.
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 ;).
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 gemierenneuk :z

Door Tweakers user kwaakvaak_v2, maandag 10 januari 2011 22:58

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

Door Tweakers user stits, 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 :+

Door Tweakers user Meijuh, dinsdag 11 januari 2011 09:33

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 :+
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.

Door Tweakers user krvabo, dinsdag 11 januari 2011 09:46

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 :)

Door Tweakers user Meijuh, dinsdag 11 januari 2011 18:55

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

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