Technický článek

Převod inline assembleru _ftol z 32bitového Delphi do DCC64

Původní idiom _ftol ve 32bitovém Delphi vypadá jako chytrý jednořádkový kód: wrapper funkce v Pascalu, který se ponoří do inline assembleru, aby zmanipuloval řídicí slovo x87 FPU, zkrátil (truncate) hodnotu na zásobníku FPU a vyjmul výsledek. Dlouhou dobu se to pod DCC32 bez problémů kompilovalo, což je přesně ten důvod, proč to skončilo v tolika starších grafických a PDF jednotkách, aniž by o tom někdo pochyboval

Přepněte cíl sestavení na 64 bitů a kompilátor se ukončí s chybou E1025 Unsupported language feature: 'ASM'. Tato chyba není varováním o kompatibilitě. Znamená to, že DCC64 rutinu vůbec nezkompiluje, bez ohledu na to, jak dobře tento assembler fungoval dříve

Původní 32bitová verze typicky vypadala nějak takto:

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

Tento blok asm uvnitř těla begin...end v Pascalu je přesně to, co DCC64 odmítá. Tyto dva kompilátory mají odlišná pravidla ohledně toho, kde je povolen assembler, a na této hranici záleží

Proč DCC64 rýsuje čáru jinde

DCC32 povoluje inline assembler uvnitř běžných rutin Pascalu. Kompilátor zná 32bitovou konvenci volání a dokáže odvodit, kde žijí lokální proměnné a parametry, takže toleruje fragmenty assembleru, které sahají do rámce zásobníku jménem. DCC64 zaujímá přísnější postoj: assembler musí být ve vyhrazené funkci assembleru, kde celé tělo tvoří assembler a konvence volání se zpracovává explicitně. Smíšený kód Pascal-plus-asm není vůbec podporován

Základní důvod je architektonický. V konvenci volání pro 64bitové Windows (Microsoft ABI) dorazí první čtyři parametry v registrech RCX, RDX, R8 a R9 pro celočíselné typy, nebo v XMM0XMM3 pro reálná čísla (floating-point). Běžné předávání parametrů vůbec nezahrnuje x87 FPU; koprocesor x87 je technicky k dispozici, ale ABI jej pro přenos argumentů nepoužívá. Assembler, který předpokládá, že hodnota je „na zásobníku FPU“, uvažuje o stavu, který 64bitové ABI nikdy nevytváří

Takže onen starý fragment nemá jen problém se syntaxí. I kdyby jej DCC64 přijal, předpoklady o registrech by byly chybné

Diagram Delphi srovnávající DCC32 kompilující inline asm uvnitř rutin Pascalu s DCC64 končícím s E1025 a dovolujícím pod ABI Win64 jen čisté assembler funkce
DCC32 snese blok asm uvnitř Pascal rutiny, zatímco DCC64 odpoví E1025, protože Win64 ABI přenáší argumenty v registrech místo zásobníku x87

Zápis správné 64bitové verze v assembleru

Když skutečně potřebujete exportovat symbol _ftol s konvencí cdecl pro binární kompatibilitu, funkce musí být zapsána jako čistá rutina v assembleru. Podle 64bitového ABI parametr typu Double dorazí v XMM0 a výsledek (celé číslo) musí být při návratu v RAX. Direktiva .NOFRAME říká DCC64, že si rutina spravuje svůj vlastní zásobník, což je vhodné pro takto krátkou listovou (leaf) funkci:

function _ftol: Integer; cdecl;
// Podle 64bitového ABI se v XMM0 očekává hodnota typu Double
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // zkrácení na celé číslo, výsledek v rax
end;

CVTTSD2SI je instrukce SSE2 pro převod reálného čísla s dvojitou přesností (double) na znaménkové celé číslo s oříznutím k nule, což je přesně to, co má _ftol dělat. Je to jediná instrukce, bere parametr přímo odtud, kde ho ABI zanechalo, a umístí výsledek tam, kde ho ABI očekává. Není potřeba žádné žonglování s řídicím slovem FPU

Všimněte si, že pokud vstup překročí rozsah 32bitového celého čísla se znaménkem, CVTTSD2SI vrátí neurčitou celočíselnou hodnotu ($80000000). To je stejné chování jako u instrukce x87 fistp při vstupu mimo rozsah. Než prohlásíte migraci za hotovou, stojí za to ověřit, zda vaši volající mohou takové hodnoty produkovat

Delphi: Diagram datové cesty ukazující parametr Double přijíždějící v XMM0 a převedený jedinou instrukcí CVTTSD2SI na oříznuté celé číslo vrácené v RAX, s hodnotami mimo rozsah dávajícími vzor integer indefinite
Pod DCC64 dorazí double v XMM0 a CVTTSD2SI umístí zkrácený 32bitový výsledek do RAX jako jediná instrukce SSE2

Kdy je lepším řešením Trunc

Výše uvedenou verzi v assembleru má smysl psát pouze tehdy, když máte skutečný požadavek na binární kompatibilitu: nějaký externí volající očekává symbol _ftol se specifickou konvencí volání a vy tyto volající nemůžete změnit. Taková situace je neobvyklá. Ve většině případů byl _ftol jen privátním pomocníkem používaným výhradně v rámci jedné jednotky a neexistuje vůbec žádná externí závislost na jeho názvu nebo konvenci

Pro takový případ jej nahraďte prostým kódem v Pascalu:

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

Trunc ořezává směrem k nule, což odpovídá tomu, co dělal _ftol s řídicím slovem x87 nastaveným na režim oříznutí. Zkompiluje se na DCC32 i DCC64 bez úprav. Kompilátor vygeneruje pro každý cíl vhodnou instrukci: na x64 obvykle stejně emituje CVTTSD2SI, tedy tutéž instrukci jako ručně psaná verze. Získáte totožné chování, žádné podmínky platforem a žádný assembler k udržování

Jediný sémantický rozdíl, který stojí za to zkontrolovat: ve výchozí konfiguraci Delphi vyvolá Trunc výjimku EInvalidOp, pokud je vstupem NaN (Not-a-Number) nebo nekonečno. Instrukce x87 fistp v původním kódu jen zapsala bitový vzor bez toho, aby něco vyvolala. Pokud váš kód do této funkce posílá neobvyklé reálné hodnoty a staré chování bylo tiché, pojistěte jej před zavoláním Trunc funkcemi IsNaN a IsInfinite z jednotky Math

Rozhodovací diagram Delphi pro port _ftol: nahraďte jej obyčejným Trunc, když žádný externí volající nepotřebuje symbol cdecl, jinak ponechte čistý assembler export a chraňte vstupy NaN a nekonečno proti EInvalidOp
Přejděte na obyčejný Trunc, pokud externí volající nezávisí na exportovaném symbolu, a v obou případech chraňte vstupy NaN či nekonečno proti EInvalidOp

Podmíněná kompilace, když oba cíle zůstávají aktivní

Některé projekty musí i nadále dodávat 32bitové i 64bitové binární soubory. Pokud musí být původní verze v assembleru zachována pro 32 bitů a poskytnuta nová implementace pro 64 bitů, použijte podmínku CPUX64:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32bitová větev: DCC32 přijímá inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

To je minimální mechanická oprava a je na ni třeba pohlížet jako na dočasnou. Kódová základna, která ve svém nitru nese na architekturu vázaný assembler v pomocné funkci, jejímž jediným účelem je oříznutí čísla s plovoucí řádovou čárkou na celé číslo, s sebou vleče zbytečný dluh. Jakmile potvrdíte, že nic nezávisí na vedlejších efektech FPU ve staré implementaci, může 32bitová větev zcela zmizet

Pokud se tato funkce objeví v komponentě používané ve více jednotkách, prohledejte celou kódovou základnu a najděte _ftol, než se rozhodnete, jak migrovat. Symbol s takovým názvem lze deklarovat na více než jednom místě; linker si jeden vybere a ty ostatní tiše ignoruje, což znamená, že můžete opravit jednu kopii, ale přesto linkovat (propojovat) jinou, které se nikdo nedotkl