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

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ú

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

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