Deprecated: mysql_connect(): The mysql extension is deprecated and will be removed in the future: use mysqli or PDO instead in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: mysql_connect(): Headers and client library minor version mismatch. Headers:50562 Library:100020 in /home/www/novacisko.cz/subdomains/bredy/init.php on line 11

Warning: Cannot modify header information - headers already sent by (output started at /home/www/novacisko.cz/subdomains/bredy/init.php:11) in /home/www/novacisko.cz/subdomains/bredy/index.php on line 38
Vracíme z funkce objekty - Bredyho blog - Ondřej Novák
Bredyho blog - Ondřej Novák

Vracíme z funkce objekty

Podívejme se blížeji na to, jakým způsobem je implementováno vracení výsledku z volání funkcí. Zboříme při tom všelijaké mýty


Programátoři začátečníci o takových věcech vůbec nepřemýšlí. Naučí se, že každá funkce (členska i nečlenská) může vracet nějaký výsledek. Tímto výsledkem může být hodnota libovolného typu nebo objekt.

Programátoři pokročilejší už jsou trochu více informováni a často vědí, že vracet objekt může mít velký vliv na celkovou rychlost aplikace. A protože neví více, snaží se tomuto vyhnout. Programování tak podle toho vypadá tak jak vypadá. Jen připomínám, že valná část programátorů patří do této skupiny

Zkušení programátoři samozřejmě vědí, jak je předávání výsledku implementováno a dokáží toho náležitě využít. Těmto programátorům doporučuji následující článek jen opakování.

Ještě existuje jedna skupina a to jsou "Javisté". Znalci jazyka Java vědí, že v Javě se nepracuje přímo s objekty, ale s jejich referencí, takže vracet objekt vlastně znamená vracet jeho referenci. Reference je přitom jednoduchá hodnota (ukazatel), a vracení jednoduché hodnoty zpravidla nemá dopad na výkon. Proto tito programátoři zásadně vrací ukazatele v domění, že dosáhnou nejlepšího výkonu své aplikace. Ne vždy však mají pravdu.

Vracíme jednoduchou hodnotu

Pojďme se podívat jak to celé funguje. Nejprve si ukažme návrat jednoduché hodnoty.

int foo(int a, int b)
{
return a+b;
}

Většina C++ překladačů (no spíš prakticky všechny) provádí návrat takové hodnoty přes registr (nebo registry). Je to velice rychlé a nepřínáší to žádné komplikace.

Na procesorech x86 je návratová hodnota vracena v AL, AX nebo v EAX. 64-bitová je vracena v EDX:EAX (na 32-bitovém stroji). Nedochází zde k žádnému kopírování dat v paměti, a překladač může velmi dobře optimalizovat kód, protože nemusí výsledky odnikud vyzvedávat atd...

Vracíme malý objekt

class MalyObjekt
{
short a;
short b;
public:
// ....
};

MalyObjekt foo(int a, int b)
{
MalyObjekt x(a,b);
return x;
}

Malým objektem se zde rozumí objekt, který se svou velikostí vejde do registru. Překladač pak generuje kód, kterým objekt přenese celý v registru a tím si ušetří spoustu času a práce.

Má to však spoustu podmínek, například tu, že kopírovací konstruktor a operátor přiřazení je plně v režii překladače (tedy že nedefinujeme vlastní)

Vracíme objekt z template nebo inline

Než se vrhneme na pravidla vracení objektů, je třeba podotknout, že překladač se jinak chová tam, kde voláme funkci o které ví vše než tam kde voláme funkci, ze které zná jen její prototyp. Typickým příkladem první situace jsou template funkce (jenž si překladač vyrábí přímo na míru danému volání), nebo inline funkce (kde je možnost místo volání provést vložení kódu přímo na místo volání).

V tomto případě se opět můžeme obdržet velmi efektivní kód, který nic nekopíruje, nic neukládá a nevyzvedává, výsledné objekty je schopen ihned použít.

Pravidla vracení objektu

