A Win64-es Delphi kód ott bukhat el, ahol ugyanaz a forrás Win32-en tisztán fut, és a HotPDF Delphi PDF component öt ilyen esetbe botlott egy friss megerősítő menetben: a Power(10, N) Single overloadjába kötés, egy while ciklus, ami elavult TList.Count-ot olvas, egy High(Int64) határ, ami 2^63-ra felfelé kerekít, 15 számjegyű float szöveg FPC-n és teszt assertek, amik leállítják a kompilálást
Egyik sem mutatkozik meg, ha csak Win32-t buildelsz és tesztelsz, és pontosan így csúsztak be. Az alábbi esetek a HotPDF SVG és XPS importereiből, az oldalrendereréből és a JSON job olvasójából jönnek, és az idézett numerikus eredmények kis, Win32-re és Win64-re épített próbaprogramokkal lettek reprodukálva. Ha épp egy Delphi kódbázist viszel 64 bitesre, mindegyik megér egy grep-et
Miért csak Win64-en csordul túl a Power(10, 100)?
Win64-en a System.Math.Power(10, N) egész argumentumokkal a Single overloadhoz kötődik, így az eredmény single pontossággal számolódik és adódik vissza, és minden, nagyjából 3.4E38 fölötti túlcsordul. Win32-n ugyanez a hívás az Extended overloadhoz kötődik, és az x87 FPU-n fut 80 bites pontossággal, így a Power(10, 100) egyszerűen 1E100
A System.Math a Power-t Extended-re, Double-ra és Single-re deklarálja, plusz egy illeszkedő IntPower családot, amit a Power hív, ha a kitevő egész szám. Win64-en az Extended csak a Double aliasa (SizeOf(Extended) = 8), és két egész argumentumra a kompilátor a Single verziót választja. Az árulkodó jel a pontosság, nem csak a túlcsordulás: Win64-en a Power(10, 20) 1.0000000200408773E20-t ad vissza, ami pontosan Single(1E20). Egy Double eredmény 1E20-ként íródna. Ugyanezt a kötést láttuk minden Win64 kompilátorral, amit kipróbáltunk, Delphi 10.3-tól a 37.0-s kompilátorverzióig
Aztán az történik, amit a lebegőpontos kivételmaszk enged. A Delphi 12 és újabb alapból minden lebegőpontos kivételt maszkol, így a túlcsordulás csendes: a Power(10, 100) +Inf-et ad, a Power(10, -100) 0-t. A Delphi 11 és korábbi maszkolatlanul hagyja az exOverflow-ot, és ugyanez a hívás EOverflow-t dob. Azok az alkalmazások, amik maguk állítják a maszkot, és az ilyen hostokba töltött DLL-ek, azt a viselkedést kapják, amit a host választott, ezért egy library egyik kimenetre sem számíthat
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 1E20-t ír; Win64 1.0000000200408773E20-t (Single overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reprodukálja, amit a Delphi 11 vagy egy szigorú FP beállítású host csinál
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Az exOverflow és exInvalidOp maszkolásának feloldása egy teszt idejére a legolcsóbb módja annak, hogy lásd, mit lát egy öregebb kompilátor vagy egy szigorú host. Modern kompilátornál alapbeállításokkal a hiba nem crashel, végteleneket és nullákat gyárt, és azokat sokkal nehezebb észrevenni egy tesztnaplóban. Állítsd vissza az előző maszkot finally-ben: a maszk threadenkénti állapot, és a tesztfutás többi része örökli, amit magad után hagysz
Hogyan került az overload a HotPDF SVG és XPS importjába
A HotPDF SVG és XPS útolvasói egyetlen számszkenert osztanak meg, és az a szkener Power(10, Exponent)-tal skálázta a mantisszát, amint kiolvasott egy kitevőt. Bármely SVG, ami a THotPDF.ImportSVGFormXObject-ba megy (a SVG PDF-be importálása újrahasznosítható form XObjectként mögötti belépőpont), és bármely útgeometria, ami a XPS és OpenXPS PDF-be konvertálása közben kezelődik, tehát olyan koordinátát is beleenethet ebbe a hívásba, mint az 1e100 vagy az 5e99
A v2.770.91 már 100-ra tette fel a kitevőt, és elutasította azokat az értékeket, amik átmennének az 1E300-on, ami elégnek nézett ki: az 1E100 messze van a Double nagyjából 1.8E308-as limitjétől. Win64-en mégis túlcsordult, mert a számolás egyáltalán nem Double-ben történt. v2.770.155 óta a szkener maga építi a tíz hatványát, és az olyan számok, mint az 1e-100, vagy egy hosszú mantissza nagy negatív kitevővel, a valódi értékükön olvasódnak, 0-ba zsugorodás helyett
Biztonságos tíz hatványa korlátolt kitevőkre
Ha a kitevő korlátos, a legbiztonságosabb tíz hatványa az, amit magad építesz Double szorzásokkal. Legfeljebb 100 szorzás ciklusa semmit sem kerül a körülötte lévő szöveg szkenneléséhez képest, sosem produkál a végső skálánál nagyobb köztes értéket, és Win32-n, Win64-en és Free Pascalban azonosan viselkedik
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;
// Elutasítja azokat az eredményeket, amik kiszélesednének a Double tartományból
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; // sosem haladja meg az 1E100-at
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // osztás: az 1E-100-nak nincs egzakt Double reprezentációja
Result := True;
end;
Három részlet hordozza a súlyt. A tartományteszt két összehasonlítást használ, nem Abs(Exponent) <= 100-at, mert az Abs(Low(Integer)) továbbra is negatív, és simán átsiklana. A negatív kitevők osztanak a skálával, ahelyett hogy egy előre kiszámolt 1E-100-mal szoroznának, aminek nincs egzakt Double reprezentációja, és még egy kerekítési lépést adna. És a Log10 előteszt még a szorzás előtt elutasítja a Double tartományán kívüli eredményeket, mielőtt a túlcsordulásnak módja lenne
Légy tisztában azzal, mit ad fel a ciklus. A tíz hatványai 1E22-ig egzaktok Double-ben; azon túl minden szorzás kerekít, és 100 után a skála néhány egységre az utolsó helyen ül a helyesen kerekített 1E100-tól. Rajzolási koordinátákhoz ez láthatatlan. Egy általános célú szöveg-double konverzióhoz, aminek minden értéket bájtra bájtra vissza kell adnia, nem elég jó, és helyesen kerekítő konverziós algoritmus kell helyette
Amikor a dcc64 elavult TList.Count-ot olvas egy while-ban
Láttuk, hogy a Win64 kompilátor (dcc64, kompilátorverzió 37.0) kódot generált egy while List.Count > Start do ciklusra, ami a lista végéről törölt, és stack-temporálissal hasonlított a Count újraolvasása helyett. A megoldás egy for ... downto ciklus volt, aminek a határai definíció szerint pontosan egyszer értékelődnek ki
A ciklus a v2.769.3-ban érkezett, ami megtanította a renderer transzparencia-csoport kódjának, hogy a csoporton belül készített soft maszkokat életben tartsa egy kétmenetes renderen át, és azután szabadítsa fel őket. A takarítás egy finally blokkban ült egy egy- vagy kétmenetes for ciklus után, a csempénkénti cikluson belül. Alakjára zsugorítva az előtte-utána így néz ki:
// Az a forma, amit dcc64 (kompilátor verzió 37.0) félrefordítva láttunk
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;
// Csere: a határok egyszer értékelődnek ki, nincs elavuló temporális
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // a TList.Count Delphi 12 óta NativeInt
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
A generált Win64 kódban a ciklusfeltételben lévő Count és a testben olvasott Count egyetlen stack helyet osztott meg. A feltétel belépéskor ahhoz a helyhez hasonlított, mielőtt bármi írt volna bele, és a Delete után semmi nem frissítette. Amikor egy csoport nem készített saját soft maszkot, a test így is lefutott, és egy üres listától kérte a -1. elemet, így 64 bites buildekben minden olyan oldal, ami ilyen transzparencia-csoportot tartalmazott, EListError-ral bukott el. Ugyanennek a forrásnak a Win32 kódja helyes volt, és a v2.770.1 kicserélte a ciklust
Nem zsugorítottuk minimális reprodukcióra, és egy kis önálló ciklus, mint a DropMasksWhile, jól lehet helyesen fordul; a környező try/finally és a beágyazott ciklusok tűnnek számítódnak. Kezeld úgy, mint egy kompilátorverzión megfigyelt kódgenerálást, nem mint minden Win64 kompilátor ismert hibáját. A gyakorlati tanulság olcsóbb, mint a gyökérok: olyan ciklust, aminek a feltétele újraolvassa egy kollekció countját, miközben a test zsugorítja azt a kollekciót, érdemes rögzített határú for ... downto-ra átírni, és a renderer változtatások teljes Win64 tesztfutást igényelnek, nem csak Win32-t
Egy crash megtalálása, amit csak az optimalizált Win64 build mutat
A hiba csak az optimalizált Win64 buildben reprodukálódott, így a hely az IDE-n kívüli eszközökből jött. Egy kis próbaprogram vektorált kivételkezelőt regisztrált AddVectoredExceptionHandler-ral, az első kivételnél RtlCaptureStackBackTrace-szel elkapta a stacket, és a visszatérési címeket függvénynevekké fordította a részletes map fájl segítségével, amit a linker -GD-vel ír. Annak a függvénynek a szétszerelése megmutatta, hogy az összehasonlítás egy stack helyet olvas, [rbp+0x298], amit csak a ciklus testén belül írtak soha. Ez az a bizonyítékszint, amit akarsz, mielőtt a kompilátort vádolod, és kevesebb időbe telt, mint egy release builden végiglépkedni
Miért nem biztonságos felső határ a Double-nak a High(Int64)?
Egy Double nem tudja reprezentálni a High(Int64)-et: a 9223372036854775807 Double-ra váltása pontosan 2^63-ra kerekít fel, egyvel a legnagyobb Int64 fölött. Win64-en az a konverzió magában az összehasonlításban történik, így a D <= High(Int64) True a D = 2^63-ra, és az azt követő Round vagy Trunc túlcsordul
A Win32 ugyanazért rejti ezt, amiért a Power problémát rejti. Az összehasonlítás 80 bites Extended pontossággal fut 64 bites mantisszával, ahol a High(Int64) egzakt, és a 2^63 helyesen nagyobbnak hasonlít. A Win64-nek nincs szélesebb típusa, amire visszaeshet. A tartományon kívüli konverzió sem szép: Win64 tesztjeinkben a Round(2^63) Low(Int64)-ot adott vissza, csendes előjelcsere, függetlenül attól, hogy az exInvalidOp maszkolva volt-e. A Win32 maszkolva ugyanazt az értéket adja, maszkolatlanul EInvalidOp-ot dob
| Kifejezés | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), kivételek maszkolva (Delphi 12+ alapérték) | 1E100 | +Inf |
Power(10, 100), exOverflow maszkolatlan | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp maszkolatlan | EInvalidOp | Low(Int64) |
A HotPDF a dokumentum job értékek mögötti JSON olvasójában találkozott ezzel. A JSON nem tesz tartománykorlátot a számokra, és a régi szerializátor minden Frac(Value) = 0 értéket Round-dal egésszé alakított, így egy teljesen legális 1e19 vagy rossz egésszé vált, vagy kivétellé, a maszk szerint. v2.770.169 óta egy egész szám csak akkor íródik egészként, ha belefér az Int64-be, minden más megtartja a lebegőpontos szövegét, és az egész getterek a hívó alapértékét adják vissza tartományon kívüli értékekre, becsomagolt helyett
const
TwoPow63 = 9223372036854775808.0; // 2^63, egzakt Double-ben és Extendedben
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
// A hívók előbb utasítják el a NaN-t és a végteleneket: a JSONnek nincs írásmódja rájuk
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // az FPC Win64 ffGeneral 15 számjegynél megáll
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
A felső határ a 9223372036854775808.0 literál szigorú <-tal. Az a konstans 2^63, egzakt Double-ben és Extended-ben egyaránt, így az összehasonlítás minden platformon ugyanazt jelenti. Az alsó határ használhat >=-t, mert a -2^63 pontosan Low(Int64). Az IsNan és IsInfinite először való tesztelése, rövidzáras kiértékeléssel, távol tartja a NaN-t és a végteleneket a Frac-tól és az összehasonlításoktól, amik EInvalidOp-ot dobhatnak, ha a host maszkolatlanul hagyta
Mennyi számjegyet ad valójában a float-szöveg konverzió Win64-en?
Kevesebbet, mint amennyit kérsz, három kompilátorból kettőn. A Free Pascal 3.3.1 FloatToStrF(Value, ffGeneral, 17, 0) hívása Win64-en 15 jelentős számjegynél megáll, így az 1/3 0.333333333333333-ként jön vissza, és két különböző Double érték azonos szöveggé sorosítóghat. A Str(Value:24, Text) utáni Trim 17 jelentős számjegyet ad tudományos jelölésben, ugyanarra az értékre 3.3333333333333331E-001-et, és mindig pontot ír tizedeselválasztóként, lokáltól függetlenül. Ha a FPC-s HotPDF része a build mátrixodnak, a HotPDF Free Pascal és Lazarus Win64 támogatási jegyzetei lefedik a platformkülönbségek többi részét
A Delphi elfogadja a 17 számjegyű kérést, de a két Delphi cél a kimeneten mégis eltér: a FloatToStrF(0.1, ffGeneral, 17, 0) Win32-en 0.10000000000000001-t, Win64-en 0.1-et ad. A Win64 RTL az utolsó számjegy kerekítési hibáját is becsúsztathatja formázáskor és parzoláskor egyaránt, így több számjegy szűkíti a rést anélkül, hogy garantálná, minden Double bátminta túléli a szöveges oda-vissza utat. A HotPDF dokumentációja nem ígér ilyet, és a tiéd sem ígérjen, hacsak nem szállítasz saját, helyesen kerekítő formázót és parzolót. Add át a TFormatSettings.Invariant-ot, vagy cseréld meg az elválasztót magad öregebb Delphi verziókon, hogy egy német vagy francia lokál ne írjon vesszőt a JSON-be
Miért áll le a kompilálás az Assert.AreEqual-től Win64-en?
A Assert.AreEqual(3, Length(Arr)) dinamikus tömbön Win32-re fordul, Win64-en E2532-vel elbukik, „Nem sikerült a generikus típusargumentumot következtetni különböző argumentumtípusokból", mert egy dinamikus tömb Length-e Win64-en NativeInt. Egy oldalon Integer literállal, a másikon 64 bites NativeInt-tel a DUnitX generikus Assert.AreEqual<T>-ja nem tud egyetlen T-n megállapodni, és a build megáll
A TList.Count ugyanezt a hibát váltja ki Delphi 12 óta, amikor a property NativeInt lett; a Delphi 11 még Integer-ként deklarálja. Egy string Length-e mindkét platformon Integer-t ad, és nem érintett, ezért a hiba néhány teszt unitban felbukkan, másokban nem. Írd ki expliciten a típusargumentumot, Assert.AreEqual<NativeInt>(3, Length(Arr)), és dcc64-gyel fordítsd a teszt projektet, mielőtt commitolsz. Egy suite, ami csak Win32-re buildel soha, nem árulja el, hogy a Win64 buildje törött, míg meg nem próbálja valaki más
Win64 portolási ellenőrzőlista Delphi numerikus kódhoz
- Keress
Power(ésIntPower(hívásokat egész argumentumokkal; adj átDoubletípusú értékeket, vagy építsd magad a korlátolt tíz hatványait - Futtasd a numerikus teszteket legalább egyszer
exOverflowésexInvalidOpeltávolításávalSetExceptionMask-on át, Win32-n és Win64-en egyaránt - Írd az
Int64felső határt< 9223372036854775808.0-ként, soha nem<= High(Int64)-ként, és utasítsd el a NaN-t meg a végteleneket bármely összehasonlítás előtt - Ne váld át egy parzolt számot
Int64-be csak azért, mert aFrac0; a JSON számok jóval nagyobbak lehetnek - Írd át azokat a
whileciklusokat, amik elemek törlése közben újraolvasják aCount-ot, rögzített határúfor ... downtociklusokra - FPC Win64-en használd a
Str(Value:24, Text)-et, ha 15-nél több jelentős számjegyre van szükséged - Használj
Assert.AreEqual<NativeInt>-otLengthésCountassertekhez, és dcc64-gyel fordítsd a teszteket, mielőtt commitolsz - Bármely parser- vagy renderer-változtatás után futtasd a teljes regressziós suite-ot Win32-n és Win64-en, nem csak az egyiken
A library oldali javítások, amiket itt leírtunk, mind a HotPDF-ben vannak v2.770.169 óta, így az SVG import, az XPS konverzió, a transzparencia renderelés és a JSON job kezelés mostantól Win64-en ugyanúgy viselkedik, mint Win32-n. Ha Delphiből vagy C++Builderből generálsz vagy dolgozol fel PDF fájlokat mindkét platformra, a HotPDF Delphi PDF component oldalon vannak a letöltések meg a teljes funkciólista