Tehnički članak

Portovanje _ftol ugradenog asemblera sa 32-bitnog Delphi-ja na DCC64

Originalni _ftol idiom u 32-bitnom Delphi-ju izgleda kao pametno resenje u jednom redu: Pascal omotac funkcije koji koristi ugradeni asembler za manipulaciju kontrolnom recju x87 FPU-a, iseca vrednost na FPU steku i preuzima rezultat. Ovo se dugo uspesno kompajliralo pod DCC32, sto je upravo razlog zasto je zavrsilo u toliko starih grafickih i PDF unita, a da to niko nije dovodio u pitanje

Promenite ciljnu platformu na 64-bitnu i kompajler ce se zaustaviti sa greskom E1025 Unsupported language feature: 'ASM'. Ta greska nije upozorenje o kompatibilnosti. To znaci da DCC64 uopste nece kompajlirati ovu rutinu, bez obzira na to koliko je asemblerski kod ranije dobro radio

Originalna 32-bitna verzija je obicno izgledala otprilike ovako:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Taj blok asm unutar Pascal tela begin...end je upravo ono sto DCC64 odbija. Ova dva kompajlera imaju razlicita pravila o tome gde je asemblerski kod dozvoljen, a ta granica je vazna

Zasto DCC64 povlaci granicu na drugaciji nacin

DCC32 dozvoljava ugradeni asembler unutar obicnih Pascal rutina. Kompajler poznaje 32-bitnu konvenciju pozivanja i moze da zakljuci gde se nalaze lokalne promenljive i parametri, pa toleriSe asemblerske fragmente koji pristupaju stek okviru preko naziva promenljivih. DCC64 zauzima strozi stav: asemblerski kod mora biti u namenskoj asemblerskoj funkciji, gde je celo telo funkcije asemblerski kod, a konvencija pozivanja se resava eksplicitno. Kombinovanje Pascal koda i asemblera uopste nije podrzano

Osnovni razlog je arhitektonske prirode. U 64-bitnoj Windows konvenciji pozivanja (Microsoft ABI), prva cetiri parametra stizu u registrima RCX, RDX, R8 i R9 za celobrojne tipove, ili u XMM0 do XMM3 za brojeve sa pokretnim zarezom. Uobicajeno prenosenje parametara uopste ne ukljucuje x87 FPU; x87 je tehnicki dostupan, ali ga ABI ne koristi za prenos argumenata. Asemblerski kod koji pretpostavlja da je vrednost "na FPU steku" oslanja se na stanje koje 64-bitni ABI nikada ne stvara

Dakle, stari fragment nema samo sintaksni problem. Cak i da ga DCC64 prihvati, pretpostavke o registrima bi bile pogresne

Delphi dijagram koji poredi DCC32, koji kompajlira inline asm unutar Pascal rutina, sa DCC64, koji odustaje sa E1025 i dozvoljava samo čiste assembler funkcije pod Win64 ABI
DCC32 toleriše asm blok unutar Pascal rutine dok DCC64 odgovara s E1025 jer Win64 ABI premešta argumente u registrima umesto na x87 steku

Pisanje ispravne 64-bitne asemblerske verzije

Kada zaista morate da eksportujete simbol _ftol sa konvencijom cdecl radi binarne kompatibilnosti, funkcija mora biti napisana kao cista asemblerska rutina. Pod 64-bitnim ABI-jem, parametar Double stize u XMM0, a celobrojni rezultat na povratku mora biti u RAX. Direktiva .NOFRAME govori kompajleru DCC64 da rutina sama upravlja svojim stekom, sto je prikladno za ovako kratku funkciju lisnu cvora:

function _ftol: Integer; cdecl;
// Vrednost tipa Double očekuje se u XMM0 prema 64-bitnom ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // skraćivanje na ceo broj, rezultat u rax
end;

CVTTSD2SI je SSE2 instrukcija za konverziju broja sa pokretnim zarezom dvostruke tacnosti u oznaceni ceo broj sa skracivanjem prema nuli, sto je upravo ono sto bi _ftol trebalo da radi. To je jedna instrukcija koja uzima parametar direktno odande gde ga je ABI ostavio i smesta rezultat tamo gde ga ABI ocekuje. Nema potrebe za zokiranjem kontrolnom recju FPU-a

