Technical Article

Prenos _ftol Inline Assembly z 32-bitového Delphi do DCC64

Pôvodný idiom _ftol v 32-bitovom Delphi vyzerá ako šikovná jednovrstevnica: pascalský wrapper funkcie, ktorý sa prepadá do inline assembly, aby manipuloval riadiace slovo x87 FPU, orezal hodnotu na zásobníku FPU a zobral výsledok. Pod DCC32 sa dlho kompiloval bez problémov, čo je presne dôvod, prečo skončil v mnohých starších grafických a PDF jednotkách bez toho, aby si to ktokoľvek problematizoval.

Prepnite cieľ zostavenia na 64-bit a kompilátor sa ukončí s chybou E1025 Unsupported language feature: 'ASM'. Tá chyba nie je varovanie o kompatibilite. Znamená, že DCC64 rutinu vôbec nezskompiluje, bez ohľadu na to, ako dobre assembly fungovala predtým.

32-bitový originál zvyčajne vyzeral nejako takto:

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

Blok asm vnútri tela begin...end v Pascale je presne to, čo DCC64 odmieta. Oba kompilátory majú odlišné pravidlá o tom, kde je assembly povolená, a táto hranica je dôležitá.

Prečo DCC64 určuje hranicu inak

DCC32 umožňuje inline assembly vo vnútri bežných pascalských rutín. Kompilátor pozná 32-bitovú volajúcu konvenciu a dokáže určiť, kde sa nachádzajú lokálne premenné a parametre, takže toleruje fragmenty assembly, ktoré sa odvolávajú na zásobník snímky podľa mena. DCC64 zastáva prísnejšiu pozíciu: assembly musí byť vo vyhradzenej funkcia assemblera, kde je celé telo assembly a volajúca konvencia sa spracováva explicitne. Zmiešaný Pascal-plus-asm nie je podporovaný vôbec.

Základný dôvod je architektonický. V 64-bitovej volajúcej konvencii Windows (Microsoft ABI) prichádzajú prvé štyri parametre cez RCX, RDX, R8 a R9 pre celočíselné typy, alebo cez XMM0XMM3 pre typy s pohyblivou desatinnou čiarkou. V normálnom odovzdávaní parametrov nie je zapojené x87 FPU; x87 je technicky dostupné, ale ABI ho nepoužíva na prenos argumentov. Assembly, ktorá predpokladá, že hodnota je "na zásobníku FPU", uvažuje o stave, ktorý 64-bitové ABI nikdy nevytvára.

Starý fragment teda nemá len syntaktický problém. Aj keby ho DCC64 akceptovalo, predpoklady o registroch by boli nesprávne.

Písanie správnej 64-bitovej verzie assemblera

Ak skutočne potrebujete exportovať symbol _ftol s konvenciou cdecl kvôli binárnej kompatibilite, funkcia musí byť napísaná ako čistá rutina assemblera. V 64-bitovom ABI prichádza parameter Double v XMM0 a celočíselný výsledok musí byť po návrate v RAX. Direktíva .NOFRAME hovorí DCC64, že rutina spravuje vlastný zásobník, čo je vhodné pre takto krátku listovú funkciu:

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 je inštrukcia SSE2 na prevod čísla s pohyblivou desatinnou čiarkou dvojitej presnosti na číslo so znakom s orezaním smerom k nule, čo je presne to, čo má _ftol robiť. Je to jedna inštrukcia, berie parameter priamo odtiaľ, kde ho ABI zanechalo, a umiestni výsledok tam, kde ho ABI očakáva. Žiadne žonglovanie s riadiacim slovom FPU.

Upozorňujeme, že ak vstup prekročí rozsah 32-bitového čísla so znakom, CVTTSD2SI vráti neurčitú celočíselnú hodnotu ($80000000). To je rovnaké správanie ako x87 fistp pri vstupe mimo rozsah. Či vaši volajúci môžu takéto hodnoty produkovať, stojí za overenie pred vyhlásením migrácie za dokončenú.

Keď je Trunc lepšou odpoveďou

Verzia assemblera vyššie sa oplatí písať iba vtedy, keď máte skutočnú požiadavku na binárnu kompatibilitu: nejaký externý volajúci očakáva symbol _ftol s konkrétnou volajúcou konvenciou a tieto volajúcich nemôžete zmeniť. Takáto situácia je nezvyčajná. Väčšinu času bol _ftol súkromný pomocník používaný len v rámci tej istej jednotky a vôbec neexistuje externá závislosť na jeho názve ani konvencii.

Pre tento prípad ho nahraďte čistým Pascalom:

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

Trunc orezáva smerom k nule, čo zodpovedá tomu, čo robila _ftol s riadiacim slovom x87 nastaveným na režim orezávania. Kompiluje sa na DCC32 aj DCC64 bez modifikácie. Kompilátor generuje príslušnú inštrukciu pre každý cieľ: na x64 zvyčajne emituje CVTTSD2SI aj tak, tú istú inštrukciu ako ručne napísaná verzia. Dostanete identické správanie, žiadne podmienky platformy a žiadnu assembly na udržiavanie.

Jeden sémantický rozdiel, ktorý stojí za overenie: Trunc vyvolá výnimku EInvalidOp v predvolenej konfigurácii Delphi, keď je vstupom NaN alebo nekonečno. fistp x87 v pôvodnom kóde len zapísalo bitový vzor bez vyvolania čohokoľvek. Ak váš kód do tejto funkcie odovzdáva neobvyklé hodnoty s pohyblivou desatinnou čiarkou a staré správanie bolo tiché, ošetrujte to pomocou IsNaN a IsInfinite z jednotky Math pred volaním Trunc.

Podmienená kompilácia, keď sú oba ciele stále aktívne

Niektoré projekty musia naďalej dodávať binárne súbory pre 32-bit aj 64-bit. Ak pôvodná verzia assemblera musí zostať pre 32-bit a pre 64-bit sa má poskytnúť nová implementácia, použite podmienku 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;

To je minimálna mechanická oprava a oplatí sa ju považovať za dočasnú. Kódová základňa, ktorá nesie assembly špecifickú pre architektúru v pomocníkovi, ktorého jediným účelom je orezanie hodnoty s pohyblivou desatinnou čiarkou na celé číslo, nesie zbytočný dlh. 32-bitová vetva môže úplne odísť, keď potvrdíte, že nič nezávisí od vedľajších účinkov FPU starej implementácie.

Ak sa funkcia vyskytuje v komponente používanom vo viacerých jednotkách, pred rozhodnutím o migrácii prehľadajte celú kódovú základňu na výskyt _ftol. Symbol tohto názvu môže byť deklarovaný na viac ako jednom mieste; linker vyberie jeden a ticho ignoruje ostatné, čo znamená, že môžete opraviť jednu kópiu a stále linkovať voči inej, ktorej sa nedotkli.