Kód Delphi pre Win64 môže zlyhať tam, kde ten istý zdroj beží čisto na Win32, a komponent HotPDF Delphi PDF narazil počas nedávneho posilňovacieho prechodu na päť takýchto prípadov: Power(10, N) viažuci sa na overload Single, slučka while čítajúca zastaralé TList.Count, hranica High(Int64) zaokrúhľujúca sa nahor na 2^63, 15-ciferný float text na FPC a testové asserty, ktoré prestanú kompilovať
Žiadne z nich sa neukáže, ak staviate a testujete len Win32, presne takto sa dovnútra pretlačili. Príklady nižšie pochádzajú z importérov SVG a XPS v HotPDF, z jeho page rendereru a z jeho JSON čítačky zakázok a citované numerické výsledky sa reprodukovali malými probe programami zostavenými pre Win32 a Win64. Ak presúvate Delphi codebase na 64 bitov, každý z nich stojí za jeden grep
Prečo Power(10, 100) pretečie len na Win64?
Na Win64 sa System.Math.Power(10, N) s celočíselnými argumentmi vyrieši na overload Single, takže sa výsledok počíta a vracia v jednoduchej presnosti a čokoľvek nad zhruba 3.4E38 pretečie. Na Win32 sa to isté volanie viaže na overload Extended a beží na x87 FPU s 80-bitovou presnosťou, takže Power(10, 100) je jednoducho 1E100
System.Math deklaruje Power pre Extended, Double a Single plus zodpovedajúcu rodinu IntPower, ktorú Power volá, keď je exponent celé číslo. Na Win64 je Extended len alias pre Double (SizeOf(Extended) = 8) a pre dva celočíselné argumenty vyberie kompilátor verziu Single. Prozradí to presnosť, nie len pretečenie: na Win64 vráti Power(10, 20) 1.0000000200408773E20, čo je presne Single(1E20). Výsledok Double by sa vypísal ako 1E20. Ten istý binding sme videli pri každom Win64 kompilátore, ktorý sme skúšli, od Delphi 10.3 po verziu kompilátora 37.0
Čo sa stane ďalej, závisí od maske výnimiek s plávajúcou bodkou. Delphi 12 a novšie maskujú všetky výnimky s plávajúcou bodkou predvolene, takže pretečenie je tiché: Power(10, 100) vráti +Inf a Power(10, -100) vráti 0. Delphi 11 a staršie nechávajú exOverflow nemaskované a to isté volanie vyhodí EOverflow. Aplikácie, ktoré si masku nastavia samy, a DLL načítané do takýchto hostiteľov dostanú to správanie, ktoré si hostiteľ zvolil, prečo knižnica nesmie predpokladať ani jeden výsledok
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 vypíše 1E20; Win64 vypíše 1.0000000200408773E20 (overload Single)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reprodukujte, čo robí Delphi 11 alebo hostiteľ s prísnymi FP nastaveniami
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Odmaskovanie exOverflow a exInvalidOp na dobu testu je najlacnejší spôsob, ako vidieť, čo vidí starší kompilátor alebo prísny hostiteľ. Na modernom kompilátore s predvolenými nastaveniami bug nespadne, vyrobí nekonečná a nuly a tie sa v testovacom logu hľadajú oveľa ťažšie. Predchádzajúcu masku obnovujte v finally: maska je stav na vlákno a zvyšok testovacieho behu zdedí to, čo po sebe necháte
Ako sa overload dostal do importu SVG a XPS v HotPDF
Čítačky ciest SVG a XPS v HotPDF zdieľajú jeden numerický skener a ten skener škáloval mantisu cez Power(10, Exponent), akonáhle prečítal exponent. Akékoľvek SVG podané do THotPDF.ImportSVGFormXObject (vstupného bodu za importom SVG do PDF ako znovu použiteľných form XObjectov) a akákoľvek geometria ciest obslúžená počas konverzie XPS a OpenXPS do PDF mu tak mohla podstrčiť súradnicu ako 1e100 alebo 5e99
v2.770.91 už kapovala exponent na 100 a odmietala hodnoty, ktoré by prešli 1E300, čo vyzeralo dosť: 1E100 je ďaleko od limitu Double zhruba 1.8E308. Na Win64 to aj tak preteklo, lebo výpočet sa nikdy neudial v Double. Od v2.770.155 si skener mocninu desiatky stavia sám a čísla ako 1e-100 alebo dlhá mantisa s veľkým záporným exponentom sa čítajú ako ich skutočná hodnota namiesto zvinutia na 0
Bezpečná mocnina desiatky pre ohraničené exponenty
Keď je exponent ohraničený, najbezpečnejšia mocnina desiatky je taká, ktorú si postavíte sami násobením v Double. Slučka najviac 100 násobení nič nestojí vedľa skenovania textu okolo nej, nikdy nevyrobí medzivýsledok väčší ako finálna mierka a správa sa identicky na Win32, Win64 aj 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;
// Odmietnite výsledky, ktoré by opustili rozsah Double
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; // nikdy nepresiahne 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // delenie: 1E-100 nemá presné Double
Result := True;
end;
Váh nesú tri detaily. Kontrola rozsahu používa dve porovnania namiesto Abs(Exponent) <= 100, lebo Abs(Low(Integer)) je stále záporné a prekĺzala by priamo cez. Záporné exponenty delia mierkou namiesto násobenia predpočítanou 1E-100, ktorá nemá presné Double a pridala by ďalší krok zaokrúhľovania. A predkontrola Log10 odmietne výsledky mimo rozsahu Double skôr, než má násobenie šancu preteiec
Buďte jednoznační v tom, čo tá slučka obetuje. Mocniny desiatky do 1E22 sú v Double presné; za tým každé násobenie zaokrúhľuje a po stoch z nich sedí mierka o pár jednotiek posledného miesta od korektne zaokrúhlenej 1E100. Pre kresliace súradnice je to neviditeľné. Pre všeobecnú konverziu textu na double, ktorá musí reprodukovať každú hodnotu bit za bitom, to nestačí a potrebujete korektne zaokrúhľujúci konverzný algoritmus
Kedy dcc64 číta zastaralé TList.Count vo while slučke
Pozorovali sme, ako Win64 kompilátor (dcc64, verzia kompilátora 37.0) vygeneroval kód pre slučku while List.Count > Start do, ktorá mazala od konca zoznamu a porovnávala proti stackovej dočasnej hodnote namiesto opätovného čítania Count. Prepis, ktorý to opravil, bola slučka for ... downto, ktorej hranice sa z definície vyhodnocujú presne raz
Slučka prišla vo v2.769.3, ktorá naučila kód transparency skupín rendereru držať soft masky vytvorené vo vnútri skupiny nažive cez dvojprúbový render a uvoľni ich potom. Upratovanie sedelo v bloku finally za jednoprúbovou alebo dvojprúbovou slučkou for, vo vnútri slučky na dlaždicu. Zredukované na tvar, pred a po vyzerajú takto:
// Tvar, ktorý sme videli zle skompilovaný dcc64 (verzia kompilátora 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;
// Náhrada: hranice sa vyhodnotia raz, žiadna dočasná hodnota nestarne
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;
Vo vygenerovanom Win64 kóde zdieľali Count v podmienke slučky a Count čítané vo vnútri tela jeden stack slot. Podmienka porovnávala proti tomu slotu pri vstupe, skôr než čokoľvek zapísalo, a nič ho po Delete neobnovilo. Keď skupina nevytvorila žiadne vlastné soft masky, telo sa aj tak spustilo a poprosilo prázdny zoznam o položku -1, takže v 64-bitových zostaveniach zlyhala každá strana obsahujúca takúto transparency skupinu s EListError. Win32 kód pre ten istý zdroj bol korektný a v2.770.1 slučku vymenila
Nezredukovali sme to na minimálnu reprodukciu a malá samostatná slučka ako DropMasksWhile sa môže pokojne kompilovať korektne; okolo ležiace try/finally a vnorené slučky zjavne hrajú rolu. Berte to ako generovanie kódu pozorované na jednej verzii kompilátora, nie ako známy defekt každého Win64 kompilátora. Praktická lekcia je lacnejšia než koreňová príčina: slučka, ktorej podmienka znovu číta počet kolekcie, kým telo tú kolekciu zmenšuje, stojí za prepis na for ... downto s fixnými hranicami a zmeny rendereru potrebujú celý Win64 testovací beh, nielen Win32
Lokalizácia pádu, ktorý ukazuje len optimalizované Win64 zostavenie
Zlyhanie sa reprodukovalo len v optimalizovanom Win64 zostavení, takže lokalizácia pochádzala z nástrojov mimo IDE. Malý probe program zaregistroval vectored exception handler cez AddVectoredExceptionHandler, zachytil stack pri prvej výnimke cez RtlCaptureStackBackTrace a preložil návratové adresy na mená funkcií cez detailný map súbor, ktorý linker zapisuje s -GD. Rozanalýza tej funkcie potom ukázala porovnávanie čítajúce stack slot [rbp+0x298], ktorý sa zapisoval len vo vnútri tela slučky. To je úroveň dôkazov, ktorú chcete, skôr než obviníte kompilátor, a trvalo to menej času než krokovanie release zostavením
Prečo High(Int64) nie je bezpečná horná hranica pre Double?
Double nedokáže zastúpiť High(Int64): konverzia 9223372036854775807 na Double sa zaokrúhli nahor presne na 2^63, jednu za najväčším Int64. Na Win64 sa tá konverzia deje vo vnútri samotného porovnania, takže D <= High(Int64) je True pre D = 2^63 a Round alebo Trunc, ktoré nasledujú, pretečú
Win32 to skrýva z rovnakého dôvodu, pre aký skrýval problém Power. Porovnávanie beží v 80-bitovej presnosti Extended s 64-bitovou mantisou, kde je High(Int64) presné a 2^63 sa korektne porovnáva väčšie. Win64 nemá širší typ, na ktorý by mohol spadnúť. Konverzia mimo rozsahu nie je pekná ani tam: v našich Win64 testoch vrátil Round(2^63) Low(Int64), tiché preklopenie znamienka, bez ohľadu na to, či bolo exInvalidOp maskované. Win32 vracia tú istú hodnotu pri maske a vyhodí EInvalidOp pri nemaskovanom
| Výraz | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), výnimky maskované (predvolené Delphi 12+) | 1E100 | +Inf |
Power(10, 100), exOverflow nemaskované | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp nemaskované | EInvalidOp | Low(Int64) |
HotPDF sa s tým stretol v JSON čítačke za hodnotami dokumentových zakázok. JSON nekladie na čísla žiadny limit rozsahu a starý serializátor menil akúkoľvek hodnotu s Frac(Value) = 0 na celé číslo cez Round, takže úplne legálne 1e19 sa stalo buď nesprávnym celým číslom, alebo výnimkou, podľa masky. Od v2.770.169 sa celé číslo zapisuje ako celočíselné, len keď sa zmestí do Int64, všetko ostatné si drží svoj text s plávajúcou bodkou a celočíselné gettery vracajú predvolenú hodnotu volajúceho pre hodnoty mimo rozsahu namiesto pretočenej
const
TwoPow63 = 9223372036854775808.0; // 2^63, presné v Double aj 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
// Volajúci najprv odmietnu NaN a nekonečná: JSON nemá pre ne žiadny zápis
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral skončí na 15 čísliciach
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Horná hranica je literál 9223372036854775808.0 s prísnym <. Tá konštanta je 2^63, presná v Double aj Extended, takže porovnanie znamená to isté na každej platforme. Spodná hranica môže použiť >=, lebo -2^63 je presne Low(Int64). Testovanie IsNan a IsInfinite najprv, so skratovým vyhodnotením, drží NaN a nekonečná ďaleko od Frac a porovnaní, ktoré dokážu vyhodiť EInvalidOp, keď si ich hostiteľ odmaskoval
Koľko číslic vám konverzia floatu na text naozaj dá na Win64?
Menej, než si pýtate, pri dvoch kompilátoroch z troch. FloatToStrF(Value, ffGeneral, 17, 0) Free Pascalu 3.3.1 na Win64 skončí na 15 významných čísliciach, takže 1/3 sa vráti ako 0.333333333333333 a dve odlišné hodnoty Double sa dokážu serializovať do identického textu. Str(Value:24, Text) nasledované Trim vyrobí 17 významných číslic vo vedeckej notácii, 3.3333333333333331E-001 pre tú istú hodnotu, a vždy zapisuje bodku ako desatinný oddeľovač bez ohľadu na locale. Ak je HotPDF na FPC súčasťou vašej build matice, poznámky o podpore HotPDF Free Pascal a Lazarus Win64 pokrývajú zvyšok rozdielov platformy
Delphi prijme požiadavku 17 číslic, ale oba Delphi ciele sa aj naďalej rozchádzajú vo výstupe: FloatToStrF(0.1, ffGeneral, 17, 0) dáva 0.10000000000000001 na Win32 a 0.1 na Win64. RTL Win64 dokáže navyše vniesť chybu zaokrúhlenia poslednej číslice pri formátovaní aj pri parsovaní, takže viac číslic zúži medzeru bez záruky, že každý bitový vzor Double prežije okružnú cestu textom. Dokumentácia HotPDF taký sľub nedáva a váš ani nemá, pokiaľ nedodávate vlastný korektne zaokrúhľujúci formátovač aj parser. Podávajte TFormatSettings.Invariant alebo si oddeľovač vymeňte sami na starších verziách Delphi, aby nemecké alebo francúzske nezapisovalo do JSON čiarku
Prečo Assert.AreEqual prestane kompilovať na Win64?
Assert.AreEqual(3, Length(Arr)) nad dynamickým poľom sa skompiluje pre Win32 a pre Win64 zlyhá s E2532, „Couldn't infer generic type argument from different argument types“, lebo Length dynamického poľa vracia na Win64 NativeInt. S literálom Integer na jednej strane a 64-bitovým NativeInt na druhej si DUnitX generické Assert.AreEqual<T> nevie vybrať jediné T a zostavenie sa zastaví
TList.Count spúšťa tú istú chybu od Delphi 12, kde sa property stala NativeInt; Delphi 11 ju stále deklaruje ako Integer. Length nad string vracia Integer na oboch platformách a nie je postihnutá, prečo sa chyba ukáže v niektorých testových jednotkách a v iných nie. Píšte typový argument explicitne, Assert.AreEqual<NativeInt>(3, Length(Arr)) a pred commitom skompilujte testový projekt s dcc64. Sada, ktorá sa stavia len pre Win32, vám nepovie, že jej Win64 zostavenie je pokazené, kým to neskúsi niekto iný
Checklist portovania Win64 pre numerický kód Delphi
- Hľadajte volania
Power(aIntPower(s celočíselnými argumentmi; podávajte hodnoty typovanéDoublealebo si ohraničené mocniny desiatky stavajte sami - Púšťajte numerické testy aspoň raz s odstránenými
exOverflowaexInvalidOpcezSetExceptionMaskna Win32 aj Win64 - Hornú hranicu
Int64píšte ako< 9223372036854775808.0, nikdy<= High(Int64)a odmietajte NaN a nekonečná skôr než akékoľvek porovnanie - Nekonvertujte parsované číslo na
Int64len preto, žeFracje 0; JSON čísla môžu byť oveľa väčšie - Prepíšte
whileslučky, ktoré znovu čítajúCount, zatiaľ čo mažú položky, na slučkyfor ... downtos fixnými hranicami - Na FPC Win64 používajte
Str(Value:24, Text), keď potrebujete viac než 15 významných číslic - Používajte
Assert.AreEqual<NativeInt>pre assertyLengthaCounta pred commitom skompilujte testy s dcc64 - Po každej zmene parsera alebo rendereru pustite celú regresnú sadu na Win32 aj Win64, nielen na jednom z nich
Opravy na strane knižnice popísané tu sú všetky v HotPDF od v2.770.169, takže import SVG, konverzia XPS, renderovanie transparency aj obsluha JSON zakázok sa teraz správajú na Win64 rovnako ako na Win32. Ak generujete alebo spracovávate PDF súbory z Delphi alebo C++Builder pre obe platformy, stránka HotPDF Delphi PDF component má stiahnutia a kompletný zoznam funkcií