Idiomul original _ftol din Delphi pe 32 de biți arată ca o linie de cod ingenioasă: o funcție Pascal învelitoare care coboară în asamblare inline pentru a manipula cuvântul de control al FPU-ului x87, a trunchia valoarea de pe stiva FPU și a extrage rezultatul. S-a compilat fără probleme sub DCC32 multă vreme, ceea ce explică exact de ce a ajuns în atâtea unități mai vechi de grafică și de PDF fără ca nimeni să îl pună la îndoială
Comutați ținta de compilare pe 64 de biți și compilatorul se oprește cu E1025 Unsupported language feature: 'ASM'. Acea eroare nu este un avertisment de compatibilitate. Înseamnă că DCC64 nu va compila deloc rutina, oricât de bine ar fi funcționat asamblarea înainte
Originalul pe 32 de biți arăta de obicei cam așa:
function _ftol(f: Double): Integer; cdecl;
begin
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
end;
Acel bloc asm din interiorul unui corp Pascal begin...end este exact ceea ce refuză DCC64. Cele două compilatoare au reguli diferite despre unde este permisă asamblarea, iar granița contează
De ce trasează DCC64 linia altfel
DCC32 permite asamblare inline în interiorul rutinelor Pascal obișnuite. Compilatorul cunoaște convenția de apel pe 32 de biți și poate raționa despre locul în care stau variabilele locale și parametrii, așa că tolerează fragmente de asamblare care ajung la cadrul de stivă folosind numele. DCC64 are o poziție mai strictă: asamblarea trebuie să stea într-o funcție dedicată de tip assembler, una în care întregul corp este asamblare, iar convenția de apel este tratată explicit. Amestecul de Pascal plus asm nu este deloc acceptat
Motivul de fond este arhitectural. În convenția de apel Windows pe 64 de biți (ABI-ul Microsoft), primii patru parametri sosesc în RCX, RDX, R8 și R9 pentru tipurile întregi, sau în XMM0 până la XMM3 pentru virgulă mobilă. FPU-ul x87 nu este implicat deloc în transmiterea normală a parametrilor; x87 este disponibil din punct de vedere tehnic, dar ABI-ul nu îl folosește pentru transportul argumentelor. Asamblarea care presupune că o valoare este „pe stiva FPU” raționează despre o stare pe care ABI-ul pe 64 de biți nu o creează niciodată
Așadar, vechiul fragment nu are doar o problemă de sintaxă. Chiar dacă DCC64 l-ar accepta, presupunerile despre registre ar fi greșite
Scrierea unei versiuni assembler potrivite pentru 64 de biți
Când chiar aveți nevoie să exportați un simbol _ftol cu convenția cdecl pentru compatibilitate binară, funcția trebuie scrisă ca rutină pur assembler. Sub ABI-ul pe 64 de biți, un parametru Double sosește în XMM0, iar rezultatul întreg trebuie să fie în RAX la revenire. Directiva .NOFRAME îi spune lui DCC64 că rutina își administrează singură stiva, ceea ce este potrivit pentru o funcție frunză atât de scurtă:
function _ftol: Integer; cdecl;
// valoare Double așteptată în XMM0, conform ABI-ului pe 64 de biți
asm
.NOFRAME
cvttsd2si rax, xmm0 // trunchiere la întreg, rezultatul în rax
end;
CVTTSD2SI este instrucțiunea SSE2 pentru conversia unui număr în virgulă mobilă cu precizie dublă într-un întreg cu semn, cu trunchiere spre zero, adică exact ce ar trebui să facă _ftol. Este o singură instrucțiune, ia parametrul direct de unde l-a lăsat ABI-ul și pune rezultatul unde îl așteaptă ABI-ul. Nu este nevoie de nicio jonglerie cu cuvântul de control al FPU-ului
Rețineți că, dacă intrarea depășește intervalul unui întreg cu semn pe 32 de biți, CVTTSD2SI returnează valoarea întreagă nedefinită ($80000000). Este același comportament ca al instrucțiunii x87 fistp pentru intrări în afara intervalului. Dacă apelanții dumneavoastră pot produce astfel de valori merită confirmat înainte de a declara migrarea încheiată
Când Trunc este răspunsul mai bun
Versiunea assembler de mai sus merită scrisă doar atunci când aveți o cerință reală de compatibilitate binară: un apelant extern așteaptă simbolul _ftol cu o anumită convenție de apel, iar acei apelanți nu pot fi schimbați. Situația este rară. De cele mai multe ori, _ftol era o funcție ajutătoare privată, folosită doar în interiorul aceleiași unități, și nu există nicio dependență externă de numele sau de convenția ei
Pentru acel caz, înlocuiți-o cu Pascal simplu:
function _ftol(f: Double): Integer; cdecl;
begin
Result := Trunc(f);
end;
Trunc trunchiază spre zero, ceea ce corespunde cu ce făcea _ftol având cuvântul de control x87 setat pe modul de trunchiere. Se compilează pe DCC32 și pe DCC64 fără modificări. Compilatorul generează instrucțiunea potrivită pentru fiecare țintă: pe x64 va emite oricum, de regulă, CVTTSD2SI, aceeași instrucțiune ca în versiunea scrisă de mână. Obțineți comportament identic, fără condiționale de platformă și fără asamblare de întreținut
Singura diferență semantică pe care merită să o verificați: Trunc ridică o excepție EInvalidOp în configurația implicită din Delphi atunci când intrarea este NaN sau infinit. Instrucțiunea x87 fistp din codul original scria pur și simplu un tipar de biți, fără să ridice nimic. Dacă în codul dumneavoastră ajung valori neobișnuite în virgulă mobilă în această funcție, iar vechiul comportament era tăcut, puneți o gardă cu IsNaN și IsInfinite din Math înainte de a apela Trunc
Compilare condiționată atunci când ambele ținte rămân active
Unele proiecte trebuie să livreze în continuare atât binare pe 32 de biți, cât și pe 64. Dacă versiunea assembler originală trebuie păstrată pentru 32 de biți și trebuie oferită o implementare nouă pentru 64, folosiți condiționalul CPUX64:
function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
Result := Trunc(f);
{$ELSE}
// cale pe 32 de biți: DCC32 acceptă asm inline
asm
lea eax, f
fstp qword ptr [eax]
end;
Result := Trunc(f);
{$ENDIF}
end;
Aceasta este corectura mecanică minimă și merită tratată ca temporară. O bază de cod care poartă asamblare specifică unei arhitecturi într-o funcție ajutătoare al cărei unic scop este trunchierea din virgulă mobilă în întreg poartă o datorie inutilă. Ramura pe 32 de biți poate dispărea complet imediat ce confirmați că nimic nu depinde de efectele secundare asupra FPU-ului din vechea implementare
Dacă funcția apare într-o componentă folosită în mai multe unități, căutați _ftol în toată baza de cod înainte de a decide cum migrați. Un simbol cu acest nume poate fi declarat în mai multe locuri; linkerul alege unul și le ignoră în tăcere pe celelalte, ceea ce înseamnă că ați putea repara o copie și tot să faceți legarea cu alta, neatinsă