Javascript – a tömb rejtett értékei
JavaScriptben arra a problémára kerestem a megoldást, hogy egy objektumokat tartalmazó tömbben hogyan lehet egy objektum/függvény értékű tulajdonság alapján a leghatékonyabban keresni. Ez elég specifikus téma, mert legtöbbször már létrehozott DOM objektumokkal dolgozunk JavaScriptben, szóval az objektumok felvétele egy adott tömbbe és kikeresésük már adott (lásd
Az újrahasznosíthatóság jegyében az ember úgy áll neki megírni egy ilyen objektum kulcsú adattárolót, hogy az általánosan használható legyen, tehát a tulajdonság, ami alapján hozzáad és keres, példányonként változtatható legyen.
Normál esetben a (hibás) megoldás így néz ki:A
Nyilván ez a logikus megoldás, viszont nem ez a legjobb, mert a ciklusban a szöveges kulcs alapján történő kikeresés jelentősen lelassítja a függvény futását.
Az egészen úgy lehet gyorsítani, hogy kiemeljük az azonosító objektumot, és egy tömbben tároljuk le az azonosítót és az adatot:Így a ciklusban a szöveges adatelérést a jóval gyorsabb szám alapú adateléréssel helyettesítjük. Az ember nem is gondolná, hogy mennyit számít mindez.
Amivel teszteltem:Az első esetben 350 ms körüli értékeket kaptam, a második esetben pedig 280 ms körülieket. A különbség a nagyobb érték 20%-a, ami már igen jelentős.
Ennek a fajta megközelítésnek a hatékonysága még jobban tükröződik az olyan helyzetekben, amikortípusú tárolást hajtunk végre.
Nagyon sok helyen láttam már azt a mintát (
Ilyen esetben érdemesebb egy előre megírt objektum kulcsú osztályt használni a tárolásra.
■ getElementsByTagName()
).Az újrahasznosíthatóság jegyében az ember úgy áll neki megírni egy ilyen objektum kulcsú adattárolót, hogy az általánosan használható legyen, tehát a tulajdonság, ami alapján hozzáad és keres, példányonként változtatható legyen.
Normál esetben a (hibás) megoldás így néz ki:
var reference = function (key)
{
var store = [];
var indexOf = function (identifier_object)
{
for (var j = 0; j < store.length; j++)
{
var data_object = store[j];
if (data_object[key] == identifier_object) {
return j;
}
}
return -1;
};
this.set = function (data_object)
{
var index = indexOf(data_object[key]);
store[index == -1 ? store.length : index] = data_object;
return this;
};
this.get = function (identifier_object)
{
var index = indexOf(identifier_object);
if (index != -1) {
return store[index];
}
};
};
set()
függvénnyel adunk hozzá egy elemet a listához vagy módosítunk egy már létező elemet. Ehhez nyilván be kell járni a tömböt, le kell kérni a tárolt objektumoktól a key
tulajdonságot, ami az azonosító objektumokat tartalmazza, és az azonosító objektumokat össze kell hasonlítani a beállítandó objektum azonosító objektumával.Nyilván ez a logikus megoldás, viszont nem ez a legjobb, mert a ciklusban a szöveges kulcs alapján történő kikeresés jelentősen lelassítja a függvény futását.
Az egészen úgy lehet gyorsítani, hogy kiemeljük az azonosító objektumot, és egy tömbben tároljuk le az azonosítót és az adatot:
var reference = function (key)
{
var store = [];
var indexOf = function (identifier_object)
{
for (var j = 0; j < store.length; j++) {
var current_identifier_object = store[j][0];
if (current_identifier_object === identifier_object) {
return j;
}
}
return -1;
};
this.set = function (data_object)
{
var index = indexOf(data_object[key]);
store[index == -1 ? store.length : index] = [data_object[key], data_object];
return this;
};
this.get = function (identifier_object)
{
var index = indexOf(identifier_object);
if (index != -1) {
return store[index][1];
}
};
};
Amivel teszteltem:
<script type="text/javascript" src="store.js"></script>
<script type="text/javascript">
var d1 = new Date();
var instance = new reference('identifier_object');
for (var i = 0; i < 1000; i++) {
var data_object = {identifier_object: {}};
instance.set(data_object);
}
var d2 = new Date();
alert((d2 - d1) + ' milliseconds');
</script>
Ennek a fajta megközelítésnek a hatékonysága még jobban tükröződik az olyan helyzetekben, amikor
[
{
kulcs: kulcs_objektum,
adat: adat_objektum,
},
⋮
]
Nagyon sok helyen láttam már azt a mintát (
prototype.js
-ben azt hiszem szintén előfordult), hogy ilyen esetben a kulcsnak és az adatnak is szép, beszédes nevet adnak. Ez a lehető leghibásabb megközelítés, ami a keresési sebességét körülbelül a felére csökkenti.Ilyen esetben érdemesebb egy előre megírt objektum kulcsú osztályt használni a tárolásra.
[
[kulcs_objektum, adat_objektum],
⋮
]
Extremitások
Érdekes lenne megnézni a bejegyzés végén felvetett saját konténerobjektumos megoldás sebességét.
Valamint talán életszerűbb lenne egy
set(kev, value)
típusú megoldás ahol elzárjuk a usertől a konténerobjektumokat – ilyenkor ugyebár teljes uralmat élvezel a konténerobjektum fölött olyan rendszerközeli lehet ahogy csak szeretnéd.:-)
Nem nagyon értem mire gondolsz :-)
A
set(value,key)
sorrendet azért javaslom, mert sokkal általánosabb, mint aset(key,value)
. Tömböknél nyilván egyszerűbb, ha lehagyod a kulcsot a hozzáfűzéshez. Persze a key, value sorrend is előfordulhat, csak úgy nehezebb jelölni ha a tömbhöz akarsz hozzáfűzni. Mondjukset(-1,value)
lehetne egy ilyen jelölés.A dologgal kapcsolatban az
each
is szerepet játszik, mondjuk egy sima tömbre (break
,continue
kihagyásával) azeach
:each
nél az adott értéket használjuk csak az esetek nagy részében, ezért jobb előrevenni, viszont ha azeach
ben előreveszem, akkor aset
ben is nyilván, hogy a sorrend azonos legyen.Alapvetően JS-ben 3 alapszintű gyűjteménnyel szoktam dolgozni a 3 kulcstípus miatt:
Ez így nem igaz szerintem. Itt az a lényeg, hogy objektum kulccsal tároljak le adatot. A DOM object az adat, a tárolási forma meg közömbös, mert úgysem látod.
logika, tárolás
A) A felhasználó számára direkt elérhetővé teszed a valódi (natív) tömböt, ami a konténer alapját képezi. Ilyenkor a lehető legáltalánosabb konvenciókat használnám (például kulcsként tetszóleges tulajdonság szerepelhet, ahogy az első példában). Ekkor a hibamentességet a maximális flexibilitás garantálja.
B) A tárolásra használt töböt az objektumom belsejében elrejtem, és kizárólag a saját metódusaimon kereszül teszem elérhetővé. Ez garantálja a hibementességet, és ilyenkor a belső feléptés lehet olyan rendszerközeli, amit holnap már én magam sem értek, ha elég gyors. - Természetesen ilyenkor a módostás újraírást jelent.
No, itt a mondat első fele programtervezési feladat, a második pedig az eszköz késztőjéé. Logikusan hangzik hogy gyorsabb stringekkel indexelni, mint objektumokkal. Ha emellett döntöttél, akkor választhatsz egy oylan konténert ami garantáltan stringgel indexel (például egy objektum tulajdonságaként tárolni), vagy egy általános konténert aminek indexei akár objektumok is lehetnek.
Ez két különböző eszköz. A string-indexű konténer nem jobb semnem rosszabb az objektum indexűnél. Az előbbi előnye a sebesség a másodiké a flexibilitás. Egy önkényesen kiválasztott felhasználás tükrében nincs értelme magukat a tárolási módszereket összehasonlítani.
Yepp
Lehetséges, akkor ez az én egyedi megközelítésem, ami valószínűleg azért alakult ki, mert az append-et és a replace-et összevontam egy függvénybe. Hash-nél ezt nyilván gond nélkül megtehetem, tömbnél a kulcsot egyszerűen le tudom ellenőrizni, szóval ott sincs gond vele. Úgy látszik a probléma az object kulcsú tárolásnál jött elő.
Ami konkrétan ehhez vezetett az az, hogy próbáltam a 3 típus (Objektum kulcsú,Hash,Array) felületét egységesíteni, és ezért összevontam a felülírást és hozzáfűzést. A második kettőnél ez a módszer bevált, viszont az Objektum típusúnál már erősen lassít. Jobbat viszont egyelőre nem találtam ki.
Nem emlékszem, hogy az object kulcsú tárolást bárhol összehasonlítottam volna a Hash-el. Mindkét megoldás a bejegyzésben arra irányult, hogy az objektum kulcsú tárolást hogyan lehet a leggyorsabban megoldani, és engem őszintén szólva meglepett, hogy ebben a speciális esetben gyorsabb kiszedni a kulcsot tömbbe, mint bennhagyni tulajdonságként, mert magának a kulcsnak a kiolvasása az objektumból nagyon lassú.
Harmadik út :)
A másik, hogy én elrejteném a user elől ezeket a kulcs-érték párokat megtestesítő objektumokat, bármi is legyen a tárolás formája.
Igen
Pont ez a lényeg, hogy messze nem olyan gyors, mint a tömbös megoldás. Úgy látszik rosszul fogalmaztam meg a blog bejegyzést, vagy már nagyon le voltam fáradva :-)
A másik, hogy én elrejteném a user elől ezeket a kulcs-érték párokat megtestesítő objektumokat, bármi is legyen a tárolás formája.
Jelenlegi formában el vannak rejtve, de általában nem ezt a megoldást szoktam használni, mert ha a prototype-ba rakom a függvényeket, akkor sokkal gyorsabb egy-egy objektum felépítése. Szóval javascriptben sajnos csak publikusra lehet megírni. Majd szerintem még erről is lesz bejegyzésem, hogy miben más a javascript OOPje a többi oop-hez képest.
Egyébként engem érdekelne, hogy te hogyan oldanád meg a set,get,each metódusokat mindhárom fajta tároláshoz?
A probléma, amit nem tudtam áthidalni, és ami miatt összevontam a set-be az összes értékadást:
A fenti példában egy adott értéket kell hozzáadni valamilyen gyűjteményből egy tetszőleges típusú másik gyűjteménybe. A gond vele az, hogy Hash és Reference(nevezzük ennek az objektum kulcsút, ha nem tudsz jobbat) esetében kell a kulcs a hozzáfűzéshez, tömb esetében viszont szükségtelen, sőt kifejezetten káros.
Ezért csináltam a set(value,key) függvényt, hogy áthidaljam a problémát.
Még gyorsítható…
Teszteredmények (tiéd vs. enyém; FF 3.0, WinVista):
771 msec vs. 36 msec (!), ha minden kulcs különböző
654 msec vs. 330 msec, ha minden kulcs egyezik (mint a Te példádban is).
Hibás
Alapvetően a legnagyobb hiba a megoldásoddal, hogy a toString-re nem lehet alapozni az objektumok összehasonlítását, mert a toString értéke attól függetlenül megváltozhat, hogy a gyűjteményhez hozzányúlnánk.
Mondjuk az egyik dátumnál megváltoztatom az évszámot stb....
vélemények
Én sem feltétlen értek azzal egyet, hogy az első megoldást kinevezted hibásnak, bár tény, hogy a második gyorsabb. A teszt viszont elég veszélyes, nem méred, hogy a kiolvasás mivel jár. Ki kéne találni, hogy az írások-olvasások aránya milyen lesz használat közben és ez alapján mérni a futásidőt. Ha nincs erről információd akkor mondjuk feltételezheted, hogy ugyanannyi az írások mint az olvasások száma.
Hogy a Te vonaladat kövessem itt egy 3., még gyorsabb megoldás:
Viszont az alapvető probléma ezekkel a megoldásokkal, hogy a műveletidő erősen függ a mapben lévő elemek számától. Az első ezer elemet jóval gyorsabb belerakni, mint a második ezret. Viszont ezen sajnos a kulcsobjektumok érintése nélkül nem lehet gyorsítani (ill. javítani a műveletigény aszimptotikus nagyságrendjén). Viszont meg lehet úgy oldani, hogy a végrehajtott módosítás általános legyen, ne pedig map-specifikus. Azaz, ha feltételezzük, hogy használható a következő függvény:
Szerintem
Az elsővel egyetértek, hogy a 2 tömb gyorsabb, mint az egy. A beállítás valóban gyorsabb vele, és takarékosabb, viszont a törlési sebességet felére lassítja, mert 2 tömbön kell áthelyezni az elemeket, nem csak az egyiken. Szóval ez is olyan, hogy valamit valamiért, viszont set-et biztos, hogy többet használja az ember, mint a remove-t.
Hash-el sajnos nem lehet összekötni, csak speciális helyzetben. Ha két külön gyűjteménybe rakod be ugyanazokat az objecteket, akkor felülírják egymás hash tulajdonságait. Persze ezt is tovább lehet bonyolítani, ha a __hash egy tömb, és minden reference kap egy azonosító számot. A törlés ebben az esetben még problémásabb lesz, szóval javaslom, hogy mérjük a törlési sebességet is, ha ilyen irányban megyünk el.
Az ObjectMap valóban jobb név. HashString függvény írása javascript objectekre szerintem lehetetlen. Már ha egy objecthez egyedi azonosítót generáló függvényre gondolsz. Ha létezne ilyen, akkor nem kéne a tömbökkel foglalkozni.
félreértesz
Nézd csak meg jobban a hash függvényt, teljesen független attól, hogy milyen mapról van szó. Sőt, más is használhatja, nem csak egy map. Próbáltam hangsúlyozni ezt az előző hozzászólásomban, azért írtam külön blokkban a hash függvényt. Vagy ha nem esik le, próbálj ki valamit különböző mapekbe tenni és nézegesd firebug-ban a mapek reprezentációt. A törlés is nagyon egyszerű, csak egy hash hívás és egy delete. Megmutatom, ha nem egyértelmű.
Visszatérve a másik, tömbös megoldásnál a törlésre: el kell dönteni, hogy hogyan szeretnél törölni. A teljes tömb átrendezése műveletigényes, lehet, hogy gyorsabb egy null-t vagy undefined-edet (nem tudom olcsóbb-e valamelyik és ha igen melyik) rakni a törlendő helyére. De ez megintcsak attól függ, hogyan fogod használni a mapet.
Az ObjectMap elnevezés félreérthető, ez egy ReferenceMap. Nem mindegy! Itt egy példa, amin láthatod a különbséget:
Egyébként az én ReferenceMap-em nem viselkedik pont ugyanúgy, mint a Te reference-ed. Mégpedig olyankor amikor primitív típust (string literál, szám literál, logikai érték) használsz kulcsnak, amiknél ugye nem létezik referencia (azoknál nem is referenciát hasonlít össze az === operátor). Tehát azok kapcsán nem sok értelme van ReferenceMap-ről beszélni.
Miért ne lehetne hashString-et írni javascriptben? Általánosat csak nehezen, kompromisszumokkal, de általában az osztálytól függ, hogy mitől számít két objektum egyenlőnek. (A fenti példánál ugyebár az összeadás kommutatív, de az osztás már nem lenne az.) Ha pedig olyanra van szükséged, ami referenciánként ad egyedi értéket és független a műveletigénye ez eddig kiszámolt értékek darabszámától, akkor azt csak az objektum módosításával lehet elérni (mert nincs hozzáférésed a referencia értékéhez). Erre viszont már mutattam is Neked egy függvényt, és azt hiszem ilyenre gondoltál ez alatt: "egy objecthez egyedi azonosítót generáló függvény".
Benéztem
Azt hiszem még ennyivel kiegészíteném:
Nem lehetsz benne biztos...
Jó volt az írásod, több szempontból is, még több ilyet, köszi!
Köszi