Win64 Delphi kod može pasti tamo gdje isti izvor teče čisto na Win32, i HotPDF Delphi PDF komponenta pogodila je pet takvih slučajeva tijekom nedavnog prolaza otvrđivanja: Power(10, N) vezan na Single pretjeranje, while petlja koja čita zastarjeli TList.Count, High(Int64) granica koja se zaokruži na 2^63, 15-znamenkasti float tekst na FPC-u i test aserti koji prestanu kompilirati
Nijedno se ne pokazuje ako samo gradite i testirate Win32, što je točno način na koji su se uvukli. Slučajevi dolje dolaze iz HotPDF SVG i XPS uvoznika, njegovog renderera stranica i njegovog JSON čitača poslova, i navedeni numerički rezultati reproducirani su malim probnim programima građenima za Win32 i Win64. Ako selite Delphi codebase na 64 bita, svaki od njih vrijedi jedan grep
Zašto Power(10, 100) overflowa samo na Win64?
Na Win64 se System.Math.Power(10, N) s cjelobrojnim argumentima razrješuje na Single pretjeranje, pa se rezultat računa i vraća u jednostrukoj preciznosti i sve iznad otprilike 3.4E38 overflowa. Na Win32 se isti poziv veže na Extended pretjeranje i izvodi na x87 FPU-u s 80-bitnom preciznošću, pa je Power(10, 100) jednostavno 1E100
System.Math deklarira Power za Extended, Double i Single, plus odgovarajuću IntPower obitelj koju Power poziva kad je eksponent cijeli broj. Na Win64 Extended je samo alias za Double (SizeOf(Extended) = 8), i za dva cjelobrojna argumenta kompajler bira Single verziju. Otkrivenje je preciznost, ne samo overflow: na Win64 Power(10, 20) vraća 1.0000000200408773E20, što je točno Single(1E20). Double rezultat ispisao bi se kao 1E20. Isti smo binding vidjeli sa svakim Win64 kompajlerom koji smo iskušali, od Delphi 10.3 do kompajler verzije 37.0
Što slijedi ovisi o maski iznimki pokretnog zareza. Delphi 12 i kasniji maskiraju sve iznimke pokretnog zareza po zadanim vrijednostima, pa je overflow tih: Power(10, 100) vraća +Inf, a Power(10, -100) vraća 0. Delphi 11 i raniji ostavljaju exOverflow nemaskiranim, i isti poziv diže EOverflow. Aplikacije koje same postave masku, i DLL-ovi učitani u takve domaćine, dobiju ponašanje koje je domaćin odabrao, 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 pretjeranje)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproducirajte što radi Delphi 11, ili domaćin sa strogim FP postavkama
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Demaskiranje exOverflow i exInvalidOp na trajanje testa najjeftiniji je način da vidite što vidi stariji kompajler ili strog domaćin. Na modernom kompajleru sa zadanim postavkama bug se ne ruši, proizvodi beskonačnosti i nule, a one su puno teže uočljive u dnevniku testa. Vratite prethodnu masku u finallyu: maska je stanje po dretvi, i ostatak izvođenja testa nasljeđuje sve što ostavite za sobom
Kako je pretjeranje dospjelo u HotPDF SVG i XPS uvoz
HotPDF SVG i XPS čitači putanja dijele jedan skener brojeva, i taj je skener skalirao mantisu s Power(10, Exponent) jednom kad bi pročitao eksponent. Svaki SVG predan THotPDF.ImportSVGFormXObjectu (ulazna točka iza uvoza SVG-a u PDF kao višekratne form XObjecte), i svaka geometrija putanja obrađena tijekom pretvorbe XPS i OpenXPS u PDF, mogla je stoga utisnuti koordinatu poput 1e100 ili 5e99 u taj poziv
v2.770.91 već je ograničio eksponent na 100 i odbijao vrijednosti koje bi prešle 1E300, što je izgledalo dosta: 1E100 nikud nije blizu Double granice od oko 1.8E308. Na Win64 je i dalje overflowao, jer se račun nikad uopće nije dogodio u Doubleu. Od v2.770.155 skener sam gradi potenciju desetice, pa se brojevi poput 1e-100, ili duga mantisa s velikim negativnim eksponentom, čitaju kao stvarna vrijednost umjesto da se uruše u 0
Sigurna potencija desetice za ograničene eksponente
Kad je eksponent ograničen, najsigurnija potencija desetice jest ona koju sami gradite Double množenjem. Petlja od najviše 100 množenja ne košta ništa naspram skeniranja teksta oko nje, nikad ne proizvodi međuvrijednost veću od konačnog mjerila, 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 raspon
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; // nikad ne prelazi 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // dijeli: 1E-100 nema točan Double
Result := True;
end;
Tri detalja nose težinu. Provjera raspona koristi dvije usporedbe umjesto Abs(Exponent) <= 100, jer je Abs(Low(Integer)) i dalje negativno i prošlo bi ravno kroz. Negativni eksponenti dijele mjerilom umjesto da množe unaprijed izračunatom 1E-100, koja nema točan Double i dodala bi još jedan korak zaokruživanja. I Log10 pretprovjera odbija rezultate izvan Double raspona prije nego množenje dobije priliku overflowati
Budite jasni oko toga čega se petlja odriče. Potencije desetice do 1E22 točne su u Doubleu; iza toga svako množenje zaokružuje, i nakon 100 njih mjerilo stoji par jedinica na posljednjem mjestu od točno zaokružene 1E100. Za crtačke koordinate to je nevidljivo. Za općenamjensku pretvorbu teksta u double koja mora reproducirati svaku vrijednost bit po bit nije dovoljno dobro, i trebate algoritam pretvorbe s točnim zaokruživanjem
Kad dcc64 čita zastarjeli TList.Count u while petlji
Primijetili smo da Win64 kompajler (dcc64, kompajler verzija 37.0) generira kod za petlju while List.Count > Start do koja briše s kraja liste i uspoređuje s privremenom vrijednošću na stogu umjesto da ponovno pročita Count. Prepravka koja je to popravila bila je petlja for ... downto, čije se granice po definiciji vrednuju točno jednom
Petlja je stigla u v2.769.3, koji je naučio kod transparency grupe renderera da drži soft maske stvorene unutar grupe živima kroz dvoprolazni render i oslobodi ih poslije. Čišćenje je sjedilo u finally bloku iza jedno- ili dvoprolazne for petlje, unutar petlje po pločici. Svedeno na oblik, prije i poslije izgleda ovako:
// Oblik koji smo vidjeli pogrešno kompiliran od dcc64 (kompajler 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;
// Zamjena: granice se vrednuju jednom, nema privremene vrijednosti koja zastarjeva
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 generiranom Win64 kodu Count u uvjetu petlje i Count pročitan unutar tijela dijelili su jedan stog slot. Uvjet se uspoređivao s tim slotom pri ulasku, prije nego je išta u njega zapisalo, i ništa ga nije osvježilo nakon Delete. Kad grupa nije stvorila nijednu vlastitu soft masku, tijelo je ipak izvedeno i zatražilo je od prazne liste stavku -1, pa je u 64-bitnim verzijama svaka stranica koja sadrži takvu transparency grupu padala s EListError. Win32 kod za isti izvor bio je ispravan, i v2.770.1 zamijenio je petlju
Nismo ovo sveli na minimalnu reprodukciju, i mala samostalna petlja poput DropMasksWhile sasvim može kompilirati ispravno; okolni try/finally i ugniježđene petlje čine se bitnima. Tretirajte to kao generiranje koda koje smo promatrali na jednoj verziji kompajlera, a ne kao poznati defekt svakog Win64 kompajlera. Praktična lekcija jeftinija je od korijena: petlja čiji uvjet ponovno čita broj zbirke dok tijelo skuplja tu zbirku vrijedi prepraviti u for ... downto s fiksnim granicama, i promjene renderera trebaju potpuno Win64 izvođenje testova, ne samo Win32
Lociranje rušenja koje pokazuje samo optimizirana Win64 verzija
Kvar se reproducirao samo u optimiziranoj Win64 verziji, pa je lokacija došla iz alata izvan IDE-a. Mali probni program registrirao je vectored exception handler s AddVectoredExceptionHandler, zabilježio stog pri prvoj iznimci s RtlCaptureStackBackTrace, i preveo povratne adrese u imena funkcija koristeći detaljnu map datoteku koju linker piše s -GD. Rastavljanje te funkcije pokazalo je zatim usporedbu koja čita stog slot, [rbp+0x298], koji je bio zapisan samo unutar tijela petlje. To je razina dokaza koju želite prije nego optužite kompajler, i trebalo je manje vremena nego korakati kroz release verziju
Zašto High(Int64) nije sigurna gornja granica za Double?
Double ne može predstaviti High(Int64): pretvorba 9223372036854775807 u Double zaokružuje gore točno na 2^63, jedno iza najvećeg Int64a. Na Win64 ta se pretvorba događa unutar same usporedbe, pa je D <= High(Int64) True za D = 2^63, i Round ili Trunc koji slijedi overflowa
Win32 ovo krije iz istog razloga iz kojeg je kriao Power problem. Usporedba izvodi u 80-bitnoj Extended preciznosti s 64-bitnom mantisom, gdje je High(Int64) točan i 2^63 se ispravno uspoređuje većim. Win64 nema širi tip na koji može pasti. Pretvorba izvan raspona također nije lijepa: u našim Win64 testovima Round(2^63) vraćao je Low(Int64), tihu promjenu predznaka, bio exInvalidOp maskiran ili ne. Win32 vraća istu vrijednost kad je maskiran i diže EInvalidOp kad nije
| Izraz | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), iznimke maskirane (Delphi 12+ zadano) | 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 JSON čitaču iza vrijednosti poslova dokumenata. JSON ne stavlja granicu raspona na brojeve, i stari je serijalizator svaku vrijednost s Frac(Value) = 0 pretvarao u cijeli broj s Roundom, pa je savršeno legalni 1e19 postao ili pogrešan cijeli broj ili iznimka, ovisno o maski. Od v2.770.169 cijeli se broj zapisuje kao integer samo kad stane u Int64, sve ostalo zadržava svoj tekst pokretnog zareza, i cijelobrojni getteri vraćaju zadanu vrijednost pozivatelja za vrijednosti izvan raspona umjesto omotane
const
TwoPow63 = 9223372036854775808.0; // 2^63, toč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
// Pozivatelji prvo odbijaju NaN i beskonačnosti: JSON nema zapisa za njih
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral staje na 15 znamenki
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Gornja granica literal je 9223372036854775808.0 sa strogim <. Ta je konstanta 2^63, točna i u Doubleu i u Extendedu, pa usporedba znači isto na svakoj platformi. Donja granica može koristiti >= jer je -2^63 točno Low(Int64). Ispitivanje IsNan i IsInfinite prvo, s kratkovalnim vrednovanjem, drži NaN i beskonačnosti dalje od Frac i usporedbi, koje mogu dignuti EInvalidOp kad ga je domaćin demaskirao
Koliko znamenki vam float-u-tekst stvarno 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 znamenki, pa se 1/3 vraća kao 0.333333333333333 i dvije različite Double vrijednosti mogu se serijalizirati u identičan tekst. Str(Value:24, Text) s Trimom proizvodi 17 značajnih znamenki u znanstvenom zapisu, 3.3333333333333331E-001 za istu vrijednost, i uvijek piše točku kao decimalni separator bez obzira na locale. Ako je HotPDF na FPC-u dio vaše matrice gradnje, HotPDF Free Pascal i Lazarus Win64 napomene o podršci pokrivaju ostatak razlika platformi
Delphi prihvaća zahtjev od 17 znamenki, ali se dva Delphi cilja i dalje ne poklapaju u izlazu: FloatToStrF(0.1, ffGeneral, 17, 0) daje 0.10000000000000001 na Win32 i 0.1 na Win64. Win64 RTL može uvesti i pogrešku zaokruživanja zadnje znamenke i pri formatiranju i pri parsiranju, pa više znamenki sužava jaz bez garancije da će svaki Double bitni obrazac preživjeti putanju teksta tamo i natrag. HotPDF dokumentacija ne daje takvo obećanje, i vaša ne bi smjela osim ako isporučujete vlastiti formater i parser s točnim zaokruživanjem. Prenosite TFormatSettings.Invariant, ili sami zamijenite separator na starijim Delphi verzijama, da njemački ili francuski locale ne zapiše zarez u JSON
Zašto Assert.AreEqual prestaje kompilirati na Win64?
Assert.AreEqual(3, Length(Arr)) na dinamičkom polju kompilira se za Win32 i pada za Win64 s E2532, „Couldn't infer generic type argument from different argument types", jer Length dinamičkog polja vraća NativeInt na Win64. S Integer literalom na jednoj strani i 64-bitnim NativeIntom na drugoj, DUnitX generički Assert.AreEqual<T> ne može se odlučiti za jedan T, i build staje
TList.Count okida istu pogrešku od Delphi 12, gdje je svojstvo postalo NativeInt; Delphi 11 još ga deklarira kao Integer. Length od string vraća Integer na obje platforme i nije zahvaćen, pa se pogreška pojavljuje u nekim testnim jedinicama, a u drugima ne. Zapišite argument tipa eksplicitno, Assert.AreEqual<NativeInt>(3, Length(Arr)), i kompilirajte testni projekt s dcc64 prije predaje. Paket koji se ikad gradi samo za Win32 neće vam reći da mu je Win64 build slomljen dok to netko drugi ne iskuša
Win64 kontrolna lista preseljenja za Delphi numerički kod
- Tražite pozive
Power(iIntPower(s cjelobrojnim argumentima; prenositeDoubletipizirane vrijednosti ili sami gradite ograničene potencije desetice - Izvedite numeričke testove barem jednom s
exOverflowiexInvalidOpuklonjenima prekoSetExceptionMask, na Win32 i Win64 - Pišite
Int64gornju granicu kao< 9223372036854775808.0, nikad<= High(Int64), i odbijajte NaN i beskonačnosti prije bilo koje usporedbe - Ne pretvarajte parsirani broj u
Int64samo zato što jeFrac0; JSON brojevi mogu biti daleko veći - Prepravite
whilepetlje koje ponovno čitajuCountdok brišu stavke ufor ... downtopetlje s fiksnim granicama - Na FPC Win64 koristite
Str(Value:24, Text)kad trebate više od 15 značajnih znamenki - Koristite
Assert.AreEqual<NativeInt>zaLengthiCountaserte, i kompilirajte testove s dcc64 prije predaje - Nakon svake izmjene parsera ili renderera pokrenite potpuni regresijski paket na Win32 i Win64, ne samo na jednom od njih
Popravci na strani biblioteke opisani ovdje svi su u HotPDF-u od v2.770.169, pa se SVG uvoz, XPS pretvorba, transparency renderiranje i rukovanje JSON poslovima sada ponašaju isto na Win64 kao na Win32. Ako generirate ili obrađujete PDF datoteke iz Delphija ili C++Buildera za obje platforme, stranica HotPDF Delphi PDF komponente ima preuzimanja i potpuni popis značajki