V okamžiku, kdy zavoláme funkci o které známe jen jeji prototyp, musí překladač generovat kód podle předem dohodnutého protokolu. Neví totiž nic o volané funkci. Nezapomeňme, že program se skládá s kompilačních jednotek, které se překládají samostatně a výsledky z těchto překladů jsou linkovány linkerem. Ten už nemůže do těchto vztahů nijak zasáhnout (Posledním hitem Microsoftích překladačů jsou globální optimalizace, kdy i linker může výsledný kód dále optimalizovat, zatím bych na to ale nespoléhal).

Při vysvětlování pravidel vracení objektů je třeba problém řešit na dvou místech.

  • Na straně volajícího
  • Na straně volaného.

Funkci nadeklarujeme například takto:

Objekt foo(int a, int b);

A voláme ji třeba takto

Objekt x;
x=foo(a,b);

Funkce samotná je implementována takto:

Objekt foo(int a, int b)
{
Objekt x;
//...
return x;
}

Protože se objekt nevejde do registru, musí překladač interně na obou stranách provést malou úpravu prototypu funkce.

void foo(int a, int b, Objekt *result);

Ano v tom je celý fígl, při volání funkce se přidává ještě jeden parametr, a to je ukazatel, který obsahuje adresu, kam se má uložit výsledek. Cílem volané funkce je tedy naplnit toto paměťové místo instancí výsledku.

(Dodatek, objekt result v tuto chvíli ještě není inicializován, je třeba jej zkonstruovat).

Komůrka

Jistě leckoho teď napadlo, proč tedy vracet, když mohu předat ukazatel a tak se vyhnout nějakým pravidlům. Přece překladač to dělá stejně.

Ano to je jistě pravda. Ale vracet objekt je často transparentnější a čitelnější a taktéž nemusí nutně přinášet výkonové dopady. Nehlědě na to, že mnohdy může být i rychlejší.

Překladač zde za nás dělá spoustu práce, která by nám zabrala čas. Překladač totiž nejen mění prototyp funkce, ale také musí někde vyhradit místo (komůrku) pro výsledek. To většinou dělá na zásobníku, i když né vždy je to potřeba. Právě rozhodnutí o tom, kdy to má smysl a kdy ne, je na překladači. Z našeho pohledu je tohle vše transparentní. I přesto, že o vytvoření komůrky vždy rozhoduje překladač, můžeme její existenci docela pekně využít.

V komůrce totiž vzniká další objekt, jehož život začíná uvnitř volané funkce a končí (často) středníkem na volající straně. Přitom alokace objektu je velice rychlá a proto tvrdím, že předávat přímo objekt je někdy rychlejší, než předávat pouze ukazatel na objekt alokovaný v heapu (alokace v heapu je pomalejší).

Kopírovací operátor a konstruktor

Zopakujme si kopírování objektů.

  • kopírovací konstruktor, jehož cílem je vytvoři přesnou kopii objektu
  • operátor přiřazení, který nejen kopíruje objekt ale původní objekt musí zničit.

Pokud voláme funkci takto:

Objekt x;
x=foo(a,b);

vzniká při volání funkce foo komůrka pro výsledek na zásobníků. Po návratu komůrka obsahuje výsledný objekt. Protože jsme řekli, že život tohoto objektu končí středníkem, musí se výsledek překopírovat do x. K tomu se využije operátor přiřazení.

Kopírovací konstruktor využijeme při návratu z funkce u příkazu return.

Objekt foo(int a, int b)
{
Objekt x;
//...
return x;
}

Jeho cílem je prostě zkonstruovat objekt v komůrce.

Jak se vyhnout kopírování?

Možná si někdo říká, že je to děsivé. Dvoje kopírování. Z funkce do komůrky, z komůrky do proměnné. Otázka zní, lze to nějak redukovat?

A odpověď zní: ANO!

Podívejme se na možnosti komůrky. Psal jsem, že její platnost končí středníkem. Dalo by se nějak tuto platnost prodloužit?

V normě C++ se píše, že v případě že deklarujeme objekt a zároveň mu přiřazujeme hodnotu, převede se tato operace na volání konstruktoru. Takže

Objekt x=Objekt(...);

se přetransformuje na

Objekt x(...);

Co se stane, když napíšeme.

Objekt x=foo(a,b);

Eliminujeme komůrku!

Jak je to možné? Překladač v tomto bodě ví, že prostor který rezervoval pro x je zatím nevyužit, objekt není inicializován.

