Compago

...free knowledge

 
  • Increase font size
  • Default font size
  • Decrease font size
Home Manuali Programmazione Studio di funzioni e procedure in delphi e assembler - Come rendere più veloce una funzione

Studio di funzioni e procedure in delphi e assembler - Come rendere più veloce una funzione

E-mail Stampa PDF
Indice
Studio di funzioni e procedure in delphi e assembler
Come rendere più veloce una funzione
Tutte le pagine

Una cosa che mi aveva sempre incuriosito era l'inserimento di procedure in assembler all'interno di funzioni, in modo da renderle più "veloci". Devo dire che sebbene per calcoli più complicati l'uso di procedure o funzioni in assembler si riveli efficace, questo modo di procedere potrebbe rivelare delle sorprese.

Prima di procedere vorrei ricordare che usando la keyword asm è come se fi facesse riferimento ad una funzione o procedura esterna, inoltre devono essere preservati i valori nei registri EDI, ESI, ESP, EBP e  EBX mentre possono essere liberamente utilizzati i registri EAX, ECX e EDX. Se non ci si attiene a queste regole è possibile che il programma compilato dia dei risultati imprevisti.

Prendiamo ad esempio il sorgente in delphi di una applicazione dos e compiliamola.

program ProvaSomme;
{$APPTYPE CONSOLE}
uses
  SysUtils;

function Somma1(a,b:integer):integer;
begin
  Result:=a+b;
end;

function Somma2(a,b:integer):integer;
begin
  asm
    mov eax,a
    add eax,b
    mov result,eax
  end;
end;

function Somma3(a,b:integer):integer; assembler;
asm
    mov eax,a
    add eax,b
    mov result,eax
end;

function Somma4(a,b:integer):integer; assembler;
asm
    lea eax,a+b
end;

function Somma5(a,b:integer):integer;  inline;
begin
  Result:=a+b;
end;

var
  a,b,c:integer;
begin
  a:=2;
  b:=3;
  c:=a+b;
  Writeln(c);

  c:=Somma1(a,b);
  Writeln(c);

  c:=Somma2(a,b);
  Writeln(c);

  c:=Somma3(a,b);
  Writeln(c);

  c:=Somma4(a,b);
  Writeln(c);

  c:=Somma5(a,b);
  Writeln(c);

  Readln;
end.

In pratica calcola le somme in diversi modi e le stampa a video. Partendo dalla semplice operazione esplicita di somma, abbiamo inserito l'uso della funzione di somma, dato che l'obbiettivo è quello di creare una funzione da riutilizzare anche da altre parti del programma.

Ora vediamo un po come vengono implementate in codice macchina le funzioni:

Per prima cosa troviamo l'inizializzazione delle 2 variabili

// a:=2;
0040914F BE02000000       mov esi,$00000002
// b:=3;
00409154 BF03000000       mov edi,$00000003

Poi avremo la vera e propria operazione di somma, come si può vedere per fare ciò il compilatore usa l'istruzione LEA, che compatta e veloce porta a termine il suo compito.

// c:=a+b;
00409159 8D1C37           lea ebx,[edi+esi]

Poi avremo la solita stampa a video che si ripeterà allo stesso modo dopo ogni somma.

//Writeln(c);
0040915C A1F0A94000       mov eax,[$0040a9f0]
00409161 8BD3             mov edx,ebx
00409163 E85CA5FFFF       call @Write0Long
00409168 E883A5FFFF       call @WriteLn
0040916D E86A9CFFFF       call @_IOTest

Iniziamo con la Somma1, che non è altro che una semplicissima funzione. questa avendo solo 2 parametri implementa la classica fastcall di delphi, infatti memorizza in 2 registri i valori dei parametri e li passa alla funzione vera e propria, quindi nel migliore dei casi al posto della versione esplicita (1 clock) usando questa funzione abbiamo già aggiunto altre 4 istruzioni che corrispondono a 6 operazioni dal parte della CPU (6 clock).

// c:=Somma1(a,b);
00409172 8BD7             mov edx,edi
00409174 8BC6             mov eax,esi
00409176 E889FAFFFF       call Somma1
0040917B 8BD8             mov ebx,eax
// Writeln(c);
.
.
.

Vediamo come è implementata la funzione Somma1:

// Result:=a+b;
00408C04 03D0             add edx,eax
00408C06 8BC2             mov eax,edx
// end;
00408C08 C3               ret

Con queste istruzioni raggiungiamo quindi quota 9 clock.

Per la Somma2 abbiamo voluto inserire del codice assembler al suo interno, e vediamo che il concetto di chiamata della funzione rimane inalterato.

// c:=Somma2(a,b);
00409193 8BD7             mov edx,edi
00409195 8BC6             mov eax,esi
00409197 E870FAFFFF       call Somma2
0040919C 8BD8             mov ebx,eax
// Writeln(c);
.
.
.

Ma guardiamo l'implementazione della funzione somma2:

