Po dlouhé době jsem se rozhodl, že zaseněco napíšu na blog zde na Rootu a i tentokrát to bude něco z mé vlastní programátorské tvorby. Jde o knihovnu ImtJSON která je určena k práci s formátem JSON a je k dispozici zcela zdarma pod licencí MIT. Odkaz najdete na konci článku.
Důvody vzniku další knihovny pro práci s JSONem mohou být pro „přespolního“ programátora nejasné. Vždyť se stačí podívat na stránky www.json.org, které jsou výživným zdrojem informací o tomto populárním formátu, a lze zde najít také stovky odkazů na rozličné knihovny ve všech možných programovacích jazycích, z nichž pro jazyk C a C++ jich jsou desítky. A tak k čemu další knihovna? Pokusím se v tomto článku stručně shrnout zajímavé vlastnosti právě knihovny ImtJSON a vysvětlit motivace k jejímu vzniku.
Parsovat a serializovat formát JSON je jednoduché, proto zpracovat takové zadání zvládne každý programátor za odpoledne. Je to tak snadné, že existují minimální implementace ve formě jednoho hlavičkového souboru, který se pouze vloží do zdrojového kódu a mohu začít pracovat s JSONem. Nicméně jsem přesvědčen o tom, že to k vytvoření dobrého nástroje nestačí. Důležitý je i způsob, jak jsou parserem načtená data organizovaná v paměti a skrze jaké funkce se přistupuje k dílčím datům. Tady existují dva přistupy. První, jednodušší, avšak v případě formátu JSON ne tak často užívaný (častěji jsem ho viděl u XML) je parser, který při průchodu JSONem volá programátorem určené události, ve kterých se data přímo zpracovávají. Tyto implementace mají svůj význam, bývají jednodušší a slouží víceméně k takovému rychlému načtení dat z JSONu, něco ve stylu „zajímá mne jen tady to pole, zbytek ignoruj“, případně ke zpracování nekonečných streamů v JSONu. Druhým přístupem je vytvoření DOMu . Parser v tomto případě ukládá data do vnitřní reprezentace, která se označuje zkratkou DOM (Document Object Model) a stejně tak serializátor převádí DOM reprezentaci zpět do formátu JSON. Programátor tak nepracuje s JSONem přímo, ale s nějakou vnitřní reprezentací, byť ta má často podobnou strukturu jako původní JSON. Konečně, zkratka JSON znamená JavaScript Object Notation, což není nic jiného, než textová reprezentace objektového DOMu v JavaScriptu.
Motivací bylo několik. V původní verzi článku jsem je tu měl všechny popsané, ale bylo to dlouhý, tak to zkrátím. Hlavní motivací zavést imutabilní DOM bylo aby se zabránilo modifikacím již vytvořené struktury. Na začátku přitom stál systém cachování výsledků jedné NoSQL databáze. Dokud bylo možné měnit kontejnery DOMu (což znamená objekty a pole), pak se snadno dal měnit obsah cache. Skutečnost, že se kontejnery při vkládání do dalších kontejnerů zpravidla předávají jako odkazy (nebo chcete-li reference), tak jak je běžné v JavaScriptu, klade na programátora potažmo na vlastní kód povinnost vědět, s čím zrovna pracuje a jak budou fungovat různé operace s dílčím kontejnerem. Že vložit do kontejneru kontejner je něco jiného, než vložit tam konstantu. Že lze vytvořit cyklus, atd.
Hodnota proměnné by měla být identifikována svým obsahem, nikoliv nějakou referencí. Pokud do kontejneru vložím číslo 42, zůstane číslem 42 i když ho později – mimo kontejner – změním na číslo 56. Totéž by mělo platit i o vloženém poli nebo objektu. V C++ mám dostatečné vyjadřovací nástroje, abych mohl vyjádřit, zda některá proměnná může být měněna nebo nesmí. Mohu do funkcí předávat proměnnou volanou hodnotou nebo referencí. Nesmí se mi tedy stát, že proměnnou volanou hodnotou budu moci změnit tak, že se změna projeví i u volajícího.
Zavedením pravidla neměnitelnosti se spoustu věcí zjednodušilo. Zmizely totiž reference, zmizela nutnost neustále kopírovat DOM tam, kde jsem chtěl mít jistotu, že existuje jediný vlastník DOMu, a zmizely pointery. A přestože interně je DOM implementován pomocí pointerů a polymorfizmu, uživatel knihovny toto nemusí vůbec znát ani se tím zabývat. Všechny proměnné, bez ohledu na obsah, se chovají jednotně.
Samozřejmě to znamenalo trochu změnit přístup k práci. Díky tomu, že s každou změnou vzniká nový DOM, jsou v knihovně nástroje (třídy), které umožňují zaznamenat dílčí změny, než jsou použity na vytvoření nového DOMu. Tyto třídy nejsou součástí DOMu, takže se nedají moc dobře sdílet. I tak se mi s tím pracuje dobře a nemám pocit, že by neustále vytváření nových a nových DOMů při každé změně znamenalo významný výkonový zásah. Je to částečně dáno tím, že knihovna využívá neměnitelnost k tomu, aby se redukovalo vytváření zbytečných kopii. Pokud by toto pravidlo nebylo vynuceno, pak každý nový DOM by musel být hlubokou kopii původního DOMu, protože by stále hrozilo nebezpečí, že někdo nebo něco dokáže změnit obsah nějakého vnitřního kontejneru tím, že drží jeho referenci.
Ještě se zmíním o jedné z motivací a tou bylo napsat knihovnu co nejvíce KISS (Keep it simple stupid), tak aby práce s formátem i s výsledným DOMem byla maximálně jednoduchá. intuitivní a výsledkem byl stabilní kód. Nenajdete v ní žádné pointery, minimum šablon, většinu moderních prvků C++, lambdy, iterátory a jak jsem napsal výše, jednotlivé hodnoty DOMu se při manipulaci chovají jednotně a předvídatelně.
Immutablní řešení má samozřejmě nějaké nevýhody:
Příklad vytváří celočíselnou hodnotu, float hodnotu, stringovou hodnotu, boolovskou hodnotu a poslední příklad vytváří hodnotu null.
using namespace json; Value a(10), b(12.3), c("Ahoj svete"),d(true), e(false), f(nullptr);
using namespace json; Value pole = {10, 12.3, "Ahoj svete", true, false, nullptr};
using namespace json; Value objekt = Object("foo", 10) ("bar", 12.3) ("baz", "Ahoj svete") ("damn", nullptr);
Měnit lze pole nebo objekt. Pro změnu pole se použije třída Array, pro změnu objektu pak třída Object. Instance obou tříd lze zpět převést na Value čímž vznikne kopie upravené verze kontejneru. Instance tříd Array a Object nemohou být součástí DOMu.
Object e(objekt); // uprav objekt v promenne e e.set("flag",false); // nastav klic e.unset("damn"); // smaz klic objekt = e; //prepis promennou objekt novou verzi Array a(pole); a.push_back(42); pole = a;
Pro iteraci kontejnery lze použít standardní begin() a end() a tedy i nové range-based for
for (Value v: pole) {
std::cout << "value:" << v.toString() << std::endl;
}
Iterovat lze i objekt
for (Value v: objekt) { StrViewA key = v.getKey(); std::cout << "key:" << key << ", value:" << v.toString() << std::endl; }
U iterace objektů se klíč získává z hodnoty. Je vracen jako typ StrViewA, což je typedef na StringView<char>. Tento typ je v knihovně zaveden pro rychlé a univerzální předávání řetězců a lze jej velice jednoduše převést na std::string.
Iterovat lze i přes index
for (std::size_t i = 0, cnt = pole.size(); i < cnt; ++i) {
Value v = pole[i]; std::cout << "value:" << v.toString() << std::endl;
}
Iterovat lze i přes index i objekty
for (std::size_t i = 0, cnt = objekt.size(); i < cnt; ++i) { Value v = objekt[i]; StrViewA key = v.getKey(); std::cout << "key:" << key << ", value:" << v.toString() << std::endl; }
Hodnoty se vyzvedávají pomocí funkcí:
v.getUInt() → uintptr v.getInt() → intptr v.getNumber() → double v.getString() → StrViewA v.getBool() → bool v.isNull() → bool v.defined() → bool
Použití metod pro vyzvedávání hodnot je zavedeno čistě záměrně. Šlo by samozřejmě použít konverzní operátor, ale mám vyzkoušeno, že to dělá neplechu. Samotná třída Value přijímá mnoho typů a pokud by se potkala s proměnnou, která se dá konvertovat do mnoho typů, překladač bude protestovat.
Pro použití v šablonách, kdy je třeba vyzvednout určitý typ podle T je k dispozici metoda as<T>(). K tomu lze pomocí specializace třídy ConvValueAs<T> rozšiřovat schopnost automatické konverze přes tuto metodu.
v.as<unsigned int>(); v.as<bool>() v.as<std::string>();
K prvkům kontejneru lze přistoupit přes hranaté závorky. K dispozici jsou i funkce na zjištění velikosti kontejneru (size), typu objektu (type) a další. Nechci zabíhat do podrobností, mnohem víc se dozvíte na stránkách projektu.
objekt["foo"].getUInt(); pole[1].getNumber();
Na závěr se krátcezmíním o parseru a serializatoru. Nejsem totiž velký fanda standardní knihovny a zejména iostreamů. Přesto je mi jasné, že by knihovna měla umět serializovat do ostreamu a parsovat z istreamu. Proto zde najdete funkce Value::toStream() a Value::fromStream(). Ale to nestačí. V knihovně najdete mnohem univerzálnější nástroj pro serializaci do čehokoliv a parsování z čehokoliv. A nejde o definici nějakých nových speciálních streamů. Naopak, snažil jsem se to maximálně zjednodušit.
Funkce Value::parse() a Value.serialize() jsou totiž šablony, obě přijímají funkci nebo objekt který implementuje volaní funkce.
V obou případech se předpokládá, že programátor použije lambda funkci. Při serializaci se parametrem předává znak, pro zápis. Při parsování se očekává že funkce vrátí další znak, nebo znak –1 pro konec souboru.
Příklad čtení z stdin a zápis na stdout (v C variantě)
Value v = Value::parse( []{return getchar();} ); v.serialize( [](int c){putchar(c);} );
Tímto jednoduchým způsobem lze implementovat například posílání JSONu rourou, síťovým spojením, nebo uložení JSONu do sdílené paměti. V rámci knihovny ImtJSON je k dispozici také jednoduchý komprimační nástroj, o kterém bych se zmínil v příštím článku.
Aktuální verzi, která by se měla dát přeložit jak v Linuxu tak ve Windows najdete na Githubu. Pokud by někdo našel chybu, nemusí váhat využít issue tracker nebo napsat pull request. Potěší mě, když to bude někdo používat a zašle mi zpětnou vazbu. Na všechny otázky v diskuzi se pokusím odpovědět.
Připravil jsem si ještě dva články o některých featurách. Příště by to bylo o binárním formátu JSONu a ten další pak o popisu struktury dat uložené v JSONu a validaci dat (dokumentu) oproti tomu popisu.
Děkuji za pozornost.
Zatím jsem si jen pročetl článek a nedíval jsem se na zdrojový kód, takže nevím, jak přesně pracuje s de/alokací, nicméně ohledně JSON mám zkušenost z praxe - máme server, který hodně pracuje s JSON dokumenty a obsluhuje tisíce uživatelů. Obvykle to funguje tak, že dojde zpráva, načtou se vnitřní data, vygeneruje se odpověď a příp. uloží upravená vnitřní data - vše v několika JSONech. Při tomto systému je ideální při zpracování zprávy minimalizovat alokace. Ideálně tedy, aby všechny JSON hodnoty měly při zpracování jedné zprávy jeden (sdílený) alokátor (může být třeba tls v C++11 - zpracování jedné zprávy nepřesahuje hranice aktuálního vlákna), který alokaci (malloc/free) snižuje na minimum. V ideálním případě alokuje jeden větší buffer a pokud nepřeteče, celé zpracování zprávy bude mít jen tuto jednu alokaci - a protože vlákno zůstane aktivní pro další zprávy, lze buffer využít znovu, takže počet alokací na zprávu bude nula (resp. 1 / průměrný počet zpráv na vlákno za jeden restart serveru). A díky tomu, že po ukončení zpracování zprávy se všechny JSON struktury uvolní, nebude dlouhodobě docházet k velké fragmentaci.
Souhlas, mrkl jsem se na zdrojové kódy a vypadá to na dynamické alokace na každém kroku. Já bych šel ještě dál a vytvořil bych alokátor, který by dokázal prvník X bytů alokovat přimo na zásobníku, třeba JSONTmpDocument<1024> doc; ...
Další věc je virtuální metody a interface IValue. Nikdy bych JSON knihovnu nenavrhl v tomto duchu a spíš bych se poučil z toho jak dnešní moderní JS-VM pracují s JS, např. použití pouze 8 bytů na JS value, do kterých lze uložit cokoliv (jak double, tak pointer na String, Object nebo Array), jde to, a není to komplikované. Existujou i další věci, které bych určitě zvážil, něco jako hidden class pro velké dokumenty, apod...
Měl jsem verzi, kde jsem měl vlastní alokátory. Měl jsem verzi bez virtuálních metod. Provedl jsem několik měření. Výsledkem bylo, že to mělo absolutně nulový vliv. Záleží samozřejmě na platformě, ale ty rozdíly budou malé. Když nebudu používá virtuální metody, budu tam mít switch/case a z toho pramení... ano bude tam tabulka pro skoky. Co je volání virtuální metody? Tabulka pro skoky... výsledek je ekvivalentní.
Alokátor tam chystám, ale jak říkám, nikdy jsem neměl v ruce alokátor, který by byl výrazně rychlejší, než standardní new třeba ve MS Windows. Dost mě ta měření kolikrát překvapila. Opravdu bývají standardní alokátory dost rychlé, že se optimalizace nevyplatí.
Pokud tam bude alokátor, bude nejspíš globální, cokoliv jiného věci jen komplikuje. Mohu demonstrovat, když bude zájem. Mám několik serverů pracující s protokolem JSONRPC, takže tak nějak vím o čem to je :)
Vždycky člověk bojuje s tím, jestli udržet kód maximálně KISS, maximálně standardní, nebo ho zaplevelit ručními optimalizacemi.
Děkuji za názor.
V takovém případě bylo něco špatně na tom měření. Custom alokátory, které jsou navržené pro specifické účely (v našem případě pro short-lived data struktury) jsou na tom vždycky o moc líp než globální alokátory, které musí být thread-safe. Nebo jinak, ještě jsem neviděl situaci, kdy by globální alokátor překonal ten lokální.
Tohle dávno neplatí. Například windows používá low-fragmentation heap To je heap, kdy je paměť rozdělena na pevné bloky podle normované velikosti. Přidělení bloku je atomická operace, nic se nezamyká. Jen když dojdou předpřipravené bloky, rozdělí se další větší blok. Je to tak ďábelsky rychlé, že to lze vyrovnat jen maximálně pool alokátorem, to je alokátor, který reusuje uvolněné bloky. Drží je ve spojovém seznamu a ještě rozdělené podle velikosti. Alokace je opět atomické vydání bloku z listu. Řešit alokátor per thread a domnívat se, že to je rychlejší... dávno ne!
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366750(v=vs.85).aspx
Možná že v linuxu jsou horší časy, nevím. Každopádně když tak na to nasazuju ten alokátor s poolem a reuseuju uvolněné bloky. Je to rychlý jednoduchý primitivní mt safe a funguje samo.
Když už jdete do technikálií, tak switch může být zoptimalizován kompilátorem různě, ale nebude to horší než přečtení hodnoty z jedné statické tabulky a následně nepodmíněný skok.
Nicméně toto vůbec nemusí být bottleneck programu. Bottleneckem COW přístupu můžou být právě alokace, a dokonce se může stát, že to co se často zpracovává nebude v paměti blízko sebe a dojde k missům v nějakých procesorových cachích. Dá se totiž předpokládát, že kompilátor bude umět kód zkompilovat tak, že tabulka virtuálních metod, obzvlášť pokud bude často používaná a nebude měněná, bude v rychlé procesorové cachi, zatímco vlastní zpracovávaná data budou třeba kvůli alokacím ke cachím necitlivá.
Teda jak root vymyslel tohle formátování příspěvku by mne zajímalo.
No tak to nepřehánějte :)
Volání virtuální metody:
00034493 mov ecx,dword ptr [ebx]
00034495 mov edx,dword ptr [ecx]
00034497 call dword ptr [edx+20h]
Vlastní metoda
--- e:\users\ondra\cpp-projects\immujson\src\imtjson\arrayvalue.cpp ------------
return v.size();
0009CF90 mov eax,dword ptr [ecx+0Ch]
0009CF93 sub eax,dword ptr [ecx+8]
0009CF96 sar eax,2
0009CF99 ret
Na alokace je přidaný alokátor, doimplementovat si vhodný může každý, to přesahuje rámec knihovny. Při COW přístupu se zpravidla dělá jedna alokace pro nový kontejner, následně se tam nasdílí všechny nezměněné prvky. Největší zdržovákem je lock inc a lock dec u počítání referencí. Nicméně je to daň za to, že nemusím vytvářet hluboké kopie a nemusím používat zámky při práci s daty, které jsou sdílené mezi vlákny (při změně DOMu stačí zámek na změnu té sdílené proměnné která to celé drží - něco jako commit změn, dokud změna neni finální, ostatní vlákna vidí původní obsah)
Allocator mohu do knihovny zapracovat maximálně takto
https://github.com/ondra-novak/imtjson/pull/2
Je to globální vlastnost. To nijak neomezuje možnost aby byla TLS, jen si to musí uživatel knihovny doprogramovat (zajistit si mechanismus výběru instance podle threadu). Cokoliv jiného mi přijde komplikované a těžkopádné narušující princip KISS a princip obecnosti.
. Přes alokátor v TLS lze zařídit i alokaci ve stacku, prostě ten prostor ve stacku se zahlásí tomu TLS správci alokátorů a ten k němu bude směrovat požadavky pocházející ze stejného vlákna.
Měl bych terminologickou poznámku. U JSONu nejde o "dokument" ale o "objekt". Proto je nepatřičné mluvit o DOMu, protože to "D" tady chybí. Jde o to, že JSON je vhodný na "objekty" (Cčkové 'structy'), zatímco XML je vhodné pro dokumenty. (Ostatně jeho bratříčkem je HTML, kde tagy jsou pouze formátovací značky s jasným významem - jeden z důvodů proč je HTML populární.). Objekt má explicitní strukturu (pod konkrétním jménem atributu objektu očekáváme konkrétní datový typ), zatímco dokument může mít struktruru implicitní - do nějakého nodu potřebujeme vložit sekvenci nodů, kdy každý node může reprezentovat nějaký menší dokument.
Děkuji ohledně termínů Pokud budu používat Objekt, bude se to plést s Objektem v C++ nebo s Objektem jako elementem JSONu. Slovo Dokument používám proto, že se to používá u dokumentových databází, kde dokumentem se myslí data zapsaná v JSONu, což je tedy zpravidla objekt. Já jsem hledal vhodné slovo jak nahradit sousloví JSON struktura (taky špatné pojmenování)... V předchozí verzi jsem jednotlivým elementům říkal uzly (node).
Nicméně parsování a sestavení struktury do organizace objektů, které představují jednotlivé elementy JSON formatu hodně připomíná DOM - Document Object Model. Chtěl jsem jen odlišit, že zpracování JSONu a držení dat v nějaké datové organizaci mající shodnou strukturu jako JSON - nazval jsem to DOM jako u XML. Protože koneckonců onu datovou organizaci mohu uložit i v jiném formátu, uměl bych z toho vyrobit i třeba XML.
Jestli znáte lepší pojmenování, klidně navrhněte
Děkuji za názor.
Atomické operace nejsou rozhodně zadarmo. Navíc lokální alokátor není pouze o rychlosti alokace, ale je hlavně o lokálnosti. Pokud vytvářím JSON dokument, a můj jediný cíl je ten dokument serializovat do stringu, tak to nejlepší co můžu udělat je vytvořit lokální alokátor, který bude vracet adresy blízko sebe. Při takovém návrhu se minimalizuje cache miss, který může stát stovky cyklů. Navíc takový dokument můžu uvolnit třeba jedním zavoláním free() (nebo žádným, pokud si vystačím se zásobníkem), a nemusím se starat o uvolňování interních struktur, které jsou všechny alokované z jednoho většího bloku. Takto to dělá např. V8 engine. V podstatě každá high-performance knihovna si řeší alokaci po svém, protože to co nabízí C/C++ jsou generické alokátory.
Jinak netvrdím, že ta knihovna je špatná, jen tvrdím, že není navržena s ohledem na výkon.
No s výše uvedeným pull requestem bude možné zapojit si k tomu vlastní alokátor. Jinak genericky rozumím, šlo by to napsat komplet jako šablony, které by přijímaly alokátor jako další typ, ale ta složitost a přehlednost kódu by byla trochu někde jinde.
Destrukce DOMu není jen uvolnění paměti, ale hlavně destrukce těch objektů. Protože je immutabilita ruku v ruce se sdílením, pak do hry vstupuje i nutnost při destrukci procházet objekty a odečítat reference aby se vědělo, co je třeba všechno zničit. Je to víc zaměřeno směrem, že budu data v DOMu držet spíš než abych jen načet, vyexrtahoval a zničil, případně zaalokoval sestavil a odeslal a uvolnil.
Původně jsem měl v článku v odstavci Motivace, kde jsem se zamýšlel, zda je lepší mít to jako extractor a serializátor dat, nebo jako mnohem bohatší nástroj. Protože serializaci do JSONu nepotřebuju dělat přes DOM a čtení JSONu mohu dělat pomocí události bez překladu dat z jednoho uspořádání do druhého
Pokud je to tak, že rychlost vlastního alokátoru per-thread je srovnatelná s malloc/free, pak samozřejmě svůj příspěvek beru zpět, ale rozhodně si to budu chtít změřit.
(Pak má vlastní alokátor ještě stále smysl u konzolových her (i když pro aktuální generace stolních konzolí to už asi neplatí) a zařízení s omezenou pamětí - byť třeba u mobilních aplikací to dnes (skoro) nikdo neřeší.)
zdravim, chcel by som sa spyta, ci existuje nejake porovnanie s niektorymi inymi implementaciami. zaujimala by ma rychlost, CPU a pamat. bez tychto info je to len dalsia implementacia JSONu.
Ono smyslem nebyl dosahovat maximální výkon, i když tedy samozřejmě o výkon jde také. Nicméně není cílem vést žebříčky výkonu. Spíš bylo cílem immutabilita a nástroje pro práci s immutabilními objekty.
Samotný výkon parsování a serializace a manipulace s DOMem nemusí být oslnivá, nicméně hodně se projeví třeba možnost sdílet data mezi vlákny bez zamykání, nebo zřízení cache kde už mám data naparsovaná, atd. To by chtělo najít nějaký příklad, který by se implementoval jedním nebo druhým způsobem.
Celý návrh vychází zejména ze zkušeností, jak pracuji s dokumentovou NoSQL databází. Zpravidla stáhnu dokument, něco v něm změním a zase ho uložím nebo pošlu uživateli, přičemž ono "stažení dokumentu" může být vyřízeno právě z cache. Bez immutability by ono "něco změním" mohlo buď způsobit propagace změn do cache, nebo budu muset stejně dělat hluboké kopie. Při zaručení immutability nemusí kopírovat vše, nový DOM vlastně vzniká za maximálního přispění starého, a kopírování se omezuje jen na přesun sdílěných pointerů.
dakujem za odpoved. takze ide o specificku implementaciu na jeden konkretny problem. mne islo iba o serializaciu a parsovanie. zoberiem objekt, zoserializujem a odoslem json prec a naopak.
Muzete to tak chapat. Nicmene si myslim, ze tohle je na sirsi diskuzi presahujici ramec clanku... snahou bylo i zamyslet se, jestli by nebylo vyhodne nektera data drzet v DOMu a neprevadet je sem tam do vnitrnich reprezentaci a zpet. Tedy nejde jen o samotny json. Hodnotu ma i hotovy system imutabilnich objektu, jako ze to je taky hype o kterem vsichni mluvi ale nikdo to poradne neumi uchopit a vyuzit.
Pokud bych resil pouze garanci nemennost dat. V cem je lepsi mit specialni "immutable" strukturu proti sdileni instance "bezne" struktury pomoci const reference?
Pokud umožňujete kopírovat sdílením, tedy že máte konstruktor který přijímá const referenci a provede sdílení, pak vám const nepomůže, protože ho tímto způsobem obejdete
Pseudo příklad
const Value cv = {10,20,30}
Value v (cv);
v.push_back(40); // < změní se i cv!!;
Musel byste se vzdát jiných věcí. Tak například kopírování sdílením... tedy vytvářet plnohodnotné kopie. Kopírovací konstruktor by při přijmu const hodnoty prostě nemohl sdílet, musel by kopírovat. Přitom pokud kopírujete do objektu o kterém víte, že taky bude pouze const, pak je vytváření hluboké kopie zbytečné. Kopírovací konstruktor však tuhle informaci nemá
Value x(y); // potřebuju kopii
const Value x(y); // mohu sdílet.
Já to ze začátku řešil tak, že jsem měl dvě třídy. Value a ConstValue. Objekty Value by obsahovaly měnitelné kontejnery, objekty ConstValue by se měnit nedaly. No jenže co když chcete mít měnitelný kontejner konstantních kontejneru? Kterou třídu na to použijete? Já zavedl třetí....
No výsledek nebyl vůbec přehledný. Navíc jsem časem zjistil, že používám v zásadě jen ConstValue a objekty, kterým jsem říkal buildery, tedy něco, co mi pomáhá vytvořit kontejner (objekt , pole) než z něho udělám ConstValue. A tak jsem to celé přepsal do současné podoby.
Ptal jsem se proto, ze sam jsem nad podobnou otazku hloubal a klonil se spise k opacnemu zaveru, zajimal mne Vas nahled.
Mozna se tedy zeptam jinak - k cemu je kopirovani sdilenim dobre? Proc proste nepouzit const referenci?
Proc to neresit stylem:
Value x(y); // potřebuju kopii
const Value& x = y; // mohu sdílet. (... lifetime y bych nyni neresil)
Jde Vam o moznost jak se priblizit pouziti STL kontejneru const objektu nebo neco jineho?
Celé je to o tom vyhnout se dělání hlubokých kopií. JSON dokument může být velký, může být sestaven z mnoha částí.
Value result = {result1, result2, result3,...,resultN}
pokud result1 až resultN jsou všechno nějaké JSON kontejnery, pak stojíte před otázkou: Výše uvedený příklad
a) udělá kopie výsledků?
b) bude je sdílet?
asi a) je správnější, jenže... pokud každý ten result má několik megabajtů nebo strukturu do 10. úrovně pak přistupujete ke kroku b). Jenže tohle nemůžete udělat, pokud existuje někdo, kdo může obsah result1 až resultN změnit, třeba tak, jak měníte obsah proměnné typu vektor, a protože jsou ty kontejnery sdílené, změna se uplatní všude, kde je se to sdílí. On to bude měnit ve své instanci, která není označená const, přestože vy třeba držíte const verzi. Naprosto stejně funguje JavaScript, jen si to zkuste. Udělám kopii kontejneru, dojde ke sdílení
aa = [10,20,30];
bb = aa;
aa.push(40);
console.log(bb);
>[10, 20, 30, 40]
Takže výsledkem je, že nemohu použít ani a) a ani b)
Ještě je tu c) - totiž, že ze všeho udělám konstanty. Konstanty mohu sdílet, protože se nemění.
Jak si jednotlivé varianty vypadají při změnách?
a) mohu měnit co chci, mám kopii (tu musím udělat vždy)
b) mohu měnit jen to co není sdílené, nebo přijmout fakt, že se ta změna projeví jinde
c) pokud chci měnit, musím si udělat kopii toho co měním (mělká kopie)
ImtJSON je právě implementace DOMu variantou c).
Tím jsem odpověděl na otázku "k cemu je kopirovani sdilenim dobre?"
"Proc proste nepouzit const referenci?".Když sdílím, const reference nefungují. Mohu si udělat kopii sdílením. Aby const reference fungovaly, musím se vrátit k a).
PS: K variantě a), představte si, že sestavujete JSON dokument odspoda nahoru. Na každé úrovni kopírujete vše ze spodní úrovně tak, jak vkládáte prvky do kontejnerů... já vím, že jde použít std::move a přesouvat místo kopírování... dokud vám někdo ty výsledky neposkytnul jako const referenci.... pak kopírovat musíte
Ještě bych chtěl ke svým příspěvkům dodat, že kromě připomínek ohledně alokátoru se mi jinak Vaše řešení velmi líbí, protože problémy, které popisujete, jsem relativně nedávno taky řešil.
Na serveru, na kterém jsem pracoval, byla zvolena JSON knihovna, která sice dokument sparsuje (údajně) na jednu alokaci (to byla priorita), ale používá sdílení dat (bez COW), takže si musím hodně dávat pozor na to, abych věděl, kdo všechno tu referenci má.
Děkuji za hodnocení, názory i tipy za vylepšení. Projek je na githubu, lze ho forknout takže jakékoliv vylepšení je možné zapracovat přes pull request a budu rád, když to bude mít víc uživatelů a přispěvatelů.
Aktuálně ještě v rámci přípravy dalšího článku jsem pustil některé testy přes valgrind a jeho profiler a zjistil několik zajímavých čísel. Tak například utilitka jsonpack, která komprimuje json pomocí knihovny je složena z dvou kroků: parsování jsonu a následnou serializaci a pakování. Operace malloc zabírá as 4.77% času. Atomické operace 0,10% času. Pro porovnání std::istream::get() zabírá 7.74%. Největšími zdržováky jsou mapy. Vkládání prvku do mapy zabírá 20% celkového času běhu (!).
Rozhodně zajímavá čísla. Budu se tím dál zabývat.
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 51 059×
Přečteno 23 935×
Přečteno 22 867×
Přečteno 20 949×
Přečteno 17 755×