Optimální kód v Delphi - Builder.cz - Informacni server o programovani

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:



Delphi

Optimální kód v Delphi

serial

21. srpna 2002, 00.00 | Cílem tohoto článku je popsat problematiku optimalizace kódu v Delphi. Vznikl shromážděním mnoha různých, převážně cizojazyčných, dokumentů a poskytuje tak ucelené informace z tohoto oboru.

{Úvod, Hlavní zásady}

Co je to vlastně optimální kód

Jako optimální kód bych označil takový zápis programu, který nemá žádné závažnější nedostatky ve svém výkonu, přestože obecně panuje domněnka, že optimální rovná se nejrychlejší. Je to věc čistě subjektivní a záleží především na tom, co od programu očekáváme. Malá utilitka nebo středně velká aplikace nejspíš větší optimalizaci kódu potřebovat nebude, opakem může být například 3D aplikace.
Pokud dojde na optimalizaci, je dobré vytyčit si jasný cíl a vědět kdy s ní skončit, například pokud si programátor určí 28 fps ve své hře.

Rychlost vs. Čitelnost

Mnoho lidí se domnívá, že s rychlým kódem jde ruku v ruce ošklivý a nečitelný kód. V některých případech tomu tak opravdu bývá a vyhnout se tomu dá jen těžko, přesto však obecně platí, že takový kód může být nejen dobře čitelný, ale dokonce i velmi elegantní.

Úroveň optimalizace

Při psaní složitějšího programu jste určitě někdy narazili na to, že napsaný algoritmus je pro vaše potřeby příliš pomalý. Je třeba rozhodnout, zda je daný algoritmus ten vhodný, a případně se poohlédnout po jiném. Optimalizace má obecně dvě úrovně - optimalizace na úrovni algoritmu a na úrovni implementace algoritmu. Jestliže si jste jisti, že algoritmus je kvalitní, nebo přinejmenším nejlepší možný, přichází na řadu poupravit jeho kód, tedy to, jakými prostředky Object Pascalu je zapsán.
V žádném případě to neznamená, že by jste se měli pokoušet optimalizovat každičký řádek svého programu. Postačí vylepšit ty části, pro které je rychlost důležitá, nebo identifikovat ty části, které jsou opravdu příliš pomalé.
Velký klam je to, že optimalizace přichází až tehdy, když je kód hotový. Mnohem lepší a z programátorského hlediska také profesionálnější je naučit se psát kvalitní a rychlý kód už od začátku. K tomu je ovšem potřeba se seznámit se všemi pravidly psaní rychlého programů, a právě to má být hlavním tématem tohoto článku.

Hlavní zásady

V jednoduchosti je síla

Překladač (kompilátor, compiler) Delphi automaticky při překládání tvého kódu provádí některé zásadní optimalizace, obsahuje totiž vlastní optimizátor (optimizér, optimizer). Ten se stará nejen o jednoduché věci jako je třeba odstraňování zbytečných řádků kódu, jeho hlavní prací je pochopit náš "složitý" kód a upravit jej tak, aby se dal co nejefektivněji přeložit do spustitelného souboru.
Platí pravidlo, že čím je kód jednodušší, tím úspěšnější optimizer je. Doporučuje se v jedné rutině nepoužívat více než 5 proměnných (variables). Není také radno provádět během jedné smyčky příliš mnoho operací, to totiž často vede k realokaci řídících nebo pomocných proměnných. Často je výhodné dlouhé smyčky rozdělit do více jednotlivých nebo v nich prováděné operace přemístit do samostatné funkce nebo procedury (obecně rutiny). Příjemným vedlejším efektem bývá také zvýšení čitelnosti takového kódu.

Používejte lokální proměnné

Jen lokální proměnné, tedy ty, které jsou definované přímo v rutině, mohou být při jejím spuštění převedeny do procesorových registrů, což je velmi významné pro rychlost. Někdy je výhodné proměnné do lokálních proměnných vynuceně zkopírovat, zejména pokud jsou použity v nějaké smyčce. To se týká hlavně vlastností tříd (tedy třeba komponent).
Výjimka z tohoto pravidla jsou pole (arrays) s elementy některého jednoduchého typu (třeba integer, byte, char, atd.). Ty je někdy výhodné zařadit mezi globální proměnné, ale samozřejmě jen tehdy, pokud to má v rámci celého programu nějaký větší smysl.

Malé množství parametrů

Hodně často používané rutiny by neměly mít více než tři parametry, neb tři je přesně počet proměnných, které mohou být v jeden čas umístěny do registru. Tímto se rychlosti procesorových registrů využije na maximum a optimizer dostane větší šanci vylepšit váš kód. Metody tříd však mají skrytý parametr self, kterým odkazují na konkrétní instanci dané třídy, zbývají tedy tak už jen dva parametry k umístění do registrů.