To je právě důvod vzniku komůrky při použití ve výrazu (který není inicializací objektu), kdy překladač nemá po ruce žádné místo, kam by se mohl výsledek uložit. Přitom nemůže použít objekt uveden na levé straně rovnítka, protože ten je již inicializovaný. Musel by jej zničit, což ale taky není dobré, protože objekt zároveň může přímo nebo nepřímo figurovat v parametru, nebo jej funkce může potřebovat.

V tomto případě ale překladačí přímo podstrčíme volný prostor s tím, že objekt má vzniknout právě tam. A překladač z prostoru udělá komůrku. Životnost takového objektu však nekončí středníkem, ale je prodloužena až na konec scope.

Všimněme si, že jsme také eliminovali konstruktor.

Objekt x; //konstrukce x
x=foo(a,b); //destrukce x a kopírování

//

Objekt x=foo(a,b); //žádný konstruktor ani kopírování!

Podařílo se nám eliminovat jedno kopírování, ale bylo by možné se zbavit kopírovacího konstruktoru (kopírování výsledku do komůrky)? I zde to lze.

Překladač se řídí výrazem uvedeným za příkazem return. Zde můžeme uvést proměnnou, výraz vedoucí na vznik objektu a nebo... konstruktor.

A právě v případě, že uvedeme konstruktor objektu, který vracíme, přímo předepisujeme překladači, jakým konstruktorem má komůrku inicializovat. Ve všech ostatních případech překladač použije kopírovací konstruktor.

Objekt foo(int a, int b)
{
return Objekt(a,b); //inicializace komůrky konstruktorem
}

Když se nyní vrátíme na volající stranu...

Objekt x=foo(a,b); //1 konstruktor na straně volaného

Už je doufam všem jasné, proč jsem trval na tom, že vracení objektů je někdy rychlejší, než řešit tento problém nějak oklikou. Pokud zapojíme obě optimalizace, docílíme toho, že vracení objektu bude mít stejnou režiii, jako konstrukce objektu na strane volajícího. Režie návratu objektu se minimalizuje na pouhé předání ukazatele.

Optimalizace na straně překladače

Následující část se týká už jen některých překladačů (například VS2005 už toto také prý podporuje). Jedná se o optimalizace kódu na straně překladače s využitím výše uvedených pravidel.

Pokud totiž napíšeme

Objekt x; 
x=foo(a,b);

pak hloupý překladač skutečně generuje konstruktor a kopírovací konstruktor. Chytrý překladač pozná, že se s objektem mezi vznikem a přiřazením nic neděje a přepíše tento kód na

Objekt x=foo(a,b);

Optimalizace lze dělat i na straně volané funkce. Pokud napíšeme:

Objekt foo(int a, int b)
{
Objekt x;
//...
return x;
}

překladač nám umožní, aby objekt x byl vytvořen přímo v komůrce. Při běhu funkce můžeme na objekt volat metody, které se provádí přímo nad komůrkou a nikoliv "někde vedle" a následně výsledek kopírovat

Závěr

Doufám, že jsem čtenáře přesvědčil, že vracet objekt má svůj význam, a že nemusí mít vždy výkonnostní dopady. Mezi další výhody patří naprostá modularita tohoto systému. Optimalizaci totiž provádíme na té straně, na které ji potřebujeme. Například když volající strana ví, že nemůže optimalizovat a musí využít operátoru přiřazení. Může se rozhodnout libovolně, aniž by toto bylo nutné ošetřovat na volané straně. Stejná situace nastane i na volané straně, která nemusí řešit, kam má výsledek uložit.
Jakýkoliv jiný systém nám může svazovat ruce a nutit psát neefektivní kód. Budeme-li vracet ukazatel, je nutné řešit vlastnictví ukazatele, čili kdo je zodpovědný za destrukci objektu. Nemůžeme objekt použít uprostřed výrazu, protože se objekt implicitně nezničí. Budeme-li naopak do funkce předávat referenci na objekt, do kterého se má uložit výsledek, je volání takovéhle funkce docela těžkopádné, musíme zakládat proměnnou a nemůžeme začlenit funkci do složitějšího výrazu.