Imajte na umu da ako ulazna vrednost prekoraci opseg 32-bitnog oznacenog celog broja, CVTTSD2SI vraca nedefinisanu celobrojnu vrednost ($80000000). To je isto ponasanje kao i kod x87 instrukcije fistp za ulaz van opsega. Pre nego sto migraciju proglasite zavrsenom, vredi proveriti da li kod koji poziva ovu funkciju moze da generiSe takve vrednosti

Delphi: Dijagram podatkovne putanje koji pokazuje Double parametar koji stiže u XMM0 i konvertuje ga jedna CVTTSD2SI instrukcija u skraćeni ceo broj vraćen u RAX, pri čemu vrednosti van opsega daju integer indefinite obrazac
Pod DCC64 double stiže u XMM0 i CVTTSD2SI smešta skraćeni 32-bitni rezultat u RAX kao jednu SSE2 instrukciju

Kada je Trunc bolje resenje

Gorenavedenu asemblersku verziju vredi pisati samo kada imate stvarni zahtev za binarnom kompatibilnoscu: neki eksterni pozivalac ocekuje simbol _ftol sa specificnom konvencijom pozivanja, a vi te pozivaoce ne mozete da promenite. Ta situacija je retka. U vecini slucajeva, _ftol je bio privatna pomocna funkcija koja se koristila samo unutar istog unita i uopste ne postoji spoljna zavisnost od njenog naziva ili konvencije

U tom slucaju, zamenite ga cistim Pascal kodom:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc skracuje vrednost prema nuli, sto odgovara onome sto je _ftol radio sa kontrolnom recju x87 postavljenom na rezim skracivanja. Kompajlira se na DCC32 i DCC64 bez ikakvih izmena. Kompajler generise odgovarajucu instrukciju za svaku platformu: na x64 ce obicno ionako emitovati CVTTSD2SI, istu instrukciju kao i u rucno pisanoj verziji. Dobijate identicno ponasanje, bez uslovnog kompajliranja za platforme i bez asemblerskog koda za odrzavanje

Jedna semanticka razlika koju vredi proveriti: Trunc izaziva izuzetak EInvalidOp u podrazumevanoj konfiguraciji Delphi-ja kada je ulaz NaN ili beskonacnost. Instrukcija fistp za x87 u originalnom kodu je samo upisivala bit-obrazac bez generisanja izuzetaka. Ako vas kod prosleduje neobicne vrednosti sa pokretnim zarezom ovoj funkciji, a staro ponasanje je bilo tiho, zastiTite poziv proverama IsNaN i IsInfinite iz unita Math pre nego sto pozovete Trunc

Delphi dijagram odlučivanja za portiranje _ftol: zameni ga običnim Trunc kad nijedan spoljašnji pozivalac ne treba cdecl simbol, inače zadrži čist assembler export, i čuvaj NaN ili beskonačne unose od EInvalidOp
Preselite se na običan Trunc osim ako spoljni pozivači zavise od izvezenog simbola, i štitite NaN ili beskonačne ulaze od EInvalidOp na oba načina

Uslovno kompajliranje kada su obe ciljne platforme aktivne

Neki projekti moraju da nastave sa isporukom i 32-bitnih i 64-bitnih binarnih fajlova. Ako se originalna asemblerska verzija mora zadrzati za 32-bitnu platformu, a nova implementacija obezbediti za 64-bitnu, koristite uslov CPUX64:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bitna putanja: DCC32 prihvata inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

To je minimalna mehanicka ispravka i treba je smatrati privremenom. Baza koda koja nosi asemblerski kod specifican za arhitekturu u pomocnoj funkciji cija je jedina svrha skracivanje broja sa pokretnim zarezom u ceo broj nosi nepotreban tehnicki dug. 32-bitna grana moze se potpuno ukloniti cim potvrdite da nista ne zavisi od sporednih efekata FPU-a iz stare implementacije

Ako se funkcija pojavljuje u komponenti koja se koristi u vise unita, pretrazite celu bazu koda za _ftol pre nego sto odlucite kako da je migrirate. Simbol pod tim imenom moze biti deklarisan na vise mesta; linker bira jedan i tiho ignorise ostale, sto znaci da biste mogli da ispravite jednu kopiju, a da se i dalje povezujete sa drugom verzijom koja nije izmenjena