Win64 Delphi kod može pasti gde isti izvor radi čisto na Win32, i HotPDF Delphi PDF komponenta nanela je pet takvih slučajeva tokom skorašnjeg prolaza očvršćavanja: Power(10, N) vezan na Single overload, while petlja koja čita zastareli TList.Count, High(Int64) granica koja se zaokruži na 2^63, 15-cifreni float tekst na FPC-u i test asserti koji prekidaju kompajliranje
Ništa od ovoga ne pokazuje se ako samo gradite i testirate Win32, što je upravo kako su se ubacili. Slučajevi ispod dolaze iz HotPDF SVG i XPS uvoznika, njegovog renderer-a stranica i njegovog čitača JSON poslova, i citirani numerički rezultati reprodukovani su malim probnim programima građenim za Win32 i Win64. Ako premeštate Delphi bazu koda na 64 bita, svaki od njih vredi grep
Zašto se Power(10, 100) prelije samo na Win64?
Na Win64, System.Math.Power(10, N) sa celobrojnim argumentima razrešava se na Single overload, pa se rezultat računa i vraća u jednostrukoj preciznosti i sve iznad otprilike 3.4E38 se prelije. Na Win32 isti poziv vezuje se na Extended overload i radi na x87 FPU sa 80-bitnom preciznošću, pa je Power(10, 100) jednostavno 1E100
System.Math deklariše Power za Extended, Double i Single, plus odgovarajuću IntPower porodicu koju Power zove kad je eksponent ceo broj. Na Win64, Extended je samo alias za Double (SizeOf(Extended) = 8), i za dva celobrojna argumenta kompajler bira Single verziju. Otkrivalac je preciznost, ne samo prelivanje: na Win64, Power(10, 20) vraća 1.0000000200408773E20, što je tačno Single(1E20). Double rezultat ispisao bi se kao 1E20. Videli smo isto vezivanje sa svakim Win64 kompajlerom koji smo probali, od Delphi 10.3 do kompajlerske verzije 37.0
Šta se dalje dešava zavisi od maske izuzetaka pokretnog zareza. Delphi 12 i noviji maskiraju sve izuzetke pokretnog zareza podrazumevano, pa je prelivanje tiho: Power(10, 100) vraća +Inf a Power(10, -100) vraća 0. Delphi 11 i stariji ostavljaju exOverflow nemaskiran, i isti poziv podiže EOverflow. Aplikacije koje same postave masku, i DLL-ovi učitani u takve domaćine, dobijaju ono ponašanje koje je domaćin izabrao, pa biblioteka ne može pretpostaviti nijedan ishod
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 ispisuje 1E20; Win64 ispisuje 1.0000000200408773E20 (Single overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reprodukujte ono što rade Delphi 11 ili domaćin sa strogim FP podešavanjima
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Skidanje maske sa exOverflow i exInvalidOp za vreme testa najjeftiniji je način da vidite šta vide stariji kompajler ili strog domaćin. Na modernom kompajleru sa podrazumevanim podešavanjima bug ne pada, proizvodi beskonačnosti i nule, a one se mnogo teže primete u logu testa. Vratite prethodnu masku u finally: maska je stanje po niti, i ostatak test izvođenja nasleđuje ono što ostavite za sobom
Kako je overload stigao do HotPDF SVG i XPS uvoza
HotPDF SVG i XPS čitači putanja dele jedan skener brojeva, i taj skener skalirao je mantisu sa Power(10, Exponent) jednom kad pročita eksponent. Svaki SVG predat THotPDF.ImportSVGFormXObject (ulazna tačka iza uvoza SVG u PDF kao višekratno upotrebljive form XObject-e), i svaka geometrija putanja obrađena tokom konverzije XPS i OpenXPS u PDF, mogla je zato da dohrani koordinatu poput 1e100 ili 5e99 u taj poziv
v2.770.91 već je ograničavala eksponent na 100 i odbijala vrednosti koje bi prešle 1E300, što je izgledalo dovoljno: 1E100 nimalo nije blizu Double granice od oko 1.8E308. Na Win64 i dalje se prelivalo, jer se računanje nikada dešavalo u Double. Od v2.770.155 skener sam gradi stepen desetice, i brojevi poput 1e-100 ili duga mantisa sa velikim negativnim eksponentom čitaju se kao svoja prava vrednost umesto da se sruše na 0
Bezbedan stepen desetice za ograničene eksponente
Kad je eksponent ograničen, najbezbedniji stepen desetice je onaj koji sami sagradite Double množenjem. Petlja od najviše 100 množenja ne košta ništa uz skeniranje teksta oko nje, nikada ne proizvodi međuvrednost veću od konačne razmere, i ponaša se identično na Win32, Win64 i Free Pascalu
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;
// Odbijte rezultate koji bi napustili Double opseg
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; // nikada ne premašuje 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // delite: 1E-100 nema tačan Double
Result := True;
end;
Tri detalja nose težinu. Provera opsega koristi dva poređenja umesto Abs(Exponent) <= 100, jer je Abs(Low(Integer)) i dalje negativno i prošlo bi pravo kroz. Negativni eksponenti dele razmerom umesto množenja prethodno izračunatim 1E-100, koji nema tačan Double i dodao bi još jedan korak zaokruživanja. I Log10 provera unapred odbija rezultate van Double opsega pre nego što množenje dobije šansu da se prelije
Budite jasni oko onoga što petlja ustupa. Stepeni desetice do 1E22 tačni su u Double-u; iza toga svako množenje zaokružuje, i posle 100 njih razmera sedi nekoliko jedinica na poslednjem mestu od ispravno zaokruženog 1E100. Za koordinate crtanja to je nevidljivo. Za opštu konverziju tekst-u-double koja mora reprodukovati svaku vrednost bit po bit, nije dovoljno dobro, i treba vam ispravno zaokružen algoritam konverzije umesto
Kad dcc64 čita zastareli TList.Count u while petlji
Uočili smo da Win64 kompajler (dcc64, kompajlerska verzija 37.0) generiše kod za while List.Count > Start do petlju koja briše sa kraja liste i poredi protiv stekne privremene umesto da ponovo čita Count. Prepravka koja ga je popravila bila je for ... downto petlja, čije se granice po definiciji izračunavaju tačno jednom
Petlja je stigla u v2.769.3, koja je naučila kod prozirnih grupa renderer-a da drži meke maske stvorene unutar grupe živim kroz dvoprolazni render i oslobodi ih posle. Čišćenje je sedelo u finally bloku posle jedno- ili dvoprolazne for petlje, unutar petlje po pločici. Svedeno na svoj oblik, pre i posle izgleda ovako:
// Oblik koji smo videli pogrešno kompajliran od dcc64 (kompajlerska verzija 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;
// Zamena: granice se izračunavaju jednom, nema privremene koja zastari
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count je NativeInt od Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
U generisanom Win64 kodu, Count u uslovu petlje i Count pročitan unutar tela delili su jedan stekni slot. Uslov je poredio protiv tog slota na ulazu, pre nego što je bilo šta upisano, i ništa ga nije osvežilo posle Delete. Kad grupa nije stvorila sopstvene meke maske, telo je ipak radilo i tražilo je od prazne liste stavku -1, pa je u 64-bitnim verzijama svaka stranica koja sadrži takvu prozirnu grupu padala sa EListError. Win32 kod za isti izvor bio je ispravan, i v2.770.1 zamenila je petlju
Nismo ovo sveli na minimalnu reprodukciju, i mala samostalna petlja poput DropMasksWhile može se sasvim dobro kompajlirati ispravno; okolni try/finally i ugnježdene petlje izgleda su bitne. Tretirajte to kao generisanje koda koje smo uočili na jednoj kompajlerskoj verziji, ne kao poznat defekt svakog Win64 kompajlera. Praktična lekcija je jeftinija od korena: petlja čiji uslov ponovo čita broj zbirke dok telo skuplja tu zbirku vredi prepraviti u for ... downto fiksnih granica, i izmene renderer-a traže potpuno Win64 test izvođenje, ne samo Win32
Nalaženje pada koji pokazuje samo optimizovana Win64 verzija
Kvar se reprodukovao samo u optimizovanoj Win64 verziji, pa je lokacija došla iz alata van IDE-a. Mali probni program registrovao je vectored exception handler sa AddVectoredExceptionHandler, uhvatio stek pri prvom izuzetku sa RtlCaptureStackBackTrace, i preveo povratne adrese u imena funkcija koristeći detaljni map fajl koji linker piše sa -GD. Rastavljanje te funkcije zatim pokazalo je poređenje koje čita stekni slot, [rbp+0x298], koji je pisano samo unutar tela petlje. To je nivo dokaza koji hoćete pre nego što okrivite kompajler, i trajalo je kraće od prelaska kroz release verziju
Zašto High(Int64) nije bezbedna gornja granica za Double?
Double ne može predstaviti High(Int64): pretvaranje 9223372036854775807 u Double zaokružuje se tačno na 2^63, jedan iza najvećeg Int64. Na Win64 to pretvaranje dešava se unutar samog poređenja, pa je D <= High(Int64) True za D = 2^63, i Round ili Trunc koji prati se prelije
Win32 ovo krije iz istog razloga iz kojeg krije Power problem. Poređenje radi u 80-bitnoj Extended preciznosti sa 64-bitnom mantisom, gde je High(Int64) tačan i 2^63 se ispravno poredi kao veće. Win64 nema širi tip na koji može pasti. Van-opseg pretvaranje nije ni lepo: u našim Win64 testovima Round(2^63) vratio je Low(Int64), tihi preokret znaka, bilo da je exInvalidOp bio maskiran ili ne. Win32 vraća istu vrednost kad je maskirano i podiže EInvalidOp kad nije
| Izraz | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), izuzeci maskirani (Delphi 12+ podrazumevano) | 1E100 | +Inf |
Power(10, 100), exOverflow nemaskiran | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp nemaskiran | EInvalidOp | Low(Int64) |
HotPDF je ovo sreo u čitaču JSON-a iza vrednosti poslova njegovih dokumenata. JSON ne stavlja granicu opsega na brojeve, i stari serijalizator pretvarao je svaku vrednost sa Frac(Value) = 0 u ceo broj sa Round, pa je sasvim legalni 1e19 postajao ili pogrešan celobrojni ili izuzetak, u zavisnosti od maske. Od v2.770.169 ceo broj piše se kao celobrojni samo kad staje u Int64, sve ostalo čuva svoj tekst pokretnog zareza, i celobrojni getteri vraćaju podrazumevanu vrednost pozivaoca za van-opseg vrednosti umesto omotane
const
TwoPow63 = 9223372036854775808.0; // 2^63, tačno u Double i Extended
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
// Pozivaoci prvo odbijaju NaN i beskonačnosti: JSON nema pravopis za njih
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral staje na 15 cifara
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Gornja granica je literal 9223372036854775808.0 sa strogim <. Ta konstanta je 2^63, tačna i u Double i u Extended, pa poređenje znači isto na svakoj platformi. Donja granica može koristiti >= jer je -2^63 tačno Low(Int64). Testiranje IsNan i IsInfinite prvo, sa short-circuit vrednovanjem, drži NaN i beskonačnosti van Frac i poređenja, koja mogu podići EInvalidOp kad ih domaćin skine sa maske
Koliko cifara float-u-tekst zaista daje na Win64?
Manje nego što tražite, na dva kompajlera od tri. Free Pascal 3.3.1 FloatToStrF(Value, ffGeneral, 17, 0) na Win64 staje na 15 značajnih cifara, pa se 1/3 vraća kao 0.333333333333333 i dve različite Double vrednosti mogu se serijalizovati u identičan tekst. Str(Value:24, Text) praćen sa Trim proizvodi 17 značajnih cifara u naučnoj notaciji, 3.3333333333333331E-001 za istu vrednost, i uvek piše tačku kao decimalni separator bez obzira na lokal. Ako je HotPDF na FPC-u deo vaše matrice gradnje, HotPDF Free Pascal i Lazarus Win64 napomene podrške pokrivaju ostatak razlika platforme
Delphi prima zahtev od 17 cifara, ali se dva Delphi cilja i dalje ne slažu o izlazu: FloatToStrF(0.1, ffGeneral, 17, 0) daje 0.10000000000000001 na Win32 i 0.1 na Win64. Win64 RTL takođe može uvesti grešku zaokruživanja poslednje cifre i pri formatiranju i pri parsiranju, pa više cifara sužava procep bez garancije da svaki Double bit obrazac preživi tekstualni put tamo-nazad. HotPDF dokumentacija ne daje takvo obećanje, i vaša ne bi smela dok ne isporučite sopstveni ispravno zaokružen formater i parser. Prosledite TFormatSettings.Invariant, ili zamenite separator sami na starijim Delphi verzijama, da nemački ili francuski lokal ne upiše zarez u JSON
Zašto Assert.AreEqual prestaje da se kompajlira na Win64?
Assert.AreEqual(3, Length(Arr)) na dinamičkom nizu kompajlira se za Win32 i pada za Win64 sa E2532, „Couldn't infer generic type argument from different argument types“, jer Length dinamičkog niza vraća NativeInt na Win64. Sa Integer literalom sa jedne strane i 64-bitnim NativeInt sa druge, DUnitX generički Assert.AreEqual<T> ne može se odlučiti za jedno T, i gradnja staje
TList.Count okida istu grešku od Delphi 12, gde je svojstvo postalo NativeInt; Delphi 11 ga i dalje deklariše kao Integer. Length od string vraća Integer na obe platforme i nije pogođen, pa se greška pojavljuje u nekim test jedinicama a u drugima ne. Napišite argument tipa eksplicitno, Assert.AreEqual<NativeInt>(3, Length(Arr)), i kompajlirajte test projekat sa dcc64 pre commit-a. Paket koji se samo gradi za Win32 neće vam reći da mu je Win64 gradnja polomljena dok neko drugi ne proba
Win64 proverna lista za prenos Delphi numeričkog koda
- Tražite
Power(iIntPower(pozive sa celobrojnim argumentima; predajteDouble-tipovane vrednosti ili sami gradite ograničene stepene desetice - Pokrenite numeričke testove bar jednom sa
exOverflowiexInvalidOpuklonjenim krozSetExceptionMask, i na Win32 i na Win64 - Pišite
Int64gornju granicu kao< 9223372036854775808.0, nikada<= High(Int64), i odbijajte NaN i beskonačnosti pre bilo kog poređenja - Ne pretvarajte parsiran broj u
Int64samo zato što jeFrac0; JSON brojevi mogu biti daleko veći - Prepravite
whilepetlje koje ponovo čitajuCountdok brišu stavke ufor ... downtopetlje fiksnih granica - Na FPC Win64, koristite
Str(Value:24, Text)kad treba više od 15 značajnih cifara - Koristite
Assert.AreEqual<NativeInt>zaLengthiCountaserte, i kompajlirajte testove sa dcc64 pre commit-a - Posle svake izmene parsera ili renderer-a, pokrenite kompletan regresioni paket na Win32 i Win64, ne samo na jednom od njih
Popravke na strani biblioteke opisane ovde sve su u HotPDF-u od v2.770.169, pa se SVG uvoz, XPS konverzija, renderovanje prozirnosti i rukovanje JSON poslovima sada ponašaju isto na Win64 kao na Win32. Ako generišete ili obrađujete PDF fajlove iz Delphi-ja ili C++Builder-a za obe platforme, HotPDF Delphi PDF komponenta stranica ima preuzimanja i kompletnu listu funkcija