Mulla webbisivulla trendi, joka sivunlatauksen yhteydessä hakee ajaxilla datansa JSON-muodossa. Näytettävää dataa on kohtuu paljon ja PHP alkaa tökkiä vastaan, kuun yritän pulauttaa tietorakenteen JSONiksi:
Fatal error: Allowed memory size of 134217728 bytes exhausted
Virhe viittaa riville, jossa tulostan tietokannasta haetun $datan echo json_encode($data);
Mikä olisi siis järkevämpi tapa siirtää tuo data? Tietenkin sen voisi pilkkoa usemapaan ajax-kyselyyn, mutta itse pähkäilin, että olisi fiksumpi yrittää saada data binäärimuotoon, jonka pistää base64_encode()-funktion läpi selaimelle. Tosin ei tuossa base64:ssäkään taida "hyötysuhde" (varsinaisen datan määrä/siirrettävän data määrä) olla kovin hyvä, mutta silti huomattavasti parempi kun JSONissa.
Edit: tässä C#-koodina mitä haen:
using (MemoryStream ms = new MemoryStream()) { using(StreamWriter sw = new StreamWriter(ms)) { // tätä tulisi loopissa tietokannasta int time = 1; int data1 = 2; int data2 = 3; int data3 = 4; sw.Write(time); sw.Write(data1); sw.Write(data2); sw.Write(data3); } string base64String = Convert.ToBase64String(ms.ToArray()); }
”Hyötysuhde” riippuu datasta. JSON-muodossa UTF-8-teksti menee lähes sellaisenaan mutta tekstimuotoiset luvut ja assosiatiivisten taulukoiden avaimet hukkaavat tilaa. Base64:ssä taas yhteen 8-bittiseen merkkiin menee 6 bittiä dataa ja hyötysuhde on sisällöstä riippumaton.
Jos $data on taulukko, JSON-muunnoksen aikana tarvittavaa muistimäärää voi optimoida näin:
// [json_encode($data[0]),json_encode($data[1]),...] $alku = "["; foreach ($data as $tmp) { echo $alku; $alku = ","; echo json_encode($tmp); } echo "]";
Heittämällä mahdolliset tekstiavaimet pois eli käyttämällä vain numeerisesti indeksoituja taulukoita säästät paljon tilaa. Kannattaa myös varmistaa, että PHP käsittelee luvut lukuina eikä tekstinä. Jos kaikki data on lukuja, et tarvitse JSON-funktioita ollenkaan vaan voit helposti tuottaa itse pilkulla eroteltuja listoja:
Kiitokset vastauksesta.
Metabolix kirjoitti:
Heittämällä mahdolliset tekstiavaimet pois eli käyttämällä vain numeerisesti indeksoituja taulukoita säästät paljon tilaa. Kannattaa myös varmistaa, että PHP käsittelee luvut lukuina eikä tekstinä.
Juu, nuo olin jo optimoinutkin ja noilla vinkeillä saan tuon PHP:n muistivirheen poistettua.
Mutta jos ajatellaan vaikka yksittäisen UNIX-aikaleiman siirtämistä eri tekniikoilla, niin
- JSON: 1414850781 ==> 10 merkkiä ==> 10 tavua(vai 20, riippuu merkistökoodauksesta?)
- 1414850781 numerona käsiteltynä 4 tavua [204, 226, 84, 84]. Jos laskee, että hyötykuorma Base64:ssä on kuten mainitsit 6/8, niin tämä numero veisi 5,333 tavua eli lähes 50% vähemmän kun JSONilla.
Vai olenko ihan hakuteillä? :)
ajv kirjoitti:
JSON: 1414850781 ==> 10 merkkiä ==> 10 tavua(vai 20, riippuu merkistökoodauksesta?)
1414850781 numerona käsiteltynä 4 tavua – – Base64:ssä 5,333 tavua
Ihan totta. Kuten jo kirjoitinkin, isot luvut vievät tekstimuodossa enemmän tilaa kuin binäärimuodossa (edes Base64-enkoodattuna). JSON käyttää UTF-8-merkistöä, jolloin luku 1414850781 vie 10 tavua, ja lisäksi tietenkin tulevat JSON-formaatin pilkut ja sulut.
PHP:llä voi tuottaa binääridataa funktiolla pack ja enkoodata funktiolla base64_encode. Koska kolme tavua pakkautuu neljään merkkiin, base64_encode tuottaa aina tekstin, jonka pituus on jaollinen neljällä; 1-2 viimeistä merkkiä voivat olla täytemerkkejä.
$str = ""; foreach ($data as $tmp) { $str .= pack("NN", $tmp[0], $tmp[1]); // 2 x Uint32 } echo base64_encode($str);
Base64:n purkamiseen JS-puolella kannattaa etsiä netistä jokin kirjasto.
Jes, juuri näin. Tuota pack()-funktiota en muistanutkaan. Täytyy nyt vielä puntaroida kannattaako tuota JSONia oikeasti vaihtaa. Tietokannassa on oman kesämökin lämpötila yms. dataa, eli toisin sanoen puhutaan harrasteprojektista :)
Kannattaa tsekata myös kuinka paljon auttaa jos saat datan suoraan kannasta pakattuna (esim. MySQL:n COMPRESS()), ja jos saat sen vielä selaimessa purettua (luulisi löytyvän valmiita kirjastoja).
timoh, COMPRESS pakkaa yksittäisiä arvoja, ei suinkaan useamman rivin dataa.
Jos pakkausta haluaa käyttää, kätevintä on vain käyttää gzip- tai deflate-pakkausta HTTP-tasolla, jolloin selain purkaa vastauksen automaattisesti.
Juu, totta tosiaan COMPRESS:ia voi olla vaikea tässä hyödyntää.
Tein datan siirron tuolla base64-koodauksella ja liikuteltavan datan määrä tippui 3,8Mt => 2,2Mt, mutta sivun latausaika kasvoi 3s => 4s, johtuen siitä, että tuo base64 on ilmeisen hidasta dekoodata JS:llä. Täytyy vielä testata jollain toisella kirjastolla.
Autojen virittäjillä on olemassa sääntö Viritys voidaan katsoa onnituneeksi jossei teho laske enempää kuin 10%. Mulla laski 25%, eli viritys ei onnistunut (vielä) :) Toki viritys voidaan katsoa onnistuneeksi, jos käytän hidasta nettiyhteyttä jolloin datan lataamisen kuluva aika on huomattavasti suurempi.
Base64:n kannattavuus JS:llä riippuu todella paljon käyttötapauksesta ja koodin optimoinnista. Tein kokeeksi koodin tyypitetyillä taulukoilla pelkästään 32-bittisille kokonaisluvuille, ja tulos on varsin hyvä, jopa huomattavasti JSONia nopeampi tämänhetkisellä Firefoxilla.
<?php // Luodaan 2 megaa binääridataa. $str = openssl_random_pseudo_bytes(2 * 1024 * 1024); $b64_str = base64_encode($str); $json_str = "[".implode(",", unpack("N*", $str))."]"; // N = big endian Uint32 ?> <script> 'use strict'; (function() { var Base64 = { u32: function(s) { var tmp = new Int32Array(128); for (var i = 0; i < 64; ++i) { tmp["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charCodeAt(i)] = i; } var arr = new Uint32Array((s.length * 6 / 32) | 0); for (var i = 0, j = 0;; i += 16) { for (var k = 0; k < 16; ++k) { tmp[k] = tmp[s.charCodeAt(i + k) || 16]; } arr[j] = (tmp[ 0] << 26) + (tmp[ 1] << 20) + (tmp[ 2] << 14) + (tmp[ 3] << 8) + (tmp[ 4] << 2) + (tmp[ 5] >> 4); if (++j == arr.length) break; arr[j] = (tmp[ 5] << 28) + (tmp[ 6] << 22) + (tmp[ 7] << 16) + (tmp[ 8] << 10) + (tmp[ 9] << 4) + (tmp[10] >> 2); if (++j == arr.length) break; arr[j] = (tmp[10] << 30) + (tmp[11] << 24) + (tmp[12] << 18) + (tmp[13] << 12) + (tmp[14] << 6) + (tmp[15] >> 0); if (++j == arr.length) break; } return arr; } }; function test(name, f, s) { var t0 = (new Date()).getTime(); var a = f(s); var t1 = (new Date()).getTime(); var sum = 0; for (var i = 0; i < a.length; ++i) { sum += a[i]; } console.log({"name": name, "dt": (t1 - t0) + " ms", "s.length": s.length, "a.length": a.length, "sum": sum}); } var b64_str = <?= json_encode($b64_str); ?>; var json_str = <?= json_encode($json_str); ?>; test("JSON", JSON.parse, json_str); test("Base64, Uint32Array", Base64.u32, b64_str); }()); </script>
Tulokset:
JSON: 88 ms, s.length: 5632286 Base64: 34 ms, s.length: 2796204
Tyypitettyjen taulukoiden vaihto tavallisiin tuplaa ajankäytön – silti nopeampi kuin JSON! – ja muuttaa luvut etumerkillisiksi, ellei erikseen kikkaile etumerkkejä pois, mikä taas hidastaa huomattavasti lisää.
Toisena vaihtoehtona voisit joka tapauksessa kokeilla gzip-pakkausta. Sen saa käyttöön yhdellä koodirivillä: lisää PHP-koodin alkuun ob_start("ob_gzhandler"). Parin megan gzip-paketin purkaminen vie nykykoneilla sekunnin kymmenyksiä ja tapahtuu selaimen natiivikoodissa, ja satunnaisen JSON-numerodatan pakkaamisessa gzip pääsee hyvinkin viidenkymmenen prosentin pakkaukseen. Huono puoli on, että palvelimella pakkaaminen vie hieman aikaa. Chromiumin kehittäjäkonsolin Network-välilehdellä näkyvät erikseen ladattu datamäärä (HTTP-otsikoiden kanssa) ja puretun sisällön koko, ja pyynnön otsikkotiedoista voi vielä katsoa, onko gzip ollut käytössä.
Aihe on jo aika vanha, joten et voi enää vastata siihen.