Vložené procedury a metody

vložil Radek Červinka 27. května 2010 23:01

Předchůdcem anonymních metod z Delphi 2009 jsou vložené (nested) procedury. To ale neznamená, že by éra vložených procedur skončila. Osobně je rád používám na zpřehlednění kódu v případě trošku delší procedury nebo na lokální provedení opakované akce. Hlavní výhodou je možnost přístupu k lokálním proměnným dané procedury.

Je samozřejmé, že vložené procedury můžete použít i v případě metod třídy. Pokud se ptáte, proč raději třeba v případě metod nenapsal místo vložené procedury další metodu tak odpověď zní: další izolace kódu. Proč kód, použitý jen jednou v konkrétním případě, popř. proměnné použité jen na jednom místě, nějak publikovat i jen v rámci třídy?

Ukáži možnosti a na co si dát pozor z hlediska optimalizace v jednotlivých případech. Jen na začátek: (skoro) každé volání vložené procedury něco stojí, a to něco může být v případě častého volání (např. v cyklu) nepříjemné. Ale to platí obecně o volání metod a procedur.

procedure TestA;
var
  s: string;
  procedure TestA_A;
  begin
    s := s +'text';
    writeln(s);
  end;
begin
  s := 'TestA';
  TestA_A;
end;

Základní a asi nejpoužívanější varianta. TestA_A má přístup k lokálním proměnným (v tomto případě jen k proměnné s) a mohou je měnit. Lehce nebezpečné, ale celkem efektivní: malá ztráta při volání. Nevýhodou je, že nemůžete TestA_A označit jako inline (DCC Error: Project1.dpr(14): E2449 Inlined nested routine 'TestA_A' cannot access outer scope variable 's'), což si myslím, že je škoda.

procedure TestB;
  procedure TestB_A(var s2: string);
  begin
    s2 := s2 + 'text';
    writeln(s2);
  end;
var
  s: string;
begin
  s := 'Test B';
  TestB_A(s);
end;

V tomto případě z TestB_A nelze přistupovat k lokálním proměnným (deklarace je před deklarací proměnné), tj. případné proměnné je nutné předat jako parametry. Mimochodem to znamená, že můžete použít inline a tím (pokud ji kompilátor použije a to většinou ano) odstraníte režii spojenou s volání, přesto je kód strukturován.

Pokud potřebujete ve vložené proceduře lokální proměnnou, musí Delphi kompilátor vygenerovat trošku šachování se zásobníkem, což v případě intenzivního volání např. v cyklu může být problém. Můžete zkusit přidat inline, ale druhé řešení i když ne moc kosher, je raději deklarovat požadovanou proměnnou jako lokální v nadřízené proceduře. Ale obecně se tím moc nezabývejte, to by muselo být volání opravdu intenzivní.

Mnohem vážnější je nepoužívat při předávání const, což platí obecně pokud v rámci metody nebo procedury nebudeme parametr měnit. Z hlediska optimalizace to platí hlavně pro složitější datové typy jako string, record, Variant nebo pole.

Mějme tuto opravdu užitečnou proceduru:

procedure TestC;
var
  s: string;
  procedure TestCA(const s2:string);
  begin
    writeln(s2);
  end;
  procedure TestCB(var s2:string);
  begin
    writeln(s2);
  end;
  procedure TestCC(s2:string);
  begin
    writeln(s2);
  end;
  procedure TestCD(const s2:string); inline;
  begin
    writeln(s2);
  end;
begin
  s := 'Test C';
  TestCA(s);
  TestCB(s);
  TestCC(s);
  TestCD(s);
end;

Pokud jednotlivá volání seřadíme podle náročnosti volání, tak je to TestCD (žádná ztráta, volání je skoro jistě eliminováno a kód procedury je vložen přímo), TestCA a TestCB (v tomto případě je to jedno, ale varianta s const je preferovaná a navíc zaručuje, že se nespletete a nedojte náhodou k chybnému přiřazení), pak dlouho nic a pak TestCC.

Podíváme na výpis v assembleru - nejdříve varianta s const:

