Techninis straipsnis

„_ftol“ įterptinio asemblerio perkėlimas iš 32 bitų „Delphi“ į DCC64

Originali _ftol idioma 32 bitų „Delphi“ atrodo kaip sumanus vienos eilutės sprendimas: Paskalio funkcijos įvynioklis (angl. wrapper), kuris pereina į įterptinį asemblerį, kad manipuliuotų x87 FPU valdymo žodžiu, nupjautų reikšmę FPU steke ir ištrauktų rezultatą. Ji ilgą laiką puikiai kompiliavosi su DCC32, ir būtent todėl atsidūrė daugelyje senesnių grafikos ir PDF modulių, niekam nekeliant klausimų

Pakeitus kompiliavimo tikslą į 64 bitų, kompiliatorius nutraukia darbą su klaida E1025 Unsupported language feature: 'ASM'. Ši klaida nėra suderinamumo įspėjimas. Tai reiškia, kad DCC64 apskritai nekompiliuos šios procedūros, nepaisant to, kaip gerai asemblerio kodas veikė anksčiau

32 bitų originalas paprastai atrodė maždaug taip:

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

Būtent šio asm bloko Paskalio begin...end kūne DCC64 ir nepriima. Abu kompiliatoriai turi skirtingas taisykles, kur leidžiama naudoti asemblerį, ir ši riba yra svarbi

Kodėl DCC64 nubrėžia ribą kitaip

DCC32 leidžia įterptinį asemblerį paprastose Paskalio procedūrose. Kompiliatorius žino 32 bitų iškvietimo konvenciją ir gali nustatyti, kur yra vietiniai kintamieji ir parametrai, todėl jis toleruoja asemblerio fragmentus, kurie pasiekia steko kadrą pagal pavadinimą. DCC64 užima griežtesnę poziciją: asembleris turi būti skirtoje asemblerio funkcijoje, kurios visas kūnas yra asembleris ir kurios iškvietimo konvencija valdoma eksplicitiškai. Mišrus Paskalio ir asemblerio kodas apskritai nepalaikomas

Pagrindinė priežastis yra architektūrinė. 64 bitų „Windows“ iškvietimo konvencijoje („Microsoft ABI“) pirmi keturi parametrai pateikiami registruose RCX, RDX, R8 ir R9 sveikųjų skaičių tipams, arba nuo XMM0 iki XMM3 slankiojo kablelio skaičiams. Įprastame parametrų perdavime x87 FPU nedalyvauja; x87 techniškai yra prieinamas, bet ABI jo nenaudoja argumentų transportavimui. Asembleris, kuris daro prielaidą, kad reikšmė yra „FPU steke“, remiasi būsena, kurios 64 bitų ABI niekada nesukuria

Taigi senasis fragmentas turi ne tik sintaksės problemą. Net jei DCC64 jį ir priimtų, prielaidos apie registrus būtų neteisingos

Tinkamos 64 bitų asemblerio versijos rašymas

Kai jums tikrai reikia eksportuoti _ftol simbolį su cdecl konvencija dėl dvejetainio suderinamumo, funkcija turi būti parašyta kaip gryna asemblerio procedūra. Pagal 64 bitų ABI Double parametras atkeliauja į XMM0, o sveikojo skaičiaus rezultatas grįžtant turi būti RAX registre. Direktyva .NOFRAME nurodo DCC64, kad procedūra pati valdo savo steką, o tai tinka tokiai trumpai galinei (angl. leaf) funkcijai:

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 yra SSE2 instrukcija, skirta dvigubo tikslumo slankiojo kablelio skaičių konvertuoti į sveikąjį skaičių su ženklu, nupjaunant link nulio, o tai yra būtent tai, ką _ftol ir turi daryti. Tai yra viena instrukcija, ji paima parametrą tiesiai iš ten, kur jį paliko ABI, ir padeda rezultatą ten, kur ABI jo tikisi. Nereikia jokio žongliravimo FPU valdymo žodžiais

