Zrovna tu řeším výkonovou optimalizaci PowerShell skriptů, tak jsem se rozhod, že nějaké rychlé poznatky zveřejním. Normálně nepotřebujete po PowerShellu nějak úžasnou rychlost, ale mým aktuálním úkolem je přeformátovat .TSV soubor (.CSV oddělené TABy) na W3C. Jedná se o firewall log TMG 2010. Denní velikosti jsou v řádech stovek MB.
Hlavní výkonový problém je v tom, že se nejen musí přejmenovat sloupečky. Musí se upravit i hodnoty v nich. W3C třeba potřebuje IPv4 adresu zapsanou normálně lidsky, zatímco TMG ji má binárně ve formě IPv6. Taky je potřeba některé sloupečky spojit a jiné naopak rozdělit. Číselné hodnoty se musí nahradit nějakým konstantním slovem. Prostě peklo.
Psal jsem to už původně s důrazem na rychlost, s co nejmenší spotřebou paměti, ale běželo to třeba tři hodiny. Po nějakých optimalizacích jsem to stáhl o cca 1/3, ale stojí to strááášně moc RAM.
Možnosti optimalizace
Měl jsem výchozí skript a chtěl jsem to vylepšit. Napadly mě tyto možnosti optimalizace:
- původně jsem četl zdrojové .CSV po jednotlivých řádcích (pomocí Import-CSV). Každý z nich jsem rovnou zpracoval pomocí pipe a uložil do cíle. A teprve potom četl další řádek. To je paměťově úsporné, ale brzdí to disk. Takže načíst to celé do paměti?
- vytváření PSObject pomocí Import-CSV bude také zřejmě pomalejší, než když si budu ten soubor číst jako řetězce pomocí Get-Content a parsovat jednoduše pomocí .Split() metody.
- používal jsem různé operátory zabudované do PowerShell. Například -like, -eq. Co to takhle nahradit raději voláním řetězcových metod z .NET framework. Třeba jako .StartsWith(), nebo .CompareTo().
- používám také formátování řetězců pomocí operátoru -f. Není rychlejší řetězce třeba sečítat?
- jak pomalé je volání funkcí? Zrychlí se to, když nahradím powershell funkce přímo jejich kódem? Vzhledem k tomu, že PowerShell je skript, tak je docela možné, že to není žádný rozdíl - na rozdíl od kompilovaného jazyka, který si nemůže "vymýšelt".
- výsledek musím někam ukládat. Ukládám si ho do paměti a teprve nakonec to uložím na disk. Ale mám to raději dávat do pole [string[]], nebo raději do ArrayList?
- ve zdrojovém .CSV je mnoho číselných hodnot, které se musí do W3C nahradit nějakým konstatním řetězcem. Tyhle překlady můžete mít buď uloženy v poli [string[]], nebo v [HashTable]. Bude rychlejší indexovat do hash, nebo do pole?
- práce s řetězci. Potřebujete například vyprodukovat jeden řádek s hodnotami oddělenými TABem. Je lepší to sečíst z jednotlivých řetězců, nebo to pospojovat pomocí třídy StringBuilder?
- můj skript vypisuje průběžně progres. Tzn. po každých 1000 zpracovaných řádcích to vypíše info, abyste se ty tři hodiny nenudili (podmínka ($i % 1000) -eq 0). Co když to odstraním? Jak moc se to zrychlí? Tenhle výpis je celkem zbytečný a přitom se ta podmínka musí vyhodnocovat pro každý řádek.
Poznatky
Import-CSV je celkem podle předpokladů pomalejší a více paměťově žravý, než jednoduchý Get-Content. Je zajímavé, že udělat Get-Content vezme přibližně 6x více paměti, než je velikost zdrojového souboru. Chápu, že zdroj je ASCII, zatímco řetězce v paměti jsou Unicode, ale stejně je to hodně. Import-CSV vezme dokonce 9x tolik paměti, jako je velikost zdrojového souboru. Asi by to šlo ještě optimalizovat pomocí operátoru -ReadCount za cenu trošky rychlosti.
Ukládat výstup do pole [string[]], nebo do ArrayList je překvapivě úplně jedno. Práce s obojím je úplně stejně rychlá a žere to stejně paměti.
Operátory -eq i -ceq jsou překvapivě cca stejně rychlé. Člověk by řekl, že -ceq by mohl být rychlejší, když se nebude muset zabývat velikostmi písma. Ale není to tak. Operátory -eq a -ceq jsou podstatně rychlejší, než volání .CompareTo() z .NET Framework. To mě překvapilo. Ale je to dobrá zpráva pro PowerShellisty, protože to znamená, že ten jazyk je slušně napsán.
Operátor -like je véélmi pohodlný, ale výkonově je samozřejmě dost drahý. Jednoduché konstrukce jako -like 'zacatek*' jsem nahradil pomocí funkce .StartsWith(). A dost to pomohlo.
Formátování řetězců pomocí -f operátoru je překvapivě rychlejší, než prosté sečítání řetězců. Dokonce je rychlejší vyrábět číslo [int] pomocí tohoto operátoru, než volat [Convert]::ToInt32($hexRetezec, 16):
[int] ('0x{0}' -f $hexRetezec)
Volání funkcí se neosvědčilo. PowerShell je skutečně vykonává. Prostě je volá "jako funkce". Takže to má nějakou režii a žere to víc času, než když jejich vnitřní kód jednoduše opíšete několikrát. Funkce mají problém i s parametry, protože PowerShell všechny kopíruje. V PowerShell se skoro nic nepředává jako ukazatel. I když je to třeba pole, nebo ArrayList, tak to jeho obsah při přiřazení kopíruje.
Porovnání indexování do pole [string[]] a do hash vyšlo taky překvapivě. HashTable je rychlejší než pole. Prostě když chcete třeba desátý prvek z pole, nebo desátý prvek HashTable, která obsahuje prvky 1-15. Tak HashTable vám ho dá o cca 10 procent rychleji.
StringBuilder se ukázal, celkem podle předpokladů, jako lepší varianta, než opakované přičítání mnoha řetězců. Když sečítáte řetězce, tak je to kopíruje. Když to děláte s mnoha řetězci, zbytečně mockrát kopírujete ta stejná data. Takže StringBuilder je odpověď.
Odstranění zbytečné podmínky při 1 200 000 opakováních ušetřilo překvapivě cca 30 sekund ze dvou hodin :-) Takže progres tam zůstal.