Ukazatele

Cenná technika je využívat výhod, které přinášejí ukazatele (pointers). Jejich používání přináší optimizeru mnoho informací o tvém kódu, ale to samozřejmě neznamená abyste všechny objekty referencovali jako ukazatele. Pokud definujete objekt třeba takto

var Soubor   : TObject;
      Ukazatel : Pointer; 

možná ani nevíte, že proměnná Soubor je vlastně ukazatel, ukazuje na určité místo v paměti a navíc určuje, jaká data jsou v daném paměťovém úseku zapsány. Toho lzes výhodou využívat, a do proměnných Pointer ukládat reference na objekty jakéhokoli typu.

begin
   Soubor := TObject.Create;
   Ukazatel := Soubor;
   TObject(Ukazatel).Free;
  end; 
  

Pole

Pokud to jde, nepoužívejte dynamická pole. Práce s nimi je oproti klasickým (tedy statickým) polím o dost pomalejší. Musíte-li je použít, nezapomínejte na funkce High or Length pro zjišťování velikosti pole. Ve skutečnosti fce High volá fci Length, Length je proto doporučovanější. Nezjišťujte ale opakovaně velikost pole těmito funkcemi, je rychlejší jej zjistit jednou a uložit do lokální proměnné. Jednou z mnoha vlastností Delphi ulehčujících nám práci jsou tzv. open arrays. Příkladem jejich použití budiž tento kód:

var MalaTabulka      : Array[1..10]  of byte;
      VelkaTabulka     : Array[1..100] of byte;
      LibovolnaTabulka : Array of byte;

  procedure VypisTabulku(Tabulka : array of byte);
  var I : LongInt;
  begin
   for I := 0 to Length(X) - 1 do VypisHodnotu(Tabulka[i]);
  end;

Jak vidíte, parametr Tabulka neudává velikost předávaného pole. Problémem této pohodlné věci je, že volání procedury VypisTabulku vytvoří lokální kopii celého pole, což, kromě faktu že se tím plýtvá pamětí, není zrovna nejrychlejší operace. V tomto případě je vhodné parametr Tabulka předznamenat klíčovým slovem const nebo var, aby se tím zabránilo vytváření lokální kopie. Ještě je třeba poznamenat, že použití const zabrání programátorovi ve změně celého pole, ne už tak jeho jednotlivých elementů, takže pozor. [-more-]{Konkrétní metody optimalizace}

Konkrétní metody optimalizace


Množiny

Pokud pro přidávání/odebírání členů do/z množiny (set) používáte klasické operátory (+ a -), vězte, že o něco rychlejší je práce s rutinami Include a Exclude.

Smyčka for

Jedna z nejčastějších konstrukcí v Object Pascalu a zároveň nejsložitější prácička pro optimizer. Například smyčka

for I := M to N do
      A[i] := A[i] + B[i]

Bude optimizerem převedena do

  PA := @A[m];
  PB := @B[m];
  Pocitadlo := M - N + 1;
  if Pocitadlo > 0 then
     repeat
       PA^ := PA^ + PB^;
       Inc(PA);
       Inc(PB);
       Dec(Pocitadlo);
     Until Pocitadlo = 0;

Existují jiné možnosti, ale tato je nejčastější, a je z ní dobře vidět, kolik dodatečné práce zdánlivě tak jednoduchá věc, jako je for smyčka, vyžaduje. Platí tedy, že čím jednodušší svou smyčku uděláte, tím rychlejší bude, a proto může být někdy lepší složité smyčky rozdělit do několika menších.

Interfacy

Nevyžadují příliš mnoho optimalizační údržby. Obsahují v sobě tzv. reference counter (doslova přeloženo jako čítač referencí, odkazů), který zajišťuje uvolnění paměti okupované jednou instancí. Pokaždé, když je daný interface například přiřazen některé proměnné, zvýší se RC o jeden, a analogicky se sníží v momentě kdy je některý odkaz na tento objekt zrušen. Dosáhne-li RC nuly, je i objekt zrušen. Proto si dávej pozor a vyhýbej se známé technice, která vyžaduje opakované rušení a znovuvytváření interface.

Paměťové zarovnávání

Používejte 32-bitové proměnné kdekoli a kdykoli je to možné. Současné procesory jsou už nějaký ten pátek 32-bitové a práce s 4 bytovými bloky dat je proto nejrychlejší. Pro celočíselné proměnné to tedy nejčastěji budou DWord, Longint nebo třeba Cardinal. Když musíte používat proměnné o jiné velikosti (například z důvodů kompatibility), změňte je prostým přiřazením nejdříve v 32-bitové, a až když je potřeba, zpět na původní velikost.

Zrychlování smyček