Atkreipkite dėmesį, kad jei įvestis viršija 32 bitų sveikojo skaičiaus su ženklu diapazoną, CVTTSD2SI grąžina neapibrėžtą sveikojo skaičiaus reikšmę ($80000000). Tai yra toks pat elgesys kaip ir x87 fistp su diapazoną viršijančia įvestimi. Prieš paskelbiant migraciją baigta, verta patikrinti, ar jūsų iškvietėjai gali sukurti tokias reikšmes

Kada „Trunc“ yra geresnis atsakymas

Aukščiau pateiktą asemblerio versiją verta rašyti tik tuomet, kai turite realų dvejetainio suderinamumo reikalavimą: koks nors išorinis iškvietėjas tikisi _ftol simbolio su konkrečia iškvietimo konvencija, ir jūs negalite tų iškvietėjų pakeisti. Tokia situacija pasitaiko nedažnai. Dažniausiai _ftol buvo privatus pagalbininkas, naudojamas tik tame pačiame modulyje, ir nėra jokios išorinės priklausomybės nuo jo pavadinimo ar konvencijos

Tokiu atveju pakeiskite jį paprastu Paskaliu:

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

Trunc nupjauna link nulio, o tai atitinka tai, ką darė _ftol su x87 valdymo žodžiu, nustatytu į nupjovimo režimą. Jis kompiliuojasi su DCC32 ir DCC64 be modifikacijų. Kompiliatorius sugeneruoja atitinkamą instrukciją kiekvienai platformai: x64 sistemoje jis vis tiek paprastai išves CVTTSD2SI, tą pačią instrukciją, kaip ir ranka rašytoje versijoje. Gaunate identišką elgesį, jokių sąlyginių platformos direktyvų ir jokio asemblerio, kurį reikėtų prižiūrėti

Vienas semantinis skirtumas, kurį verta patikrinti: Trunc sukelia EInvalidOp išimtį numatytojoje „Delphi“ konfigūracijoje, kai įvestis yra NaN arba begalybė. x87 fistp originaliame kode tiesiog įrašydavo bitų šabloną nesukeldamas jokios išimties. Jei jūsų kodas į šią funkciją paduoda neįprastas slankiojo kablelio reikšmes ir senasis elgesys buvo tylus, prieš kviesdami Trunc apsaugokite naudodami IsNaN ir IsInfiniteMath modulio

Sąlyginis kompiliavimas, kai abu tikslai išlieka aktyvūs

Kai kurie projektai privalo ir toliau platinti tiek 32 bitų, tiek 64 bitų dvejetainius failus. Jei originali asemblerio versija turi būti palikta 32 bitų sistemai, o 64 bitų sistemai pateikiama nauja realizacija, naudokite CPUX64 sąlygą:

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;

Tai yra minimalus mechaninis pataisymas, ir jį verta vertinti kaip laikiną. Kodų bazė, kuri nešiojasi nuo architektūros priklausomą asemblerį pagalbiniame metode, kurio vienintelis tikslas yra slankiojo kablelio nupjovimas iki sveikojo skaičiaus, neša nereikalingą skolą. 32 bitų atšaka gali visiškai išnykti, kai tik patvirtinsite, kad niekas nepriklauso nuo senosios realizacijos FPU šalutinių poveikių

Jei funkcija pasirodo komponente, naudojamame keliuose moduliuose, prieš nuspręsdami, kaip migruoti, ieškokite _ftol visoje kodų bazėje. Tokiu pavadinimu pavadintas simbolis gali būti deklaruotas daugiau nei vienoje vietoje; saistyklė (angl. linker) pasirenka vieną ir tyliai ignoruoja kitus, o tai reiškia, kad galite pataisyti vieną kopiją ir vis tiek susieti su kita, kuri nebuvo paliesta