// begin
00408C0C 55               push ebp
00408C0D 8BEC             mov ebp,esp
00408C0F 83C4F4           add esp,-$0c
00408C12 8955F8           mov [ebp-$08],edx
00408C15 8945FC           mov [ebp-$04],eax
00408C18 8D55F4           lea edx,[ebp-$0c]
// mov eax,a
00408C1B 8B45FC           mov eax,[ebp-$04]
// add eax,b
00408C1E 0345F8           add eax,[ebp-$08]
// mov result,eax
00408C21 8945F4           mov [ebp-$0c],eax
// end;
00408C24 8B02             mov eax,[edx]
00408C26 8BE5             mov esp,ebp
00408C28 5D               pop ebp
00408C29 C3               ret

Se pensavamo di ottimizzare una semplice funzione questo è un vero e proprio disastro! Ben 13 operazioni in più raggiungendo quota 19 clock!

Proviamo allora a dichiarare la funzione Somma3 interamente assembler e vediamo che succede:

// c:=Somma3(a,b);
004091B4 8BD7             mov edx,edi
004091B6 8BC6             mov eax,esi
004091B8 E86FFAFFFF       call Somma3
004091BD 8BD8             mov ebx,eax
// Writeln(c);
.
.
.

Viene ripetuta la stessa inizializzazione delle variabili locali. Vediamo l'implementazione:

// asm
00408C2C 55               push ebp
00408C2D 8BEC             mov ebp,esp
00408C2F 51               push ecx
// mov eax,a
00408C30 89C0             mov eax,eax
// add eax,b
00408C32 01D0             add eax,edx
// mov result,eax
00408C34 8945FC           mov [ebp-$04],eax
// end;
00408C37 8B45FC           mov eax,[ebp-$04]
00408C3A 59               pop ecx
00408C3B 5D               pop ebp
00408C3C C3               ret

Và un po meglio 10 operazioni per un totale di 16 clock. La forzatura in assembler non aiuta il compilatore, anzi lo confonde, infatti il push e il pop del registro ecx è abbastanza inutile, tanto più l'uso dello stack per memorizzare il risultato. In pratica l'unica operazione utile è

add eax,edx

Potrei quindi creare una funzione assembler con questa unica istruzione e avrei risolto i miei problemi. Ma per fare ciò, in fase di programmazione, dovrei sapere per certo che i parametri saranno immagazinati in eax e in edx, ma questo non lo posso sapere. Quello che però posso intuire e che il risultato almeno deve finire in eax, quindi riprovo nella successiva versione: Somma4.

// c:=Somma4(a,b);
004091D5 8BD7             mov edx,edi
004091D7 8BC6             mov eax,esi
004091D9 E862FAFFFF       call Somma4
004091DE 8BD8             mov ebx,eax
// Writeln(c);
.
.
.

Fin qui tutto normale, vediamo l'implementazione:

// lea eax,a+b
00408C40 8D0402           lea eax,[edx+eax]
// end;
00408C43 C3               ret

questa già inizia a piacermi solo 2 operazioni che ci portano a quota 8 clock, siamo riusciti quindi ad ottimizzare la funzione... ma...ma...ma ci potrebbe essere un altro modo.

Proviamo ad usare l'opzione inline nella definizione della funzione Somma5. questa sarà in tutto e per tutto uguale alla funzione normale Somma1, ma con l'aggiunta della opzione inline, che obbliga il compilatore a non creare una chiamata alla funzione ma di inserirne il procedimento nei punti dove verrà utilizzato. Vediamo infatti:

.
.
// c:=Somma5(a,b);
004091F6 8D1C37           lea ebx,[edi+esi]
.
.

non vi è nessuna chiamata ad una funzione nel codice compilato, ritornando al caso iniziale di operazione di somma esplicita (1 clock), a questo vantaggio dobbiamo anche associare quello che infase di programmazione abbiamo utilizzato la funzione Somma5 come tutte le altre funzioni.

Come svantaggio avremo che il codice compilato avrà una dimensione superiore, dato che la stessa parte di codice verrà ripetuta per tutte le volte che noi abbiamo utilizzato la "funzione".

Concludendo se volete aumentare le prestazioni di una applicazione a scapito della dimensione del file, tenendo conto anche della memoria utilizzata dal processo, sarebbe preferibile usare funzioni inline, se invece la funzione dovesse essere utilizzata molte volte e contenesse dei calcoli un po più complessi, potrebbe essere una buona idea usare del codice assembler al suo interno, ma attenzione all'uso delle variabili, perché se si usa la logica di un normale codice delphi, si potrebbe peggiorare di molto la situazione.

 

 

Attachments:
Download this file (studio funzioni.zip)studio funzioni.zip[ ]159 Kb


Ultimo aggiornamento ( Sabato 28 Agosto 2010 19:47 )  
Italian English French German Portuguese Russian Spanish

Chiudi