je jedna z nejčastějších optimizačních technik. V angličtině se tomu říká loop unrolling. Z vlastních zkušeností ale můžu říct, že tato technika je účinná jen u relativně malých smyček a to navíc jen při použití konstrukce while. Viz. Tento kód:

  I := 0;
  while I < Count do
  begin
    Data[I] := Data[I] + 1;
    Inc(I);
  end;

Tato smyčka může být snadno zrychlena následujícím způsobem

  I := 0;
  while I < Count do
  begin
    Data[I]  := Data[I] + 1;
    Data[I + 1] := Data[I + 1] + 1;
    Inc(I, 2);
  end;

Tímto se sníží počet průchodů while smyčkou. Problém této techniky tkví v potřebě dodatečného kódu v případě, že Count je lichá hodnota a není tedy dělitelná dvěma, a také v tom, že se tím komplikuje čitelnost kódu. Poznámka: Nemusíte se samozřejmě omezovat jen na dvě operace uvnitř smyčky. Typicky se jich však pro tento účel nedoporučuje používat více než 4, a to nejen kvůli přehlednosti, ale zejména kvůli přílišné "složitosti" kódu pro optimizer. Další připomínkou je, že vedle while se dá podobně dobře zrychlit i konstrukce repeat.

Další pravidla pro smyčky

Uvnitř smyčky používejte co nejméně podmínek if, zvláště když taková podmínka volá jiné rutiny. Také se osvědčuje omezit množství řídících podmínek pro konstrukce while a repeat. Samozřejmě se ale ve většině případů složitějším podmínkám nevyhneme.

Užívejte výhod Exit, Break a Continue

Přestože tyto vlastnosti Object Pascalu bývají považovány za známku špatného a nekvalitního kódu, mají své místo, a to hlavně tam, kde je důležitá rychlost. Pro ostatní případy se doporučuje používat standardní konstrukce s podmínkami a bloky begin…end.

for a while

Tam, kde je předem známý počet průchodů smyčky, se nejčastěji používají konstrukce for nebo while. Typicky bude zvolena spíše for, pro svou jednoduchost. V některých případech je ale while o něco efektivnější a optimizer také někdy, jak už jsem ukázal na příkladu výše, konstrukce for převádí na konstrukce while. Platí, že tam, kde jsou v těle smyčky použita pole s jednotlivými elementy o velikosti 1, 2, 4, nebo 8 bytů, nebo nejsou pole použita vůbec, je použití while výkonnější. Ale tam, kde se objevují zejména vícerozměrná pole nebo pole s elementy o jiné velikosti, bývá rychlejší naopak for.
Existuje však jedna výjimka - když není v těle smyčky nikde použita řídící proměnná a jde vám tedy jen o určitý počet provedení nějakého kódu, je for efektivnější možností.
Poznámka: Zajímavou vlastností while v tomto případě je, že dosahuje většího výkonu pokud řídící proměnná nabývá negativních hodnot a během jednotlivých průběhů směřuje směrem k nule.
[-more-]{Konkrétní metody optimalizace II.}

case

Implementace case je kompilátorem poměrně dobře zvládnutá, i tato konstrukce se však dá dále optimalizovat. Nejlépe si to ukážeme na příkladu:

  case x of
    100 :DoSomething1;
    101 :DoSomethingFrequently2;
    102 :DoSomething3;
    103 :DoSomething4;
    104 :DoSomething5;
    105 :DoSomething6;
    106 :DoSomething7;
    107 :DoSomething8;
    200 :DoSomething9;
    201 :DoSomething10;
    202 :DoSomething11;
    203 :DoSomething12;
    204 :DoSomething13;
    205 :DoSomething14;
    206 :DoSomething15;
    207 :DoSomething16;
  end;

Je zde vidět poměrně veliká mezera mezi hodnotami 107 a 200, což brání optimizeru v efektivnější optimalizaci kódu. Upravíme jej jako:

  case x of
    100..107 : 
	  case x of
        100 :DoSomething1;
        101 :DoSomethingFrequently2;
        102 :DoSomething3;
        103 :DoSomething4;
        104 :DoSomething5;
        105 :DoSomething6;
        106 :DoSomething7;
        107 :DoSomething8;
	  end;
	200..210 :
	  case x of
        200 :DoSomething9;
        201 :DoSomething10;
        202 :DoSomething11;
        203 :DoSomething12;
        204 :DoSomething13;
        205 :DoSomething14;
        206 :DoSomething15;
        207 :DoSomething16;
         end;
  end;

Když víte, že některé hodnoty v case se objevují častěji, je dobré je umístit ještě před samotnou konstrukci:

  if X = 101 then
    DoSomethingFrequently2 else
    case X of
      100 :DoSomething1;
      102 :DoSomething3;
      103 :DoSomething4;
      104 :DoSomething5;
      105 :DoSomething6;
      106 :DoSomething7;
      107 :DoSomething8;
    end;

