Win64 Delphi kodas gali žlugti ten, kur tas pats šaltinis Win32 sukasi švariai, ir HotPDF Delphi PDF komponentas per neseniai vykusį kietinimo etapą užkliuvo penkiais tokiais atvejais: Power(10, N) prisirišimu prie Single perkrovos, while ciklu, skaitančiu pasenusį TList.Count, High(Int64) riba, apvalinama iki 2^63, 15 skaitmenų float tekstu FPC viduje ir testų assertais, kurie sustabdo kompiliavimą
Joks jų nepasirodys, jei statote ir testuojate tik Win32 – būtent taip jie ir įslyso. Žemiau esantys atvejai ateina iš HotPDF SVG ir XPS importuotojų, jo puslapių atvaizduoklio ir JSON užduočių skaitytuvo, o cituoti skaitiniai rezultatai atkurti mažais zondo procesais, sustatytais Win32 ir Win64. Jei keliate Delphi kodų bazę į 64 bitus, kiekvienas vertas grep'o
Kodėl Power(10, 100) perpildo tik Win64?
Win64 System.Math.Power(10, N) su sveikaisiais argumentais išspręndžia į Single perkrovą, tad rezultatas skaičiuojamas ir grąžinamas viengubu tikslumu, o viskas, kas aukščiau maždaug 3.4E38, perpildo. Win32 tas pats kvietimas prisiriša prie Extended perkrovos ir sukasi ant x87 FPU su 80 bitų tikslumu, tad Power(10, 100) tiesiog yra 1E100
System.Math deklaruoja Power Extended, Double ir Single, plius atitinkančią IntPower šeimą, kurią Power kviečia, kai laipsnio rodiklis sveikasis. Win64 Extended yra tik Double aliasas (SizeOf(Extended) = 8), o dviem sveikiesiems argumentams kompiliatorius renkasi Single versiją. Išdavikas – tikslumas, o ne vien perpildymas: Win64 Power(10, 20) grąžina 1.0000000200408773E20, kas yra lygiai Single(1E20). Double rezultatas atspausdintų 1E20. Tą patį prisirišimą matėme su kiekvienu išbandytu Win64 kompiliatoriumi – nuo Delphi 10.3 iki kompiliatoriaus versijos 37.0
Kas vyksta toliau, priklauso nuo slankiojo kablelio išimčių kaukės. Delphi 12 ir vėlesnės pagal nutylėjimą uždengia visas slankiojo kablelio išimtis, tad perpildymas tylus: Power(10, 100) grąžina +Inf, o Power(10, -100) – 0. Delphi 11 ir ankstesnės exOverflow palieka atidengtą, ir tas pats kvietimas kelia EOverflow. Programos, pačios nustatančios kaukę, ir į tokius šeimininkus įkeliamos DLL gauna tai, ką pasirinko šeimininkas – todėl biblioteka negali manyti nė vienos baigties
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 spausdina 1E20; Win64 spausdina 1.0000000200408773E20 (Single perkrova)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Atkurkite tai, ką daro Delphi 11 arba griežtų FP nustatymų šeimininkas
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
exOverflow ir exInvalidOp atdengimas testo metu – pigiausias būdas pamatyti tai, ką mato senesnis kompiliatorius arba griežtas šeimininkas. Ant modernaus kompiliatoriaus su numatytosiomis nustatymais klaida nežlugsta – ji pagimdo begalybes ir nulius, o tuos testų žurnale pastebėti gerokai sunkiau. Ankstesnę kaukę atstatykite finally bloke: kaukė – gijos būsena, ir likusi testo bėgimo dalis paveldi tai, ką paliksite
Kaip perkrova pasiekė HotPDF SVG ir XPS importą
HotPDF SVG ir XPS kelių skaitytuvai dalijasi vienu skaičių skeneriu, o tas skeneris mantisę masteluoja su Power(10, Exponent), vos perskaitęs laipsnio rodiklį. Bet koks SVG, perduotas THotPDF.ImportSVGFormXObject (įėjimo taškui už SVG importavimo į PDF kaip pakartotinai naudojamus formos XObject), ir bet kokia kelio geometrija, apdorojama XPS ir OpenXPS konversijos į PDF metu, galėjo į tą kvietimą įleisti koordinatę, tokią kaip 1e100 arba 5e99
v2.770.91 laipsnio rodiklį jau ribojo iki 100 ir atmesdavo reikšmes, kurios praeitų 1E300, ir atrodė, kad to užtenka: 1E100 niekur šalia Double ribos, apie 1.8E308. Win64 vis tiek perpildydavo, nes skaičiavimas iš viso niekada nevykdavo Double viduje. Nuo v2.770.155 skeneris dešimties laipsnį stato pats, tad skaičiai, tokie kaip 1e-100, ar ilga mantisė su didele neigiama laipsnio rodiklio reikšme skaitomi kaip tikroji jų vertė, vietoj to, kad susigriautų į 0
Saugi dešimties laipsnis apribotiems laipsnio rodikliams
Kai laipsnio rodiklis apribotas, saugiausia dešimties laipsnis – ta, kurią pasistatote patys Double daugyba. Ne daugiau kaip 100 daugybų ciklas nieko nekainuoja greta aplinkinio teksto skenavimo, jis niekada nepagamina tarpinio, didesnio už galutinį mastelį, ir elgiasi identiškai Win32, Win64 ir Free Pascal
const
MaxDecimalExponent = 100;
function TryScaleByPowerOf10(const Value: Double; Exponent: Integer;
out Scaled: Double): Boolean;
var
Scale: Double;
I: Integer;
begin
Scaled := 0;
Result := False;
if (Exponent < -MaxDecimalExponent) or (Exponent > MaxDecimalExponent) then
Exit;
// Atmeskite rezultatus, išeinančius už Double intervalo
if (Exponent > 0) and (Value <> 0) and
(Log10(Abs(Value)) + Exponent > 300) then
Exit;
Scale := 1.0;
for I := 1 to Abs(Exponent) do
Scale := Scale * 10.0; // niekada neviršija 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // dalinkite: 1E-100 neturi tikraus Double
Result := True;
end;
Trys detalės neša sunkį. Intervalo patikra naudoja du lyginimus vietoj Abs(Exponent) <= 100, nes Abs(Low(Integer)) tebėra neigiamas ir praskrietų pro šalį. Neigiami laipsnio rodikliai dalija iš mastelio vietoj daugybos iš anksto apskaičiuotu 1E-100, kuris neturi tikraus Double ir pridėtų dar vieną apvalinimo žingsnį. O Log10 išankstinė patikra atmeta rezultatus už Double intervalo, dar prieš daugybai atsirandant galimybei perpilti
Būkite aiškūs dėl to, ko ciklas atsisako. Dešimties laipsniai iki 1E22 Double viduje tikrūs; toliau kiekviena daugyba apvalina, o po 100 jų mastelis sėdi keliais vienetais paskutinėje vietoje nuo teisingai apvalinto 1E100. Piešimo koordinatėms tai nematoma. Bendrosios paskirties teksto-į-double konversijai, kuri privalo atkurti kiekvieną reikšmę bitas po bito, to nepakanka, ir jums reikia teisingai apvalinančio konversijos algoritmo
Kada dcc64 skaito pasenusį TList.Count while cikle
Stebėjome, kaip Win64 kompiliatorius (dcc64, kompiliatoriaus versija 37.0) sugeneravo kodą while List.Count > Start do ciklui, trinančiam nuo sąrašo galo ir lyginančiam su steko laikinuoju vietoj pakartotinio Count skaitymo. Perrašymas, jį ištaisęs, – for ... downto ciklas, kurio ribos pagal apibrėžtį vertinamos lygiai kartą
Ciklas atkeliavo v2.769.3, kuris išmokė atvaizduoklio permatomumo grupės kodą išlaikyti grupės viduje sukurtus švelniąsias kaukes gyvus per dviejų ėjimų atvaizdavimą ir atlaisvinti juos po to. Valymas sėdėjo finally bloke po vieno ar dviejų ėjimų for ciklo, plytelių ciklo viduje. Sutrauktas iki savo išvaizdos, prieš ir po atrodo taip:
// Išvaizda, kurią matėme sukompiliuotą neteisingai dcc64 (kompiliatoriaus versija 37.0)
procedure DropMasksWhile(Masks: TList; Start: Integer);
begin
while Masks.Count > Start do
begin
TObject(Masks[Masks.Count - 1]).Free;
Masks.Delete(Masks.Count - 1);
end;
end;
// Pakaitalas: ribos įvertinamos kartą, nėra laikinojo, galinčio pasenti
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count yra NativeInt nuo Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
Sugeneruotame Win64 kode Count ciklo sąlygoje ir Count, skaitomas kūno viduje, dalijosi vienu steko langeliu. Sąlyga lygino su tuo langeliu įėjime, dar niekam jo nerašius, ir niekas jo neatnaujino po Delete. Kai grupė nebūdavo sukūrusi savų švelniųjų kaukių, kūnas vis tiek sukodavo ir prašydavo tuščio sąrašo duoti elementą -1, tad 64 bitų dariniuose kiekvienas puslapis su tokia permatomumo grupe žlugdavo su EListError. Win32 kodas tam pačiam šaltiniui buvo teisingas, o v2.770.1 ciklą pakeitė
Mes to nesutraukėme iki minimalaus atkūrimo, ir mažas atskiras ciklas, toks kaip DropMasksWhile, gana tikra susikompiliuos teisingai; aplinkinis try/finally ir įdėtieji ciklai, atrodo, turi reikšmės. Traktuokite tai kaip vienoje kompiliatoriaus versijoje stebėtą kodo generavimą, ne kaip žinomą kiekvieno Win64 kompiliatoriaus defektą. Praktinė pamoka pigesnė už šakninę priežastį: ciklas, kurio sąlyga pakartotinai skaito kolekcijos skaičių, kol kūnas tą kolekciją mažina, vertas perrašymo į fiksuotų ribų for ... downto, o atvaizduoklio pakeitimams reikia pilno Win64 testo bėgimo, o ne vien Win32
Griūties, kurią rodo tik optimizuotas Win64 darinys, suradimas
Nesėkmė atsikartojo tik optimizuotame Win64 darinyje, tad vieta atkeliavo iš įrankių už IDE ribų. Maža zondo programa užregistravo vektorinį išimčių doroklį su AddVectoredExceptionHandler, užfiksavo steką pirmosios išimties metu su RtlCaptureStackBackTrace ir grąžos adresus išvertė į funkcijų vardus naudodama detalųjį failų žemėlapį, kurį linkeris rašo su -GD. Tos funkcijos išskleidimas tada parodė lyginimą, skaitantį steko langelį [rbp+0x298], rašytą tik ciklo kūno viduje. Tai tas įrodymų lygis, kurio norite prieš kaltindami kompiliatorių, ir užėmė mažiau laiko nei žingsniavimas pro release darinį
Kodėl High(Int64) nėra saugi viršutinė riba Double?
Double negali atvaizduoti High(Int64): 9223372036854775807 konversija į Double apvalina iki tiksliai 2^63 – vienu už didžiausiąjį Int64. Win64 ta konversija vyksta pačiame lyginime, tad D <= High(Int64) yra True esant D = 2^63, o sekantis Round arba Trunc perpildo
Win32 tai slepia dėl tos pačios priežasties, kuria slėpė Power problemą. Lyginimas sukasi 80 bitų Extended tikslumu su 64 bitų mantise, kur High(Int64) tikrus, o 2^63 teisingai lyginasi didesne. Win64 neturi platesnio tipo, į kurį grįžti. Už intervalo einanti konversija irgi graži nebūna: mūsų Win64 testuose Round(2^63) grąžino Low(Int64) – tylią ženklo apvertimą, nesvarbu, ar exInvalidOp uždengtas. Win32 grąžina tą pačią reikšmę uždengtai kaukei ir kelia EInvalidOp atidengtai
| Išraiška | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), exceptions masked (Delphi 12+ default) | 1E100 | +Inf |
Power(10, 100), exOverflow unmasked | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp unmasked | EInvalidOp | Low(Int64) |
HotPDF tai sutiko JSON skaitytuve, slypinčiame už jo dokumento užduočių reikšmių. JSON skaičiams jokio intervalo limito nėra, o senasis serializatorius bet kurią reikšmę su Frac(Value) = 0 paverčia sveikuoju su Round, tad visiškai teisėta 1e19 tapdavo arba netinkamu sveikuoju, arba išimtimi – pagal kaukę. Nuo v2.770.169 sveikasis skaičius rašomas kaip sveikasis tik tada, kai telpa į Int64, visa kita išlaiko savą slankiojo kablelio tekstą, o sveikųjų getteriai už intervalo einančioms reikšmėms grąžina kvietėjo numatytąją, o ne apvyniotąją
const
TwoPow63 = 9223372036854775808.0; // 2^63, tikras Double ir Extended viduje
function TryDoubleToInt64(const Value: Double; out R: Int64): Boolean;
begin
R := 0;
Result := not IsNan(Value) and not IsInfinite(Value) and
(Frac(Value) = 0) and (Value >= -TwoPow63) and (Value < TwoPow63);
if Result then
R := Trunc(Value);
end;
function JsonNumberText(const Value: Double): string;
var
R: Int64;
begin
// Kvietėjai pirmiausia atmeta NaN ir begalybes: JSON jų neparašo
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral sustoja ties 15 skaitmenų
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Viršutinė riba – literalas 9223372036854775808.0 su griežtu <. Ta konstanta – 2^63, tikra ir Double, ir Extended viduje, tad lyginimas kiekvienoje platformoje reiškia tą patį. Apatinė riba gali naudoti >=, nes -2^63 yra lygiai Low(Int64). IsNan ir IsInfinite tikrinimas pirmiausia, trumpojo jungimo įvertinimu, laiko NaN ir begalybes atokiau nuo Frac ir lyginimų, kurie gali kelti EInvalidOp, kai šeimininkas juos atidengęs
Kiek skaitmenų float-į-tekstą iš tikrųjų duoda Win64?
Mažiau, nei prašote, dviejuose kompiliatoriuose iš trijų. Free Pascal 3.3.1 FloatToStrF(Value, ffGeneral, 17, 0) Win64 sustoja ties 15 reikšmingųjų skaitmenų, tad 1/3 grįžta kaip 0.333333333333333, o dvi skirtingos Double reikšmės gali užrašytis į identišką tekstą. Str(Value:24, Text), po kurio Trim, duoda 17 reikšmingųjų skaitmenų moksline notacija – 3.3333333333333331E-001 tai pačiai reikšmei – ir visada rašo tašką kaip dešimtainį skirtuką, nesvarbu, kokia lokalė. Jeigu HotPDF ant FPC yra jūsų statymo matricos dalis, HotPDF Free Pascal ir Lazarus Win64 palaikymo pastabos dengia likusius platformų skirtumus
Delphi priima 17 skaitmenų prašymą, bet abu Delphi tikslai vis dar nesutaria dėl išvesties: FloatToStrF(0.1, ffGeneral, 17, 0) duoda 0.10000000000000001 Win32 ir 0.1 Win64. Win64 RTL gali įnešti paskutinio skaitmens apvalinimo klaidą ir formatuodamas, ir skaitęs, tad daugiau skaitmenų siaurina plyšį negarantuodamos, kad kiekvienas Double bitų raštas išgyvens teksto kelionę pirmyn ir atgal. HotPDF dokumentacija tokio pažado neduoda, ir jūsų neturėtų, nebent gabenate savą teisingai apvalinantį formatuotoją ir skaitiklį. Perduokite TFormatSettings.Invariant arba patys pakeiskite skirtuką senesnėse Delphi versijose, kad vokiečių ar prancūzų lokalė neįrašytų kablelio į JSON
Kodėl Assert.AreEqual sustabdo kompiliavimą Win64?
Assert.AreEqual(3, Length(Arr)) ant dinaminio masyvo susikompiliuoja Win32 ir krinta Win64 su E2532 – „Couldn't infer generic type argument from different argument types“ – nes dinaminio masyvo Length Win64 grąžina NativeInt. Su Integer literalu vienoje pusėje ir 64 bitų NativeInt kitoje DUnitX generinis Assert.AreEqual<T> neapsisprendžia dėl vienintelės T, ir statymas sustoja
TList.Count sukelia tą pačią klaidą nuo Delphi 12, kur savybė tapo NativeInt; Delphi 11 vis dar deklaruoja ją kaip Integer. string Length abiejose platformose grąžina Integer ir nepaveikta – todėl klaida matoma kai kuriuose testų unituose, o kituose ne. Rašykite tipo argumentą aiškiai – Assert.AreEqual<NativeInt>(3, Length(Arr)) – ir statykite testų projektą dcc64 prieš commitinant. Rinkinys, statomas tik Win32, apie savo sulaužytą Win64 statymą nepraneš, kol kiti nebandys
Win64 pernešimo atmintinė Delphi skaitiniam kodui
- Ieškokite
Power(irIntPower(kvietimų su sveikaisiais argumentais; perduokiteDoubletipo reikšmes arba statykite apribotas dešimties laipsnis patys - Paleiskite skaitinius testus bent kartą su
exOverflowirexInvalidOp, nuimtais perSetExceptionMask, abiejose Win32 ir Win64 - Rašykite
Int64viršutinę ribą kaip< 9223372036854775808.0, niekada<= High(Int64), ir atmeskite NaN bei begalybes prieš bet kokį lyginimą - Neverskite išanalizuoto skaičiaus į
Int64vien todėl, kadFraclygus 0; JSON skaičiai gali būti gerokai didesni - Perrašykite
whileciklus, pakartotinai skaitančiusCount, kol trinami elementai, į fiksuotų ribųfor ... downtociklus - FPC Win64 naudokite
Str(Value:24, Text), kai reikia daugiau nei 15 reikšmingųjų skaitmenų - Naudokite
Assert.AreEqual<NativeInt>LengthirCountassertams, ir statykite testus dcc64 prieš commitinant - Po bet kokio analizatoriaus ar atvaizduoklio pakeitimo praveskite pilną regresijos rinkinį Win32 ir Win64, o ne vieną iš jų
Bibliotekos pusės taisymai, aprašyti čia, visi HotPDF nuo v2.770.169, tad SVG importas, XPS konversija, permatomumo atvaizdavimas ir JSON užduočių apdorojimas dabar Win64 elgiasi taip pat kaip Win32. Jeigu PDF failus generuojate arba apdorojate iš Delphi arba C++Builder abiems platformoms, HotPDF Delphi PDF komponento puslapyje yra parsisiuntimai ir pilnas funkcijų sąrašas