Odborný článok

Portovanie inline assembleru _ftol z 32-bitového Delphi do DCC64

Pôvodný idióm _ftol v 32-bitovom Delphi vyzerá ako šikovný jednoriadkový príkaz: obal (wrapper) funkcie v Pascale, ktorý prejde do inline assembleru, aby manipuloval s riadiacim slovom FPU x87, skrátil hodnotu v zásobníku FPU a vybral výsledok. Pod DCC32 sa dlho skompilovával bez problémov, čo je presne ten dôvod, prečo skončil v toľkých starších grafických a PDF unitách bez toho, aby to niekto spochybňoval

Prepnite cieľ zostavenia na 64-bitový a prekladač sa ukončí s chybou E1025 Unsupported language feature: 'ASM'. Táto chyba nie je varovaním o kompatibilite. Znamená to, že DCC64 rutinu vôbec neskompiluje bez ohľadu na to, ako dobre ten assembler fungoval 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;

Tento blok asm vo vnútri tela begin...end v Pascale je presne to, čo DCC64 odmieta. Dva prekladače majú odlišné pravidlá o tom, kde je assembler povolený, a na hranici záleží

Prečo DCC64 kreslí čiaru inak

DCC32 povoľuje inline assembler v bežných rutinách Pascalu. Prekladač pozná 32-bitovú konvenciu volania a dokáže usúdiť, kde žijú lokálne premenné a parametre, takže toleruje fragmenty assembleru, ktoré siahajú do rámca zásobníka podľa mena. DCC64 zaujíma prísnejšiu pozíciu: assembler musí byť vo vyhradenej funkcii assembleru, v takej, kde celé telo je v assembleri a konvencia volania sa spravuje explicitne. Zmiešaný kód Pascal-plus-asm nie je podporovaný vôbec

Základný dôvod je architektonický. V 64-bitovej konvencii volania Windows (Microsoft ABI) prichádzajú prvé štyri parametre v RCX, RDX, R8 a R9 pre celočíselné typy alebo v XMM0XMM3 pre pohyblivú rádovú čiarku. Pri bežnom odovzdávaní parametrov nie je zapojený x87 FPU; x87 je technicky k dispozícii, ale ABI ho na prenos argumentov nepoužíva. Assembler, ktorý predpokladá, že hodnota je „na zásobníku FPU“, uvažuje o stave, ktorý 64-bitové ABI nikdy nevytvára

Takže starý fragment nemá len problém so syntaxou. Aj keby ho DCC64 prijal, predpoklady o registroch by boli nesprávne

Diagram Delphi porovnávajúci DCC32, ktorý kompiluje inline asm vnútri rutín Pascal, s DCC64, ktorý abortuje s E1025 a dovoľuje len čisté assembler funkcie pod ABI Win64
DCC32 znáša blok asm vo vnútri Pascal rutiny, zatiaľ čo DCC64 odpovedá E1025, pretože Win64 ABI presúva argumenty v registroch namiesto zásobníka x87

Písanie správnej verzie pre 64-bitový assembler

Keď skutočne potrebujete exportovať symbol _ftol s konvenciou cdecl pre binárnu kompatibilitu, funkcia musí byť napísaná ako čisto assemblerová rutina. V 64-bitovom ABI dorazí parameter Double v XMM0 a celočíselný výsledok musí byť pri návrate v RAX. Direktíva .NOFRAME hovorí DCC64, že rutina spravuje svoj vlastný zásobník, čo je vhodné pre koncovú (leaf) funkciu, ktorá je takáto krátka:

function _ftol: Integer; cdecl;
// Hodnota Double sa očakáva v XMM0 podľa 64-bitového ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // zrezať na celé číslo, výsledok v rax
end;

CVTTSD2SI je inštrukcia SSE2 pre konverziu čísla s pohyblivou rádovou čiarkou s dvojitou presnosťou na celé číslo so znamienkom so zrezaním k nule, čo je presne to, čo by malo _ftol urobiť. Je to jedna inštrukcia, preberá parameter priamo tam, kde ho ABI zanechalo, a umiestni výsledok tam, kde ho ABI očakáva. Nie je potrebné žiadne žonglovanie s riadiacim slovom FPU

