| 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.