Nebojte se vracet objekty.

Vracíme z funkce objekty - praktická ukázka

vytvořeno: 11.10.2006 13:31:43, změněno: 23.5.2007 11:58:18
Jsou informace v článku pro Vás užitečné?
  • (10)
  • (0)
  • (0)
  • (2)
  • (2)
Nick nebo OpenID
Vzkaz
 
25.7.2016 18:26:08

423L5BMrfH

I hardly leave a response, however after browsing a bunch of remarks on Inorcduting Kink To A Vanilla Woman | Unspeakable Axe. I do have a couple of questions for you if you do not mind. Is it only me or do some of these comments look like they are coming from brain dead people? And, if you are writing on other online sites, I would like to keep up with everything fresh you have to post. Could you make a list of the complete urls of all your social community sites like your twitter feed, Facebook page or linkedin profile?
2.7.2014 15:40:52

yCvqKxdG

ach hatte deine frage im netzt nicht beantwortet woltle noch sagen das ein stuhl gerade wegen der sitzfle4che die einem suggeriert sich setzten zu kf6nnen auf jeden fall ein interface besitzt, also weil er eine Anzeichenfunktion besitzt wie zb der rasierer
9.2.2009 09:48:53

Sleeper

Tohle jsem se v učebnici nedočetl. Díky moc! Moc mi to pomohlo.
22.12.2008 12:06:16

Ondra

Pole obecně vracet nelze, ani v C ani C++. Pole totiž není standardní objekt, ale spíš výjimka. Ale můžete si pole vyrobit jako objekt. Takové nejjednodušší řešení je vyrobit si pole jako šablonu

..... template<class T, int count>
struct Pole {
T data[count];
}; .....

Použití šablony je jednoduché, skoro nám nahradí vlastní pole. Pole<int, 10> představuje desetiprvkové pole int. To můžu vrace normálně jako objekt a platí pro něj všechny pravidla výše popsaná. K úplnému pohodlí je třeba ještě dodělat nějaké ty operátory závorek a podobně. Jediný, co nejde je možnost konstrukce pomocí { }... Tahle vymoženost se chystá až v normě C++0x, na kterou se samozřejmě docela těším.........

Použití vektoru je optimální stejně jako u jiného objektu. Problém je akorát v tom, že vektor nemá inicializaci výčtem prvků. Tam se kopírovacímu konstruktoru nevyhneme. Opět řešením nabídne až C++0x. V obou případech můžeme objekty rozšířit o vlastní konstruktory, například o konstruktor se šablonou (viz: Vracíme z funkce objekty - Skrytí konstruktoru), tím se vyhnout kopírovacímu konstruktoru, nicméně, je to víc práce a méně přehledný kód.
21.12.2008 23:44:41

optik

Jak je to s poli a jejich vracením? Pokud mám pole předem známé délky, tak mi asi nic jiného než použití ukazatele nezbude, dělat to přes vracení vectoru je elegantnější ale optimálnější to asi nikdy nebude?

Podobné články

Vracíme z funkce objekty - praktická ukázka

V článku Vracíme z funkce objekty jsem ukazoval, jak probíhá návrat objektu z funkce a jak toto optimalizovat tak, aby se minimalizovalo používání kopírování. Teď si to ukážeme prakticky - použil jsem gcc bez optimalizací

Vracíme z funkce objekty - problémy

Při praktickém používání technik Vracíme z funkce objekty narazíme na nečekané a možná nelogické problémy.

Vracíme z funkce objekty - Skrytí konstruktoru

Skrývání konstruktoru může být důležité v případě, že potřebujeme v objektu použít Pimpl Idiom. Objekty totiž mohou vznikat i z datových typů, které nechceme zveřejňovat

Jak provést operaci až po příkazu return

Pokud optimalizujeme kód tak, abychom mohli využít výhod vracení objektu z funkce, narazíme občas na problém, kterak provést některé operace až po provedení příkazu return

FastAllocPool - urychlení častých alokací a dealokací

Pokud v programu z nějakých důvodů potřebujeme často provádět new a delete nad některými třídami, můžeme zvýšit efektivitu těchto operací zavedením poolu předalokované paměti
Reklama: