Klávesové zkratky na tomto webu - rozšířené Na obsah stránky

LINQ a lenivé vyhodnocování

08.02 - 21. února 2010 | ASP.NET 2.0

Občas není na škodu být lenivý, mnohdy se to vyplatí! To platí i o našich programech, zejména o těch, které zpracovávají veliké množství dat. Jistě si říkáte, jak může být můj program lenivý, já přeci chci, aby byl co nejvýkonnější. Ano, to se nevylučuje, právě naopak! :)

Když jsem nedávno psal o tom, že Vracet list je špatné, neshledal jsem se s velikým porozuměním. Tento článek byl o návrhu rozhraní našich tříd. Dneska se podíváme na implementační detaily a výhody plynoucí z vracení (zvracení?) IEnumerable<>.

LINQ2SQL a metoda ToList()

Začněme tedy u nevýhod užívání generického listu v LINQu. Vsadím se, že většina z vás pro vykonání LINQového dotazu do databáze zavolá metodu ToList(). Okamžitě se tím vykoná dotaz a jeho výsledek je natažen do paměti, konkrétně jako položky generického listu. Jistě se tím zvýší výkon při iteraci přes výslednou kolekci. To je super, ale pro pro výpis používat velice drahý obal v podobě listu? List umožňuje kolekce snadno modifikovat, to je jistě skvělá vlastnost, ale také sebou nese zbytečně alokovanou paměť navíc.

Mnohem výhodnější je volat metodu ToArray(), která vykoná dotaz úplně stejným způsobem, ale vezme si jen tolik paměti, kolik opravdu potřebuje, nehledě na to, že samotný .net runtime je na práci s Array optimalizovaný. Jednoduchou změnou můžeme ušetřit spoustu paměti a zlepšit tak výkon naší aplikace. Pokud jsme však do našich rozhraní zanesli IList<>, musíme teď měnit spoustu kódu, protože náš původní návrh byl krátkozraký.

Zpátky k lenivosti

To, co jsme si popsali v předchozích řádcích, nemá s lenivostí stále nic společného, jde jen o malou optimalizaci dychtivého přístupu k datům (eager loading). Ten samozřejmě nemusí být z principu špatný, ale hodí se pouze pro data omezeného rozsahu. Pokud zpracováváme velké nebo předem neznámé množství dat je lepší zlenivět. Tím se opět vracíme k IEnumerable<>.

Vezměme si hypotetický příklad že náš program bude zpracovávat nekonečný zdroj pravdy:

static IEnumerable<Boolean> GetInfinityTruth() {
  while (true) {
    yield return true;
  }
}

Fůj, napsal jsem nekonečnou smyčku. Hele, ale jak jinak byste chtěli udělat nekonečný zdroj pravdy? Pomocí listu? Těžko. ;) Takovýhle kód je zcela validní a bezproblémový. Tedy do doby, než se najde někdo šikovný, komu se hodí list, a nad GetInfinityTruth() zavolá ToList(). Asi nám vyskočí OutOfMemoryException. :)

Vážně, výhodou tohoto kódu je, že (teoreticky) vrací nekonečné množství dat a přesto se s ním dá v klidu pracovat, aniž by nám sežralo adekvátní množství paměti (opět nekonečné). A to díky lenivosti! S takovýmto přístupem se nám totiž vyhodnocuje vždy jen jeden prvek z kolekce a to až v případě, že je opravdu potřeba. Oddalujeme načítání prvku do doby, kdy s ním opravdu budeme něco dělat.

Další výhodou je, že zcela bez problémů, můžeme celou tu nekonečnou pravdu popřít:

static IEnumerable<Boolean> Disclaim(this IEnumerable<Boolean> source) {
  return source.Select(value => !value);
}

Samozřejmě bych to mohl napsat i takhle:

static IEnumerable<Boolean> Disclaim(this IEnumerable<Boolean> source) {
  foreach(bool value in source) {
    yield return !value;
  }
}

Ale to je příliš mnoho psaní. Nepište zbytečný kód!

Teďka, když zavolám GetInfinityTruth().Disclaim(), dostanu nekonečný zdroj nepravdy, tedy samé lži. :) Opět to funguje tak, že je popřen každý konkrétní prvek, až když na něj dojde, ne všechny najednou, to by bylo moc práce. On se totiž příjemce naší nekonečné pravdy může kdykoli rozhodnout, že už ho to nebaví. A co my pak s tím, že jsme mu dopředu připravili krásné lži? Nic. Byla to zbytečná práce. V tomto případě se lenivost vyplatila.

Závěr

„Poslouchaj Roubíček, oni jsou nejspíš blázen. Nás tu krměj o nekonečné pravdě, co my s tím?“ No, je to určitá abstrakce. ;)

Související

Komentáře RSS

  1.  

    Rene

    09.36 - 21. února 2010 | #

    Pěkné kázání o pravdě takhle v neděli. Lazy vyhodnocování mám také rád, ale bohužel je občas matoucí a mnohonásobně dražší než volání metody ToList a ToArray. Představ si, že máš metodu GetUiDescriptors, která na základě volání mnoha dalších služeb a metod business objektů vrátí IEnumerable<I­UiDescriptor>. Sada výsledků není ale samozřejmě ani potenciálně nekonečná.:) Když metodu uděláš lazy (yield return), je výhodou pouze to, že neplatíš žádnou režii, jestliže klient nakonec o výsledek volání nemá zájem a kolekci neprojde, což není moc častý případ. Nepříjemná situace nastane, když klient bude výsledek procházet vícekrát. A to nastane už tehdy, když zavolá Count, aby zjistil počet prvků v kolekci a poté kolekci projde. Veškerá režie spojená s voláním služeb a metod business obejktů se platí 2×. Takže vrátit List (klidně skrytý za IEnumerable) je zde mnohem lepší a režie Listu je nepatrná (odstranění Listu ve prospěch Array je podle mých testů zbytečná mikrooptimalizace). IMO by bylo nejlepší, aby u každé metody bylo důsledně zdokumentováno i se symbolem v Intellisense, zda je či není lazy.

  2.  

    Aleš Roubíček

    11.02 - 21. února 2010 | #

    [1] Rene: Jak jsem v článku psal, vždy je potřeba držet se kontextu. Pokud chci vypsat dvacet položek z databáze na stráku, nevyplatí se lazy loading. Pokud ale zpracovávám data, která tečou z nějaké služby, se kterou mi může spojení kdykoli spadnout, pak mi lazy loading přijde ideální.

    Ad Count. Pokud píšu aplikaci, kde počítám, že pracuji s proudem dat, nebudu chtít tento proud materializovat. Samozřejmě to musí být zdokumentováno, aby se o to nepokusil někdo jiný. :)

Místo pro tvůj názor

Povinné je jméno a komentář, z e-mailu se rozpoznají Gravatary.
Komentář je formátován pomocí Texy! syntaxu.
Například: **tučný text**, *kurzíva*, "text odkazu":adresa.
Internetové adresy jsou převáděny na odkazy.
Na komentáře se můžete odkazovat pomocí [číslo komentáře].

Nový komentář