Výčtové typy

Pokud to jde, vyhněte se výčtovým typům. Například typ TRozsah = 0-6000 bude uložen interně jako Word, což, jak jsme si už řekli, je neefektivní. Podobně bude množina o dejme tomu 250 elementech uložena jako byte.

Celočíselné násobení

Násobení celočíselných typů je doslova utrpením pro starší procesory jako je 80486, Pentium či Pentium Pro. Situace se zlepšila s příchodem Pentia II. Kompilátor si toho je vědom a všude, kde to jen trichu jde, nahrazuje takové násobení jinými operacemi. Pokud tedy píšete kód, pro který je rychlost kritická, musíte uvažovat i cílový systém a optimalizaci tomu přizpůsobit.

Zkracování podmínek

Porovnáváte-li proměnnou s více jednoduchými typy najednou, je přehlednější a efektivnější použít místo nich výčet. Nejlepší bude ukázat si to na příkladu:

  if (((C > = 'a') and (C < = 'z')) or ((C > = '0') and (C < = '9'))) then
     DoSomething; 
  
  // změňte na
  if C in ['0'..'9', 'a'..'z'] then
     DoSomething;

  // případně na
  case C of
     '0'..'9', 'a'..'z' : DoSomething;
     End;

Dlouhé řetězce (AnsiString)

Nepoužívejte asociace pro počáteční vynulování řetězce na začátku rutiny. AnsiString (a tedy tím pádem string při standardním nastavení Delphi) je automaticky při své inicializaci nastaven na prázdný řetězec. Používání operací typu S := S + S2[I] ve smyčkách způsobuje dodatečnou a pomalou alokaci paměti při každém takovém zvětšení stringu. Abyste tomu předešli, zavolejte nejdříve SetLength(S, Nova_Delka). Toto je bohužel velmi častá chyba programátorů, způsobená jen tím, že zvětšování a zmenšování dynamických řetězců je v Delphi obstaráváno automaticky.

Procedura Copy

Častokrát je k vidění, že programátoři pro odstranění určité části řetězce používají funkci Copy. Například odstranění prvních 20ti znaků může vypadat takto:

S := Copy(S, 21, Length(S) - 20);     // nesprávně!!!
Delete(S, 1, 20);                     // mnohem, mnohem rychlejší

string jako PChar

Potřebujete-li z nějakých důvodů na proměnou dynamického řetězce (definovanou třeba jako S) odkazovat jako na PChar, máte tři ekvivalentní možnosti, přičemž nejrychlejší je ta poslední. První je použít přímo PChar(S). Druhá tkví v referenci na první znak řetězce, tedy @S[1]. Tou poslední je ukázat na řetězec přímo ukazatelem, použít tedy klasický Pointer: Pointer(S).

Datové typy s plovoucí čárkou

Tyto floating types poskytují velikou flexibilitu, ale také mohou zabrat mnoho paměti. Stále platí, 32-bitové typy jsou nejrychlejší. Povětšinou ale používáme tyto typy také pro jejich často veliký rozsah, proto někdy je nutné oněch 32-bitů nedodržet.
První pravidlo zní, používejte Extended jen když je to nezbytně nutné. Jejich velikost (10 bytů) je zejména pro základní operace (především +, -, *) vysoce neefektivní, a projeví se to ve ztrátě výkonu.
Dále, snažte se tyto typy nemixovat - pokud už musíte používat některé z těchto "desetinných" typů, drž se jen jednoho z nich.
V případě, že definujete konstantu některého typu s plovoucí čárkou, udělejte z ní konstantu s přiřazeným datovým typem, a to Single nebo Double (const X : Single;). Pokud to neuděláte, bude uložena jako Extended, což je silně nepraktické.
Namísto Trunc používejte Round. Ta je na Pentiu II přibližně 2,74x rychlejší.
Vyhýbejte se dělení kdekoli to jen jde, zejména uvnitř smyček. Je asi 35x pomalejší než ostatní běžné operace (!).

Poslední rada ohledně těchto typů říká, nepoužívejte je jako výstup funkce. Místo toho definujte proceduru a daný výstup vracejte pomocí var:

  function Fnc(X : Double) : Double;
    begin
      Result := X * 1.8 + 2;
    end;

  // po úpravě
  procedure Fnc(X : Double; var Result : Double)
    begin
      Result := X * 1.8 + 2;
    end;


Části článku převzaty z www.optimalcode.com s laskavým svolením R.Lee

Tématické zařazení:

 » Rubriky  » Delphi  

 » Rubriky  » Windows  

 

 

 

Nejčtenější články
Nejlépe hodnocené články

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: