Műszaki cikk

Az _ftol beágyazott assembly portolása 32 bites Delphi-ről DCC64-re

Az eredeti _ftol idióma a 32 bites Delphi-ben egy okos egysorosnak (one-liner) tűnik: egy Pascal függvényburkoló, amely beágyazott assembly-be (inline assembly) ugrik, hogy manipulálja az x87 FPU vezérlőszavát (control word), csonkolja az FPU veremén lévő értéket, és kivegye az eredményt. Hosszú ideig hiba nélkül lefordult a DCC32 alatt, pontosan ezért kötött ki olyan sok régebbi grafikus és PDF unitban anélkül, hogy bárki megkérdőjelezte volna

Ha átállítja a fordítási célt 64 bitesre, a fordító az E1025 Unsupported language feature: 'ASM' hibával leáll. Ez a hiba nem egy kompatibilitási figyelmeztetés. Azt jelenti, hogy a DCC64 egyáltalán nem fordítja le a rutint, függetlenül attól, hogy az assembly korábban milyen jól működött

A 32 bites eredeti általában valahogy így nézett ki:

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

Ez a Pascal begin...end törzsön belüli asm blokk pontosan az, amit a DCC64 elutasít. A két fordítónak eltérő szabályai vannak arra vonatkozóan, hogy hol engedélyezett az assembly, és a határvonal számít

Miért húzza meg a határt máshol a DCC64

A DCC32 engedélyezi a beágyazott assembly-t (inline assembly) a normál Pascal rutinokon belül. A fordító ismeri a 32 bites hívási konvenciót, és képes kikövetkeztetni, hogy hol találhatók a helyi változók és paraméterek, így tolerálja azokat az assembly töredékeket, amelyek név szerint nyúlnak be a veremkeretbe (stack frame). A DCC64 szigorúbb álláspontot képvisel: az assembly-nek egy dedikált assembler függvényben kell lennie, ahol az egész törzs assembly, és a hívási konvenciót kifejezetten kezelik. A kevert Pascal-plusz-asm egyáltalán nem támogatott

A háttérben meghúzódó ok architekturális. A 64 bites Windows hívási konvencióban (Microsoft ABI) az első négy paraméter egész szám típusok esetén az RCX, RDX, R8 és R9 regiszterekben, míg lebegőpontos számok esetén az XMM0-tól XMM3-ig érkezik. A normál paraméterátadásban nincs x87 FPU részvétel; az x87 technikailag elérhető, de az ABI nem használja argumentumok továbbítására. Az az assembly, amely feltételezi, hogy egy érték „az FPU veremén” (on the FPU stack) van, egy olyan állapotról gondolkodik, amelyet a 64 bites ABI soha nem hoz létre

Tehát a régi kódtöredéknek nem csak szintaktikai problémája van. Még ha a DCC64 el is fogadná, a regiszterekre vonatkozó feltételezések tévesek lennének

Egy megfelelő 64 bites assembler verzió írása

Amikor valóban exportálnia kell egy _ftol szimbólumot cdecl konvencióval a bináris kompatibilitás miatt, a függvényt tisztán assembler rutinként kell megírni. A 64 bites ABI alatt egy Double paraméter az XMM0-ban érkezik, és az egész szám (integer) eredménynek az RAX-ban kell lennie a visszatéréskor. A .NOFRAME direktíva megmondja a DCC64-nek, hogy a rutin saját maga kezeli a vermét, ami megfelelő egy ilyen rövid levélfüggvényhez (leaf function):

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;

A CVTTSD2SI az az SSE2 utasítás, amely egy dupla pontosságú lebegőpontos számot alakít át előjeles egész számmá nulla felé történő csonkolással, ami pontosan az, amit az _ftol-nak csinálnia kell. Ez egyetlen utasítás, közvetlenül onnan veszi a paramétert, ahol az ABI hagyta, és oda helyezi az eredményt, ahol az ABI várja. Nincs szükség az FPU vezérlőszavával való zsonglőrködésre (FPU control-word juggling)

Vegye figyelembe, hogy ha a bemenet meghaladja egy 32 bites előjeles egész szám tartományát, a CVTTSD2SI az egész szám határozatlan (indefinite) értékét ($80000000) adja vissza. Ez ugyanaz a viselkedés, mint az x87 fistp esetében tartományon kívüli bemenetnél. Hogy a hívói (callers) képesek-e ilyen értékeket előállítani, érdemes megerősíteni, mielőtt a migrációt befejezettnek nyilvánítaná

Amikor a Trunc a jobb válasz

A fenti assembler verziót csak akkor érdemes megírni, ha tényleges bináris kompatibilitási követelménye van: valamilyen külső hívó (external caller) elvárja az _ftol szimbólumot egy adott hívási konvencióval, és nem tudja megváltoztatni ezeket a hívókat. Ez a helyzet nem gyakori. Legtöbbször az _ftol egy privát segítő (private helper) volt, amelyet csak ugyanabban a unitban használtak, és egyáltalán nincs külső függőség a nevétől vagy a konvenciójától

Ebben az esetben cserélje le sima (plain) Pascal-ra:

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

A Trunc nulla felé csonkol, ami megegyezik azzal, amit az _ftol csinált a csonkolási módra állított x87 vezérlőszóval (control word). DCC32-n és DCC64-en módosítás nélkül fordul. A fordító a megfelelő utasítást generálja minden egyes célpont számára: x64-en amúgy is jellemzően CVTTSD2SI-t fog kibocsátani (emit), ugyanazt az utasítást, mint a kézzel írott verzió. Azonos viselkedést kap, platformfüggő feltételek nélkül (no platform conditionals), és nem kell assembly-t karbantartania

Az egyetlen szemantikai különbség, amelyet érdemes ellenőrizni: a Trunc egy EInvalidOp kivételt dob a Delphi alapértelmezett konfigurációjában, ha a bemenet egy NaN vagy végtelen. Az x87 fistp az eredeti kódban csak egy bitmintát írt anélkül, hogy bármit is dobott volna. Ha az Ön kódja szokatlan lebegőpontos értékeket (floating-point values) táplál ebbe a függvénybe, és a régi viselkedés csendes volt, védekezzen a Math unit IsNaN és IsInfinite függvényeivel a Trunc meghívása előtt

Feltételes fordítás, ha mindkét célpont aktív marad

Egyes projekteknek továbbra is 32 bites és 64 bites binárisokat egyaránt kell szállítaniuk. Ha az eredeti assembler verziót meg kell tartani 32 bitre, és új implementációt kell biztosítani 64 bitre, használja a CPUX64 feltételt:

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;

Ez a minimális mechanikus javítás, és érdemes ideiglenesként kezelni. Egy olyan kódbázis, amely architektúra-specifikus assembly-t hordoz egy segítőben (helper), amelynek egyetlen célja a lebegőpontos-egész (float-to-integer) csonkolás, szükségtelen technikai adósságot (debt) cipel. A 32 bites elágazás (branch) teljesen eltűnhet, amint megerősíti, hogy semmi sem függ a régi implementáció FPU mellékhatásaitól (side-effects)

Ha a függvény több unitban használt komponensben jelenik meg, keressen rá az _ftol kifejezésre a teljes kódbázisban, mielőtt eldönti a migráció módját. Egy ilyen nevű szimbólum több helyen is deklarálható; a linker kiválaszt egyet, és csendben figyelmen kívül hagyja a többit, ami azt jelenti, hogy előfordulhat, hogy kijavít egy példányt, és mégis egy olyanhoz linkel (link against), amelyhez nem nyúltak