Project1.dpr.41: begin
00408BB0 53               push ebx
00408BB1 8BD8             mov ebx,eax
Project1.dpr.42: writeln(s2);
00408BB3 A1F4A94000       mov eax,[$0040a9f4]
00408BB8 8BD3             mov edx,ebx
00408BBA E8A9BCFFFF       call @Write0LString
00408BBF E860A9FFFF       call @WriteLn
00408BC4 E813A2FFFF       call @_IOTest
Project1.dpr.43: end;
00408BC9 5B               pop ebx
00408BCA C3               ret 

Prostě přímočaré - 26 bajtů + 8 bajtů volání. A nyní varianta bez const:

Project1.dpr.49: begin
00408BE8 55               push ebp
00408BE9 8BEC             mov ebp,esp
00408BEB 51               push ecx
00408BEC 8945FC           mov [ebp-$04],eax
00408BEF 8B45FC           mov eax,[ebp-$04]
00408BF2 E8A9BBFFFF       call @LStrAddRef
00408BF7 33C0             xor eax,eax
00408BF9 55               push ebp
00408BFA 68328C4000       push $00408c32
00408BFF 64FF30           push dword ptr fs:[eax]
00408C02 648920           mov fs:[eax],esp
Project1.dpr.50: writeln(s2);
00408C05 A1F4A94000       mov eax,[$0040a9f4]
00408C0A 8B55FC           mov edx,[ebp-$04]
00408C0D E856BCFFFF       call @Write0LString
00408C12 E80DA9FFFF       call @WriteLn
00408C17 E8C0A1FFFF       call @_IOTest
Project1.dpr.51: end;
00408C1C 33C0             xor eax,eax
00408C1E 5A               pop edx
00408C1F 59               pop ecx
00408C20 59               pop ecx
00408C21 648910           mov fs:[eax],edx
00408C24 68398C4000       push $00408c39
00408C29 8D45FC           lea eax,[ebp-$04]
00408C2C E80FB8FFFF       call @LStrClr
00408C31 C3               ret 

73 bajtů + 8 bajtů volání. Na začátku se zvětší počet výskytu předávaného řetězce (System._LStrAddRef), pak se vypíše text a nakonec se sníží počet výskytů řetězce (System._LStrClr). Pokud by byl počet výskytu nulový, řetězec by byl uvolněn. Delphi provádí na pozadí celkem magii pro pohodlnou práci s řetězci (např. uvedené počítání referencí místo prosté kopie) a někdy je mu vhodné naznačit pro dosažení lepšího výsledku.

No a jen pro ukázku varianta s inline, tj. TestCD. Není volání, kód je v rámci hlavního kódu.

Project1.dpr.61: TestCD(s);
00408C74 A1F4A94000       mov eax,[$0040a9f4]
00408C79 8B55FC           mov edx,[ebp-$04]
00408C7C E8E7BBFFFF       call @Write0LString
00408C81 E89EA8FFFF       call @WriteLn
00408C86 E851A1FFFF       call @_IOTest

Celkem 18 bajtů.

Možná se Vám zdá divné, že tu počítám bajty v dnešní době, ale chtěl jsem ukázat, že stačí málo a Váš program bude přehlednější (rozdělení delší procedury na menší bloky), ale zároveň stejně efektivní jako kdyby byl psán nudloidně.

Takže se nebojte napsat vloženou proceduru (popř. anonymní metodu) a nebojte se, ony se ty bajty a cykly procesoru nastřádají :-).

Tagy: , , ,

Optimalizace | Začátečníci

Komentování ukončeno

Naše nabídka

MVP
Ing. Radek Červinka - Embarcadero MVP
profil na linkedin, Twitter:@delphicz

Nabízím placené poradenství a konzultace v oblasti programování a vývoje SW.
Dále nabízíme i vývoj speciálního software na zakázku.

Neváhejte nás kontaktovat (i ohledně reklamy).

love Delphi

O Delphi.cz

Delphi je moderní RAD nástroj podporující tvorbu nativních aplikací pro platformu Win32, Win64, Mac OSX, Linux a na iPhone a Android.

Delphi.cz je nezávislý portál pro uživatele Delphi. Portál není koncipován pro úplné začátečníky, i když i ti se zde nebudou nudit, ale spíše na programátory, kteří již něco znají a chtějí své znalosti dále rozvíjet a sledovat novinky.

Poslední komentáře

Comment RSS

Dle měsíců