Win64 Delphi-kode kan fejle, dér hvor samme kilde kører rent på Win32, og HotPDF Delphi PDF-komponenten ramte fem sådanne tilfælde under en nylig hærdningsrunde: Power(10, N) bundet til Single-overloaden, en while-løkke, der læste en forældet TList.Count, en High(Int64)-grænse, der runder op til 2^63, 15-cifret float-tekst på FPC og test-asserts, der holder op med at kompilere
Ingen af dem viser sig, hvis du kun bygger og tester Win32, hvilket præcis er sådan, de snek sig ind. Tilfældene nedenfor kommer fra HotPDF's SVG- og XPS-importører, dens side-renderer og dens JSON job-læser, og de citerede numeriske resultater blev reproduceret med små probe-programmer bygget til Win32 og Win64. Er du ved at flytte en Delphi-kodebase til 64-bit, er hver enkelt værd en grep
Hvorfor overløber Power(10, 100) kun på Win64?
På Win64 opløses System.Math.Power(10, N) med integer-argumenter til Single-overloaden, så resultatet beregnes og returneres i single precision, og alt over cirka 3.4E38 overløber. På Win32 binder samme kald til Extended-overloaden og kører på x87 FPU'en med 80-bit præcision, så Power(10, 100) bare er 1E100
System.Math deklarerer Power for Extended, Double og Single plus en matchende IntPower-familie, som Power kalder, når eksponenten er et heltal. På Win64 er Extended kun et alias for Double (SizeOf(Extended) = 8), og for to integer-argumenter vælger kompileren Single-versionen. Afsløringen er præcisionen, ikke kun overflowet: på Win64 returnerer Power(10, 20) 1.0000000200408773E20, hvilket er præcis Single(1E20). Et Double-resultat ville printe som 1E20. Vi så samme binding hos hver Win64-kompiler, vi prøvede, fra Delphi 10.3 til compiler version 37.0
Hvad der sker bagefter afhænger af floating-point exception-masken. Delphi 12 og senere maskerer alle floating-point exceptions som default, så overflowet er stille: Power(10, 100) returnerer +Inf, og Power(10, -100) returnerer 0. Delphi 11 og tidligere lader exOverflow stå umaskeret, og samme kald rejser EOverflow. Applikationer, der selv sætter masken, og DLL'er loadet ind i sådanne værter, får den adfærd, værten valgte, hvilket er hvorfor et bibliotek ikke kan regne med nogen af udfaldene
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 printer 1E20; Win64 printer 1.0000000200408773E20 (Single-overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproducér hvad Delphi 11, eller en host med strict FP-indstillinger, gør
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
At unmaskere exOverflow og exInvalidOp i Varigheden af en test er den billigste måde at se, hvad en ældre kompiler eller en strict host ser. På en moderne kompiler med default-indstillinger crasher buggen ikke, den producerer infinities og nuller, og dem er det meget sværere at få øje på i en testlog. Gendan den forrige maske i finally: masken er pr. tråd-tilstand, og resten af testkørslen arver, hvad end du efterlader
Sådan nåede overloaden frem til HotPDF's SVG- og XPS-import
HotPDF's SVG- og XPS-stilæsere deler én talscanner, og den scanner skalerede mantissen med Power(10, Exponent), så snart den havde læst en eksponent. Enhver SVG givet til THotPDF.ImportSVGFormXObject (indgangspunktet bag import af SVG til PDF som genbrugelige form XObjects), og enhver sti-geometri behandlet under XPS- og OpenXPS-til-PDF-konvertering, kunne derfor føde en koordinat som 1e100 eller 5e99 ind i det kald
v2.770.91 havde allerede kappet eksponenten ved 100 og afvist værdier, der ville passere 1E300, hvilket så ud som nok: 1E100 er ingen steder nær Double-grænsen på omkring 1.8E308. På Win64 overløb det alligevel, for beregningen skete slet ikke i Double. Siden v2.770.155 bygger scanneren titens potens selv, og tal som 1e-100, eller en lang mantisse med en stor negativ eksponent, læses som deres rigtige værdi i stedet for at kollapse til 0
En sikker titens potens for afgrænsede eksponenter
Når eksponenten er afgrænset, er den sikreste titens potens én, du bygger selv med Double-multiplikation. En løkke på højst 100 multiplikationer koster intet i forhold til at scanne teksten omkring den, den producerer aldrig en mellemværdi større end den endelige skala, og den opfører sig 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;
// Afvis resultater, der ville forlade Double-intervallet
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 aldrig 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 vægten. Interval-tjekket bruger to sammenligninger frem for Abs(Exponent) <= 100, for Abs(Low(Integer)) stadig er negativ og ville sejle lige igennem. Negative eksponenter dividerer med skalaen i stedet for at multiplicere med en forudberegnet 1E-100, som ikke har nogen eksakt Double og ville tilføje endnu et afrundingstrin. Og Log10-fortjekket afviser resultater uden for Double-intervallet, før multiplikationen får en chance for at løbe over
Vær klar over, hvad løkken giver afkald på. Titens potenser op til 1E22 er eksakte i Double; derefter runder hver multiplikation, og efter 100 af dem sidder skalaen et par units in the last place fra den korrekt rundede 1E100. Til tegnekoordinater er det usynligt. Til en generisk tekst-til-double-konvertering, der skal reproducere hver værdi bit for bit, er det ikke godt nok, og så behøver du en korrekt rundet konverteringsalgoritme i stedet
Når dcc64 læser en forældet TList.Count i en while-løkke
Vi observerede Win64-kompileren (dcc64, compiler version 37.0) generere kode for en while List.Count > Start do-løkke, der slettede fra listens ende og sammenlignede mod en stak-midlertidig i stedet for at genlæse Count. Omskrivningen, der fik det væk, var en for ... downto-løkke, hvis grænser efter definition evalueres præcis én gang
Løkken kom i v2.769.3, som lærte rendererens transparency group-kode at holde soft masks oprettet inde i en group i live på tværs af en to-trins-render og frigive dem bagefter. Oprydningen sad i en finally-blok efter en én- eller to-trins for-løkke, inde i pr. tile-løkken. Reduceret til sin form ser før og efter sådan ud:
// Den form, vi så fejlkompileret af dcc64 (compiler version 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: grænserne evalueres én gang, ingen midlertidig at blive forældet
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 genererede Win64-kode delte Count i løkkebetingelsen og Count-læsningen inde i kroppen én stak-slot. Betingelsen sammenlignede mod den slot ved indgang, før noget havde skrevet den, og intet genopfriskede den efter Delete. Havde en group oprettet ingen soft masks af egen, kørte kroppen alligevel og bad en tom liste om item -1, så i 64-bit builds fejlede hver side med en sådan transparency group med EListError. Win32-koden for samme kilde var korrekt, og v2.770.1 erstattede løkken
Vi har ikke reduceret dette til en minimal reproduction, og en lille selvstændig løkke som DropMasksWhile kompilerer sikkert fint; de omgivende try/finally og indlejrede løkker ser ud til at betyde noget. Behandl det som kodegenerering, vi observerede på én compiler-version, ikke som en kendt defekt hos hver Win64-kompiler. Den praktiske lektie er billigere end rodårsagen: en løkke, hvis betingelse genlæser en collections count, mens kroppen skrumpler den collection, er værd at omskrive til en fixed-bound for ... downto, og renderer-ændringer behøver en fuld Win64-testkørsel, ikke bare Win32
At lokalisere et crash, som kun en optimeret Win64-build viser
Fejlen reproducerede kun i den optimerede Win64-build, så lokaliseringen kom fra værktøjer uden for IDE'en. Et lille probe-program registrerede en vectored exception handler med AddVectoredExceptionHandler, fangede stakken ved den første exception med RtlCaptureStackBackTrace og oversatte returadresserne til funktionsnavne ved hjælp af den detaljerede map-fil, linkeren skriver med -GD. At disassemblere den funktion viste så sammenligningen læse en stak-slot, [rbp+0x298], der kun nogensinde blev skrevet inde i løkkekroppen. Det er det niveau af beviser, du vil have, inden du bebrejder en kompiler, og det tog mindre tid end at steppe gennem en release build
Hvorfor er High(Int64) ikke en sikker øvre grænse for en Double?
En Double kan ikke repræsentere High(Int64): at konvertere 9223372036854775807 til Double runder op til præcis 2^63, én forbi den største Int64. På Win64 sker konverteringen inde i selve sammenligningen, så D <= High(Int64) er True for D = 2^63, og den Round eller Trunc, der følger, overløber
Win32 skjuler det af samme grund, som den skjulte Power-problemet. Sammenligningen kører i 80-bit Extended-præcision med en 64-bit mantisse, hvor High(Int64) er eksakt, og 2^63 korrekt sammenlignes større. Win64 har ingen bredere type at falde tilbage på. Konverteringen uden for interval er heller ikke køn: i vores Win64-tests returnerede Round(2^63) Low(Int64), et stiltiende fortegnsskift, uanset om exInvalidOp var maskeret eller ej. Win32 returnerer samme værdi, når maskeret, og rejser EInvalidOp, når umaskeret
| Udtryk | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), exceptions maskeret (Delphi 12+ default) | 1E100 | +Inf |
Power(10, 100), exOverflow umaskeret | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp umaskeret | EInvalidOp | Low(Int64) |
HotPDF mødte det i JSON-læseren bag dens dokument job-værdier. JSON sætter ingen intervalgrænse på tal, og den gamle serializer omdannede enhver værdi med Frac(Value) = 0 til en integer med Round, så en fuldt lovlig 1e19 blev enten et forkert integer eller en exception, afhængigt af masken. Siden v2.770.169 skrives et heltal kun som integer, når det er plads i Int64, alt andet beholder sin floating-point-tekst, og integer-getterne returnerer kalderens default for værdier uden for interval i stedet for en wrapped en
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
// Kaldere afviser NaN og infinities først: JSON har ingen stavemåde for dem
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral stopper ved 15 cifre
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Den øvre grænse er literalen 9223372036854775808.0 med en strict <. Den konstant er 2^63, eksakt i både Double og Extended, så sammenligningen betyder det samme på hver platform. Den nedre grænse kan bruge >=, for -2^63 er præcis Low(Int64). At teste IsNan og IsInfinite først, med short-circuit-evaluering, holder NaN og infinities væk fra Frac og sammenligningerne, som kan rejse EInvalidOp, når værten har unmaskeret den
Hvor mange cifre giver float-til-tekst egentlig på Win64?
Færre, end du beder om, på to kompilere ud af tre. Free Pascal 3.3.1's FloatToStrF(Value, ffGeneral, 17, 0) på Win64 stopper ved 15 signifikante cifre, så 1/3 kommer tilbage som 0.333333333333333, og to forskellige Double-værdier kan serialisere til identisk tekst. Str(Value:24, Text) efterfulgt af Trim producerer 17 signifikante cifre i videnskabelig notation, 3.3333333333333331E-001 for samme værdi, og skriver altid et punktum som decimalseparator uanset locale. Er HotPDF på FPC en del af din build-matrix, dækker HotPDF Free Pascal- og Lazarus Win64-supportnoterne resten af platformsforskellene
Delphi accepterer 17-cifre-anmodningen, men de to Delphi-targets er stadig uenige om output: FloatToStrF(0.1, ffGeneral, 17, 0) giver 0.10000000000000001 på Win32 og 0.1 på Win64. Win64-RTL'en kan også introducere en sidste-ciffer-afrundingsfejl både ved formatering og parsing, så flere cifre indsnævrer kløften uden at garantere, at hvert Double-bitmønster overlever en tekst-roundtrip. HotPDF's dokumentation lover ingenting sådant, og din burde det heller ikke, medmindre du skiber din egen korrekt rundede formatter og parser. Giv TFormatSettings.Invariant med, eller erstat separatoren selv på ældre Delphi-versioner, så et tysk eller fransk locale ikke skriver et komma ind i JSON
Hvorfor holder Assert.AreEqual op med at kompilere på Win64?
Assert.AreEqual(3, Length(Arr)) på et dynamisk array kompilerer til Win32 og fejler til Win64 med E2532, "Couldn't infer generic type argument from different argument types", for Length af et dynamisk array returnerer NativeInt på Win64. Med en Integer-literal på den ene side og en 64-bit NativeInt på den anden kan DUnitX' generiske Assert.AreEqual<T> ikke lande på ét enkelt T, og builden stopper
TList.Count udløser samme fejl siden Delphi 12, hvor propertyen blev NativeInt; Delphi 11 deklarerer den stadig som Integer. Length af en string returnerer Integer på begge platforme og er upåvirket, hvilket er hvorfor fejlen optræder i nogle test-units og ikke andre. Skriv type-argumentet eksplicit, Assert.AreEqual<NativeInt>(3, Length(Arr)), og kompilér testprojektet med dcc64, inden du committer. En suite, der kun nogensinde bygger til Win32, fortæller dig ikke, at dens Win64-build er i stykker, før nogen andre prøver den
Win64-porteringstjekliste til Delphi numerisk kode
- Søg efter
Power(- ogIntPower(-kald med integer-argumenter; givDouble-typede værdier med, eller byg afgrænsede titens potenser selv - Kør numeriske tests mindst én gang med
exOverflowogexInvalidOpfjernet gennemSetExceptionMask, på både Win32 og Win64 - Skriv
Int64-øvre grænse som< 9223372036854775808.0, aldrig<= High(Int64), og afvis NaN og infinities før nogen sammenligning - Konvertér ikke et parseret tal til
Int64bare fordiFracer 0; JSON-tal kan være langt større - Omskriv
while-løkker, der genlæserCount, mens der slettes items, til fixed-boundfor ... downto-løkker - På FPC Win64, brug
Str(Value:24, Text), når du behøver mere end 15 signifikante cifre - Brug
Assert.AreEqual<NativeInt>tilLength- ogCount-asserts, og kompilér tests med dcc64, inden du committer - Efter enhver ændring af en parser eller renderer, kør den fulde regressionssuite på Win32 og Win64, ikke bare én af dem
Biblioteksside-fixene, der er beskrevet her, er alle i HotPDF siden v2.770.169, så SVG-import, XPS-konvertering, transparency-rendering og JSON job-håndtering opfører sig nu ens på Win64 som på Win32. Genererer eller behandler du PDF-filer fra Delphi eller C++Builder til begge platforme, har HotPDF Delphi PDF-komponenten-siden downloads og den fulde funktionsliste