Teknisk artikel

Portering af _ftol inline-assembly fra 32-bit Delphi til DCC64

Det originale _ftol-idiom i 32-bit Delphi ligner et smart one-liner: en Pascal-funktionswrapper, der dykker ned i inline-assembly for at manipulere x87 FPU-kontrolordet, trunkere værdien på FPU-stakken og poppe resultatet. Det kompilerede fint under DCC32 i lang tid, hvilket præcis er grunden til, at det endte i så mange ældre grafik- og PDF-enheder uden at nogen satte spørgsmålstegn ved det

Skift bygningsmålet til 64-bit, og compileren afsluttes med E1025 Unsupported language feature: 'ASM'. Den fejl er ikke en kompatibilitetsadvarsel. Det betyder, at DCC64 slet ikke vil kompilere rutinen, uanset hvor godt assembly'en fungerede før

Det 32-bit originale så typisk sådan ud:

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

Den asm-blok inde i et Pascal begin...end-legeme er præcis, hvad DCC64 nægter. De to compilere har forskellige regler for, hvor assembly er tilladt, og grænsen har betydning

Hvorfor DCC64 trækker grænsen anderledes

DCC32 tillader inline-assembly inde i almindelige Pascal-rutiner. Compileren kender 32-bit-kaldkonventionen og kan ræsonnere om, hvor lokale variabler og parametre befinder sig, så den tolererer assembly-fragmenter, der refererer til stakrammen ved navn. DCC64 tager en strengere holdning: assembly skal placeres i en dedikeret assembler-funktion, en hvor hele kroppen er assembly, og kaldkonventionen håndteres eksplicit. Blandet Pascal-plus-asm understøttes slet ikke

Den underliggende årsag er arkitektonisk. I 64-bit Windows-kaldkonventionen (Microsoft ABI) ankommer de første fire parametre i RCX, RDX, R8 og R9 for heltalstyper, eller i XMM0 til XMM3 for flydende komma. Der er ingen x87 FPU-involvering i normal parameteroverførsel; x87 er teknisk tilgængeligt, men ABI'en bruger det ikke til argumenttransport. Assembly, der antager, at en værdi er "på FPU-stakken", ræsonnerer om en tilstand, som 64-bit ABI'en aldrig skaber

Så det gamle fragment har ikke bare et syntaksproblem. Selv hvis DCC64 accepterede det, ville registerantagelserne være forkerte

Skrivning af en korrekt 64-bit assembler-version

Når du reelt har brug for at eksportere et _ftol-symbol med cdecl-konvention for binær kompatibilitet, skal funktionen skrives som en ren assembler-rutine. Under 64-bit ABI ankommer en Double-parameter i XMM0, og heltalsresultatet skal returneres i RAX. Direktivet .NOFRAME fortæller DCC64, at rutinen selv håndterer sin stak, hvilket er passende for en bladfunktion af denne korte størrelse:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

CVTTSD2SI er SSE2-instruktionen til at konvertere et dobbelt-præcisions float til et fortegnet heltal med trunkering mod nul, hvilket præcis er, hvad _ftol skal gøre. Det er én instruktion, tager parameteren direkte fra, hvor ABI'en efterlod den, og placerer resultatet, hvor ABI'en forventer det. Ingen FPU-kontrolords-jonglering nødvendig

Bemærk, at hvis input overskrider rækkevidden af et 32-bit fortegnet heltal, returnerer CVTTSD2SI heltals-ubestemthedværdien ($80000000). Det er samme adfærd som x87 fistp ved input uden for rækkevidden. Det er værd at bekræfte, om dine kaldere kan producere sådanne værdier, inden du erklærer migreringen for færdig

Hvornår Trunc er det bedre svar

Assembler-versionen ovenfor er kun værd at skrive, når du har et reelt binært kompatibilitetskrav: en ekstern kalder forventer symbolet _ftol med en bestemt kaldkonvention, og du kan ikke ændre disse kaldere. Den situation er usædvanlig. Det meste af tiden var _ftol en privat hjælper, der kun bruges inden for samme enhed, og der er slet ingen ekstern afhængighed af dens navn eller konvention

I det tilfælde: erstat den med ren Pascal:

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

Trunc trunkerer mod nul, hvilket matcher, hvad _ftol gjorde med x87-kontrolordet sat til trunkeringstilstand. Det kompilerer på DCC32 og DCC64 uden ændringer. Compileren genererer den passende instruktion for hvert mål: på x64 vil den typisk udsende CVTTSD2SI alligevel, den samme instruktion som den håndskrevne version. Du får identisk adfærd, ingen platformsbetingede kompileringer og ingen assembly at vedligeholde

Den ene semantiske forskel, der er værd at kontrollere: Trunc rejser en EInvalidOp-undtagelse i Delphis standardkonfiguration, når input er en NaN eller uendelighed. X87 fistp i den originale kode skrev blot et bitmønster uden at rejse noget. Hvis din kode sender usædvanlige flydende komma-værdier ind i denne funktion, og den gamle adfærd var stille, brug IsNaN og IsInfinite fra Math som guard inden kald til Trunc

Betinget kompilering når begge mål forbliver aktive

Nogle projekter skal fortsætte med at levere både 32-bit og 64-bit binære filer. Hvis den originale assembler-version skal bevares til 32-bit og en ny implementering leveres til 64-bit, brug den betingede CPUX64:

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

Det er den minimale mekaniske rettelse, og det er værd at betragte den som midlertidig. En kodebase, der bærer arkitekturspecifik assembly i en hjælper, hvis eneste formål er float-til-heltal-trunkering, bærer unødvendig gæld. Den 32-bit-gren kan forsvinde helt, når du har bekræftet, at intet afhænger af FPU-sideeffekterne i den gamle implementering

Hvis funktionen forekommer i en komponent, der bruges på tværs af flere enheder, søg hele kodebasen for _ftol, inden du beslutter, hvordan migreringen skal foregå. Et symbol med det navn kan være erklæret mere end ét sted; linkeren vælger én og ignorerer stiltiende de andre, hvilket betyder, at du måske retter én kopi og stadig linker mod en anden, der ikke er blevet rørt