Win64 Delphi-kode kan feile der samme kilde kjører rent på Win32, og HotPDF Delphi PDF-komponenten traff fem slike tilfeller under en nylig herdingsrunde: Power(10, N) bundet til Single-overloaden, en while-løkke som leser en foreldet TList.Count, en High(Int64)-grense som runder opp til 2^63, 15-sifret float-tekst på FPC, og test-asserts som slutter å kompilere
Ingen av delene dukker opp hvis du bare bygger og tester Win32, noe som er nøyaktig hvordan de snek seg inn. Tilfellene nedenfor kommer fra HotPDFs SVG- og XPS-importører, dens siderenderer og dens JSON-jobb-leser, og de siterte numeriske resultatene ble reprodusert med små probe-programmer bygget for Win32 og Win64. Flytter du en Delphi-kodebase til 64-bit, er hver enkelt verdt en grep
Hvorfor overflyter Power(10, 100) bare på Win64?
På Win64 løser System.Math.Power(10, N) med heltallsargumenter til Single-overloaden, så resultatet beregnes og returneres i single precision, og alt over omtrent 3.4E38 overflyter. På Win32 binder samme kall til Extended-overloaden og kjører på x87 FPU med 80-bits presisjon, så Power(10, 100) er rett og slett 1E100
System.Math deklarerer Power for Extended, Double og Single, pluss en matchende IntPower-familie som Power kaller når eksponenten er et helt tall. På Win64 er Extended bare et alias for Double (SizeOf(Extended) = 8), og for to heltallsargumenter velger kompilatoren Single-versjonen. Avsløringen er presisjon, ikke bare overflyten: på Win64 returnerer Power(10, 20) 1.0000000200408773E20, som er nøyaktig Single(1E20). Et Double-resultat ville skrevet som 1E20. Vi så samme binding med hver Win64-kompilator vi prøvde, fra Delphi 10.3 gjennom kompilatorversjon 37.0
Hva som skjer videre, avhenger av flyttalls-unntaksmasken. Delphi 12 og senere masker alle flyttallsunntak som standard, så overflyten er stille: Power(10, 100) returnerer +Inf og Power(10, -100) returnerer 0. Delphi 11 og tidligere lar exOverflow være umaskert, og samme kall reiser EOverflow. Applikasjoner som setter masken selv, og DLL-er lastet inn i slike verter, får den atferden verten valgte, noe som er grunnen til at et bibliotek ikke kan anta noe av utfallene
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 skriver 1E20; Win64 skriver 1.0000000200408773E20 (Single-overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproduser det Delphi 11, eller en host med strenge FP-innstillinger, gjør
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Å unmaskere exOverflow og exInvalidOp i varigheten av en test er den billigste måten å se hva en eldre kompilator eller en streng vert ser. På en moderne kompilator med standardinnstillinger krasjer ikke bugen, den produserer uendeligheter og nuller, og de er mye vanskeligere å få øye på i en testlogg. Gjenopprett forrige maske i finally: masken er per-tråd-tilstand, og resten av testkjøringen arver det du etterlater
Hvordan overloaden nådde HotPDF SVG- og XPS-import
HotPDFs SVG- og XPS-sti-lesere deler én tallskanner, og den skanneren skalerte mantissen med Power(10, Exponent) så snart den hadde lest en eksponent. Enhver SVG sendt til THotPDF.ImportSVGFormXObject (inngangspunktet bak å importere SVG inn i PDF som gjenbrukbare form XObjects), og enhver stigeometri håndtert under XPS- og OpenXPS-til-PDF-konvertering, kunne derfor mate en koordinat som 1e100 eller 5e99 inn i det kallet
v2.770.91 hadde allerede takset eksponenten til 100 og avvist verdier som ville passere 1E300, noe som så tilstrekkelig ut: 1E100 er ingensteds nær Double-grensen på omtrent 1.8E308. På Win64 overflytet det fortsatt, for beregningen skjedde aldri i Double i det hele tatt. Siden v2.770.155 bygger skanneren tierpotensen selv, og tall som 1e-100, eller en lang mantisse med en stor negativ eksponent, leses som sin virkelige verdi i stedet for å kollapse til 0
En trygg tierpotens for begrensede eksponenter
Når eksponenten er begrenset, er den tryggste tierpotensen én du bygger selv med Double-multiplikasjon. En løkke på høyst 100 multiplikasjoner koster ingenting ved siden av å skanne teksten rundt den, den produserer aldri et mellomprodukt større enn den endelige skalaen, og den oppfører seg identisk på Win32, Win64 og 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;
// Avvis resultater som ville forlate Double-området
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; // overstiger aldri 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // divider: 1E-100 har ingen eksakt Double
Result := True;
end;
Tre detaljer bærer vekten. Områdekontrollen bruker to sammenligninger i stedet for Abs(Exponent) <= 100, for Abs(Low(Integer)) er fortsatt negativ og ville seilet rett gjennom. Negative eksponenter dividerer med skalaen i stedet for å multiplisere med en forhåndsberegnet 1E-100, som ikke har noen eksakt Double og ville lagt til ett avrundingssteg til. Og Log10-forhåndssjekken avviser resultater utenfor Double-området før multiplikasjonen har en sjanse til å overflyte
Vær klar over hva løkken gir opp. Tierpotenser opp til 1E22 er eksakte i Double; forbi det runder hver multiplikasjon, og etter 100 av dem ligger skalaen noen enheter på sisteplassen unna den korrekt avrundede 1E100. For tegnekoordinater er det usynlig. For en generell tekst-til-double-konvertering som må reprodusere hver verdi bit for bit, er det ikke godt nok, og du trenger en korrekt avrundet konverteringsalgoritme i stedet
Når dcc64 leser en foreldet TList.Count i en while-løkke
Vi observerte at Win64-kompilatoren (dcc64, kompilatorversjon 37.0) genererte kode for en while List.Count > Start do-løkke som slettet fra slutten av listen og sammenlignet mot en stakk-midlertidig i stedet for å lese Count på nytt. Omskrivingen som fikset det, var en for ... downto-løkke, hvis grenser evalueres nøyaktig én gang per definisjon
Løkken ankom i v2.769.3, som lærte rendererens transparency group-kode å holde myke masker opprettet inne i en gruppe i live på tvers av en topass-rendering og frigjøre dem etterpå. Oppryddingen lå i en finally-blokk etter en én- eller topass for-løkke, inne i per-flis-løkken. Redusert til sin form, ser før og etter slik ut:
// Formen vi så feilkompilert av dcc64 (kompilatorversjon 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;
// Erstatning: grensene evalueres én gang, ingen midlertidig som blir foreldet
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count er NativeInt siden Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
I den genererte Win64-koden delte Count i løkkebetingelsen og Count-lesingen inne i kroppen én stakk-plass. Betingelsen sammenlignet mot den plassen ved inngang, før noe hadde skrevet den, og ingenting oppfrisket den etter Delete. Når en gruppe ikke hadde opprettet egne myke masker, kjørte kroppen likevel og ba en tom liste om element -1, så i 64-bits bygg feilet hver side som inneholdt en slik transparency group med EListError. Win32-koden for samme kilde var korrekt, og v2.770.1 erstattet løkken
Vi har ikke redusert dette til en minimal reprodusering, og en liten frittstående løkke som DropMasksWhile kan godt kompilere korrekt; den omgivende try/finally og nestede løkker ser ut til å bety noe. Behandle det som kodegenerering vi observerte på én kompilatorversjon, ikke som en kjent defekt hos hver Win64-kompilator. Den praktiske lærdommen er billigere enn rotårsaken: en løkke hvis betingelse leser en samlings antall på nytt mens kroppen krymper den samlingen, er verdt å omskrive som en fastbegrenset for ... downto, og renderer-endringer trenger en full Win64-testkjøring, ikke bare Win32
Lokalisere et krasj som bare en optimalisert Win64-build viser
Feilen reproduserte bare i det optimaliserte Win64-bygget, så lokasjonen kom fra verktøy utenfor IDE-en. Et lite probe-program registrerte en vectored exception handler med AddVectoredExceptionHandler, fanget stakken ved det første unntaket med RtlCaptureStackBackTrace, og oversatte returadressene til funksjonsnavn ved bruk av den detaljerte map-filen linkeren skriver med -GD. Å dissekere den funksjonen viste så sammenligningen lese en stakk-plass, [rbp+0x298], som bare noensinne ble skrevet inne i løkkelegemet. Det er bevisnivået du vil ha før du klandrer en kompilator, og det tok mindre tid enn å stegge gjennom et release-bygg
Hvorfor er High(Int64) ikke en trygg øvre grense for en Double?
En Double kan ikke representere High(Int64): å konvertere 9223372036854775807 til Double runder opp til nøyaktig 2^63, én forbi den største Int64. På Win64 skjer den konverteringen inne i sammenligningen selv, så D <= High(Int64) er True for D = 2^63, og Round eller Trunc som følger, overflyter
Win32 skjuler dette av samme grunn som det skjulte Power-problemet. Sammenligningen kjører i 80-bits Extended-presisjon med en 64-bits mantisse, der High(Int64) er eksakt og 2^63 korrekt sammenlignes større. Win64 har ingen bredere type å falle tilbake på. Utenfor-området-konverteringen er ikke pen heller: i våre Win64-tester returnerte Round(2^63) Low(Int64), et stille fortegnsbytte, uansett om exInvalidOp var maskert eller ikke. Win32 returnerer samme verdi når maskert og reiser EInvalidOp når umaskert
| Uttrykk | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), unntak maskert (Delphi 12+-standard) | 1E100 | +Inf |
Power(10, 100), exOverflow umaskert | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp umaskert | EInvalidOp | Low(Int64) |
HotPDF traff dette i JSON-leseren bak sine dokumentjob-verdier. JSON setter ingen områdegrense på tall, og den gamle serialisatoren gjorde enhver verdi med Frac(Value) = 0 om til et heltall med Round, så et fullt lovlig 1e19 ble enten et feil heltall eller et unntak, avhengig av masken. Siden v2.770.169 skrives et heltall som et heltall bare når det passer i Int64, alt annet beholder sin flyttallstekst, og heltallsgetterne returnerer kallerens standardverdi for utenfor-området-verdier i stedet for en verdi som har rundet over
const
TwoPow63 = 9223372036854775808.0; // 2^63, eksakt i Double og 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
// Kallere avviser NaN og uendeligheter først: JSON har ingen skrivemåte for dem
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral stopper ved 15 sifre
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Den øvre grensen er literalen 9223372036854775808.0 med en streng <. Den konstanten er 2^63, eksakt i både Double og Extended, så sammenligningen betyr det samme på enhver plattform. Den nedre grensen kan bruke >= for -2^63 er nøyaktig Low(Int64). Å teste IsNan og IsInfinite først, med kortslutningsevaluering, holder NaN og uendeligheter unna Frac og sammenligningene, som kan reise EInvalidOp når verten har umaskert den
Hvor mange sifre gir float-til-tekst virkelig deg på Win64?
Færre enn du ber om, på to av tre kompilatorer. Free Pascal 3.3.1s FloatToStrF(Value, ffGeneral, 17, 0) på Win64 stopper ved 15 signifikante sifre, så 1/3 kommer tilbake som 0.333333333333333 og to ulike Double-verdier kan serialiseres til identisk tekst. Str(Value:24, Text) etterfulgt av Trim produserer 17 signifikante sifre i vitenskapelig notasjon, 3.3333333333333331E-001 for samme verdi, og skriver alltid et punktum som desimalseparator uansett locale. Er HotPDF på FPC en del av byggmatrisen din, dekker HotPDF Free Pascal- og Lazarus Win64-støttenotatene resten av plattformforskjellene
Delphi godtar den 17-sifrede forespørselen, men de to Delphi-målene er fortsatt uenige om utgangen: FloatToStrF(0.1, ffGeneral, 17, 0) gir 0.10000000000000001 på Win32 og 0.1 på Win64. Win64 RTL kan også innføre en sistesiffer-avrundingsfeil både ved formatering og parsing, så flere sifre snevrer inn gapet uten å garantere at hvert Double-bitmønster overlever en tekst-rundtur. HotPDFs dokumentasjon lover ingenting slikt, og din bør ikke gjøre det heller, med mindre du skiper en korrekt avrundet formatter og parser av egen hånd. Send TFormatSettings.Invariant, eller erstatt separatoren selv på eldre Delphi-versjoner, så en tysk eller fransk locale ikke skriver et komma inn i JSON
Hvorfor slutter Assert.AreEqual å kompilere på Win64?
Assert.AreEqual(3, Length(Arr)) på en dynamisk array kompilerer for Win32 og feiler for Win64 med E2532, «Couldn't infer generic type argument from different argument types», fordi Length av en dynamisk array returnerer NativeInt på Win64. Med en Integer-literal på den ene siden og en 64-bits NativeInt på den andre, kan DUnitXs generiske Assert.AreEqual<T> ikke lande på én enkelt T, og bygget stopper
TList.Count utløser samme feil siden Delphi 12, der egenskapen ble NativeInt; Delphi 11 deklarerer den fortsatt som Integer. Length av en string returnerer Integer på begge plattformer og er upåvirket, noe som er grunnen til at feilen dukker opp i noen test-units og ikke andre. Skriv typeargumentet eksplisitt, Assert.AreEqual<NativeInt>(3, Length(Arr)), og kompiler testprosjektet med dcc64 før du committer. En suite som bare noensinne bygger for Win32, vil ikke fortelle deg at Win64-bygget dens er knust før noen andre prøver det
Win64-porteringssjekkliste for Delphi numerisk kode
- Søk etter
Power(- ogIntPower(-kall med heltallsargumenter; sendDouble-typede verdier eller bygg begrensede tierpotenser selv - Kjør numeriske tester minst én gang med
exOverflowogexInvalidOpfjernet gjennomSetExceptionMask, på både Win32 og Win64 - Skriv
Int64-øvre grensen som< 9223372036854775808.0, aldri<= High(Int64), og avvis NaN og uendeligheter før noen sammenligning - Ikke konverter et parsede tall til
Int64bare fordiFracer 0; JSON-tall kan være langt større - Omskriv
while-løkker som leserCountpå nytt mens elementer slettes, som fastbegrensedefor ... downto-løkker - På FPC Win64, bruk
Str(Value:24, Text)når du trenger mer enn 15 signifikante sifre - Bruk
Assert.AreEqual<NativeInt>forLength- ogCount-asserts, og kompiler tester med dcc64 før du committer - Etter enhver endring av en parser eller renderer, kjør hele regresjonssuiten på Win32 og Win64, ikke bare én av dem
Bibliotekssidens fikser beskrevet her er alle i HotPDF siden v2.770.169, så SVG-import, XPS-konvertering, transparency rendering og JSON-jobbhåndtering oppfører seg nå likt på Win64 som på Win32. Genererer eller prosesserer du PDF-filer fra Delphi eller C++Builder for begge plattformer, har HotPDF Delphi PDF-komponenten-siden nedlastinger og full funksjonsliste