pátek 15. března 2019

Program v křeči

Řetězce jsou v C# immutable...

Proč je potřeba si to neustále uvědomovat a co z toho vyplývá pro vás a váš styl programování?






Immutable

K immutable toho bylo napsáno již dost, takže jen ve stručnosti:

Pokud je proměnná immutable, znamená to, že je-li jednou vytvořena, nelze ji následně změnit.

Pokud změnu požadujeme, je nová hodnota zapsána na nové místo v paměti. Pokud tedy sčítáme dva řetězce, je napřed vytvořeno volné místo o velikosti součtu jejich délek a do tohoto místa jsou oba řetězce vkopírovány.

Příklad z praxe

Potřeboval jsem analyzovat nějaký řetězec tak, že jsem jej postupně procházel a pro každou pozici vyzobl řetězce délky 1, 2, 4 a 9 znaků. Ty jsem následně s čímsi porovnával... Z určitých důvodů jsem potřeboval zarovnat tyto řetězce na požadovanou délku pomlčkami, což jsem vtipně realizoval jejich přičtením k hlavnímu textu a teprve následně jsem to vyzobal Substringem. Elegantní...

Realizace vypadala nějak takto:

private static void Test1(string text)
{
    for (var iCitac = 0; iCitac < text.Length; iCitac++)
    {
        var znak1 = (text + "-").Substring(iCitac, 1);
        var znak2 = (text + "--").Substring(iCitac, 2);
        var znak4 = (text + "----").Substring(iCitac, 4);
        var znak9 = (text + "---------").Substring(iCitac, 9);
    }
}

Fungovalo to skvěle, sice u větších souborů zpracování chvíli trvalo, ale svedli jsme to na databázi. Ani stín podezření...

Po pár letech jsem řešil obdobnou věc a výše uvedenou rutinku jsem na prasáka zkopíroval. V testech to jelo jedna báseň, ale jak jsem tomu předhodil 200 kB dat, nemohl jsem se dočkat výsledků.

Vzpomněl jsem si na Robertovu přednášku (viz. Další informace) a začal pátrat. Po drobné úpravě už bylo vše, jak má být:

private static void Test2(string text)
{
    var temp = text + "---------";

    for (var iCitac = 0; iCitac < text.Length; iCitac++)
    {
        var znak1 = temp.Substring(iCitac, 1);
        var znak2 = temp.Substring(iCitac, 2);
        var znak4 = temp.Substring(iCitac, 4);
        var znak9 = temp.Substring(iCitac, 9);
    }
}

Schválně, tipněte si, kolikrát se zpracování řetězce zrychlilo při jeho 200 kB délce...
Prozradím v dalším odstavci.

Proč?

Jak už víme ze začátku článku, při sčítání řetězců je v paměti vyhrazeno nové místo pro výsledek, tam je stávající řetězec přesunut a k němu je dokopírována zbývající část. V kontextu 200 kB souboru procházeného znak po znaku to znamená celkem 200000x kopírovat v paměti 200000 znaků. A ještě 4x pro znaky 1,2,4 a 9, takže celkem 800000x. 

Tím, že jsem řetězec prodloužil jen jednou na začátku (nové) metody, jsem se veškeré té dřině vyhnul.

Výsledek je tady:

1300x rychlejší!
Necelá desetina sekundy oproti téměř dvěma minutám,
to už stojí za tu námahu, ne?

Závěr

Takže pokud děláte s řetězci nějaké operace více než 3x, myslete na to. A mrkněte se na třídu StringBuilder. Také neuděláte chybu, shlédnete-li níže uvedenou přednášku Roberta Hakena.

Další informace



Žádné komentáře:

Okomentovat