Všimnite si, že ak vstup prekročí rozsah 32-bitového celého čísla so znamienkom, CVTTSD2SI vráti neurčitú hodnotu celého čísla ($80000000). Ide o rovnaké správanie ako v prípade x87 fistp pre vstup mimo rozsahu. To, či vaši volajúci môžu produkovať takéto hodnoty, stojí za potvrdenie predtým, ako vyhlásite migráciu za ukončenú

Delphi: Diagram dátovej cesty ukazujúci parameter Double prichádzajúci v XMM0 a konvertovaný jednou inštrukciou CVTTSD2SI na skrátené celé číslo vrátené v RAX, s hodnotami mimo rozsahu dávajúcimi vzor integer indefinite
Pod DCC64 prichádza double v XMM0 a CVTTSD2SI umiestni skrátený 32-bitový výsledok do RAX ako jediná SSE2 inštrukcia

Keď je Trunc lepšou odpoveďou

Vyššie uvedenú verziu v assembleri sa oplatí písať len vtedy, keď máte skutočnú požiadavku na binárnu kompatibilitu: nejaký externý volajúci očakáva symbol _ftol so špecifickou konvenciou volania a týchto volajúcich nemôžete zmeniť. Táto situácia je neobvyklá. Väčšinu času bol _ftol privátnym pomocníkom (private helper) používaným len v rámci tej istej unity a na jeho mene alebo konvencii vôbec nezáležalo

V takom prípade 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 _ftol robilo s riadiacim slovom x87 nastaveným na režim zrezania. Na DCC32 a DCC64 sa skompiluje bez úprav. Prekladač vygeneruje pre každý cieľ zodpovedajúcu inštrukciu: na platforme x64 zvyčajne v každom prípade emituje CVTTSD2SI, teda rovnakú inštrukciu ako ručne písaná verzia. Dostanete rovnaké správanie, žiadne podmienky pre platformu (platform conditionals) a žiadny assembler na údržbu

Jeden sémantický rozdiel, ktorý stojí za kontrolu: Trunc vyvolá v predvolenej konfigurácii Delphi výnimku EInvalidOp, keď je vstupná hodnota NaN alebo nekonečno. x87 fistp v pôvodnom kóde len zapísal bitový vzor bez toho, aby niečo vyvolal. Ak váš kód odovzdáva do tejto funkcie neobvyklé hodnoty s pohyblivou rádovou čiarkou a staré správanie bolo tiché, ošetrite to pomocou IsNaN a IsInfinite z jednotky (unitu) Math pred zavolaním Trunc

Rozhodovací diagram Delphi pre port _ftol: nahradiť ho obyčajným Trunc, keď žiadny externý volajúci nepotrebuje symbol cdecl, inak ponechať čistý assembler export a strážiť vstupy NaN alebo nekonečno pred EInvalidOp
Migrujte na obyčajný Trunc, pokiaľ externí volajúci nezávisia na exportovanom symbole, a v oboch prípadoch chráňte vstupy NaN alebo nekonečno pred EInvalidOp

Podmienený preklad (conditional compilation), keď oba ciele zostávajú aktívne

Niektoré projekty musia pokračovať v distribúcii oboch, 32-bitových a 64-bitových binárok. Ak sa pôvodná verzia v assembleri musí zachovať pre 32-bitovú platformu a pre 64-bitovú je k dispozícii nová implementácia, použite podmienku CPUX64:

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

Je to minimálna mechanická oprava a oplatí sa k nej pristupovať ako k dočasnej. Báza kódu, ktorá nesie assembler špecifický pre architektúru v pomocníkovi, ktorého jediným účelom je zrezanie čísla s pohyblivou rádovou čiarkou na celé číslo (float-to-integer), nesie zbytočný dlh. 32-bitová vetva môže úplne zmiznúť, akonáhle potvrdíte, že nič nezávisí od vedľajších účinkov FPU v starej implementácii

Ak sa funkcia objavuje v komponente, ktorý sa používa naprieč viacerými unitami, pred rozhodnutím o migrácii vyhľadajte v celej báze kódu _ftol. Symbol s takýmto názvom môže byť deklarovaný na viacerých miestach; linker vyberie jeden a potichu ignoruje ostatné, čo znamená, že môžete opraviť jednu kópiu a stále sa linkovať proti inej, ktorej sa nikto nedotkol