Win64 Delphi-kod kan fallera där samma källa kör rent på Win32, och HotPDF Delphi PDF-komponenten råkade ut för fem sådana fall under en nyligen härdningsomgång: Power(10, N) bunden till Single-overloaden, en while-loop som läser en inaktuell TList.Count, en High(Int64)-gräns som avrundas upp till 2^63, 15-siffrig float-text på FPC, och test-asserts som slutar kompilera
Inget av detta dyker upp om du bara bygger och testar Win32, vilket är exakt hur de smög in. Fallen nedan kommer från HotPDF:s SVG- och XPS-importörer, dess sidrenderare och dess JSON-jobbläsare, och de numeriska resultat som citeras återskapades med små sondprogram byggda för Win32 och Win64. Flyttar du en Delphi-kodbas till 64 bitar är vart och ett värt en grep
Varför ger Power(10, 100) överspill bara på Win64?
På Win64 löser System.Math.Power(10, N) med heltalsargument upp sig till Single-overloaden, så resultatet beräknas och returneras i single precision och allt över ungefär 3,4E38 ger överspill. På Win32 binder samma anrop till Extended-overloaden och kör på x87-FPU:n med 80-bitars precision, så Power(10, 100) är helt enkelt 1E100
System.Math deklarerar Power för Extended, Double och Single, plus en matchande IntPower-familj som Power anropar när exponenten är ett helt tal. På Win64 är Extended bara ett alias för Double (SizeOf(Extended) = 8), och för två heltalsargument plockar kompilatorn Single-versionen. Avslöjandet är precisionen, inte bara överspillen: på Win64 returnerar Power(10, 20) 1.0000000200408773E20, vilket är exakt Single(1E20). Ett Double-resultat skulle skrivas som 1E20. Vi såg samma bindning hos varje Win64-kompilator vi prövade, från Delphi 10.3 till kompilatorversion 37.0
Vad som händer sedan beror på flyttalsexceptionmasken. Delphi 12 och senare maskar alla flyttalsexceptioner som standard, så överspillen är tyst: Power(10, 100) returnerar +Inf och Power(10, -100) returnerar 0. Delphi 11 och tidigare lämnar exOverflow omaskad, och samma anrop kastar EOverflow. Applikationer som sätter masken själva, och DLL:er laddade i sådana värdar, får det beteende värden valt, vilket är varför ett bibliotek inte kan anta något av utfallen
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));
// Återskapa vad Delphi 11, eller en värd med strikta FP-inställningar, gör
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Att avmaska exOverflow och exInvalidOp under en tests varaktighet är det billigaste sättet att se vad en äldre kompilator eller en strikt värd ser. På en modern kompilator med standardinställningar kraschar inte buggen, den framställer oändligheter och nollor, och de är mycket svårare att upptäcka i en testlogg. Återställ föregående mask i finally: masken är per-tråd-tillstånd, och resten av testkörningen ärver vadhelst du lämnar kvar
Hur overloaden nådde HotPDF SVG- och XPS-import
HotPDF:s SVG- och XPS-sökvägsläsare delar en numerisk skanner, och den skannern skalade mantissan med Power(10, Exponent) så snart den läst en exponent. Vals SVG som skickas till THotPDF.ImportSVGFormXObject (ingångspunkten bakom att importera SVG till PDF som återanvändbara form XObjects), och all sökvägsgeometri hanterad under XPS- och OpenXPS-till-PDF-konvertering, kunde därför mata en koordinat som 1e100 eller 5e99 in i det anropet
v2.770.91 hade redan taklagt exponenten vid 100 och avvisat värden som skulle passera 1E300, vilket såg tillräckligt ut: 1E100 är långt ifrån Double-gränsen på omkring 1,8E308. På Win64 gav det ändå överspill, för beräkningen skedde aldrig i Double alls. Sedan v2.770.155 bygger skannern tiopotensen själv, och tal som 1e-100, eller en lång mantissa med en stor negativ exponent, läses som sitt verkliga värde i stället för att kollapsa till 0
En säker tiopotens för avgränsade exponenter
När exponenten är avgränsad är den säkraste tiopotensen en du bygger själv med Double-multiplikation. En loop på högst 100 multiplikationer kostar inget jämte att skanna texten runt den, den framställer aldrig en mellanprodukt större än den slutliga skalan, och den beter sig identiskt på Win32, Win64 och 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;
// Avvisa resultat som skulle lämna 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; // överstiger aldrig 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // dividera: 1E-100 har ingen exakt Double
Result := True;
end;
Tre detaljer bär tyngden. Intervallkontrollen använder två jämförelser i stället för Abs(Exponent) <= 100, för Abs(Low(Integer)) är fortfarande negativ och skulle segla rakt igenom. Negativa exponenter dividerar med skalan i stället för att multiplicera med en förberäknad 1E-100, som inte har någon exakt Double och skulle lägga till ytterligare ett avrundningssteg. Och Log10-förkontrollen avvisar resultat utanför Double-området innan multiplikationen har en chans att svämma över
Var tydlig med vad loopen ger upp. Tiopotenser upp till 1E22 är exakta i Double; därutöver avrundar varje multiplikation, och efter 100 av dem sitter skalan några enheter i sista platsen från den korrekt avrundade 1E100. För ritkoordinater är det osynligt. För en generell text-till-double-konvertering som måste återskapa varje värde bit för bit räcker det inte, och du behöver en korrekt avrundad konverteringsalgoritm i stället
När dcc64 läser en inaktuell TList.Count i en while-loop
Vi observerade Win64-kompilatorn (dcc64, kompilatorversion 37.0) framställa kod för en while List.Count > Start do-loop som raderade från listans slut och jämförde mot en stacktemporär i stället för att läsa om Count. Omskrivningen som fixade det var en for ... downto-loop, vars gränser utvärderas exakt en gång per definition
Loope kom i v2.769.3, som lärde renderarens transparency group-kod att hålla soft masks skapade inuti en grupp vid liv över en tvåpassrendering och frigöra dem efteråt. Städningen satt i ett finally-block efter en en- eller tvåpass-for-loop, inuti per tile-loopen. Reducerad till sin form ser före och efter ut så här:
// Formen vi såg felkompilerad av dcc64 (kompilatorversion 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;
// Ersättning: gränserna utvärderas en gång, ingen temporär som blir inaktuell
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count är NativeInt sedan Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
I den genererade Win64-koden delade Count i loopvillkoret och Count-läsningen inuti kroppen en stackplats. Villkoret jämförde mot den platsen vid ingång, innan något skrivit den, och inget uppdaterade den efter Delete. När en grupp inte skapat egna soft masks körde kroppen ändå och bad en tom lista om post -1, så i 64-bitars byggen feller varje sida som innehöll en sådan transparency group med EListError. Win32-koden för samma källa var korrekt, och v2.770.1 bytte ut loopen
Vi har inte reducerat detta till en minimal reproduktion, och en liten fristående loop som DropMasksWhile kan mycket väl kompileras korrekt; den omgivande try/finally och nästlade loopar verkar spela roll. Behandla det som kodgenerering vi observerade på en kompilatorversion, inte som ett känt fel hos varje Win64-kompilator. Den praktiska lärdomen är billigare än rotorsaken: en loop vars villkor läser om en samlings antal medan kroppen krymper samlingen är värd att skriva om som en fastgränsad for ... downto, och renderarändringar behöver en fullständig Win64-testkörning, inte bara Win32
Lokalisera en krasch som bara ett optimerat Win64-bygge visar
Misslyckandet återskapades bara i det optimerade Win64-bygget, så platsen kom från verktyg utanför IDE:n. Ett litet sondprogram registrerade en vectored exception handler med AddVectoredExceptionHandler, fångade stacken vid första exceptionen med RtlCaptureStackBackTrace, och översatte returadresserna till funktionsnamn med hjälp av den detaljerade map-filen länkaren skriver med -GD. Att disassemblera den funktionen visade sedan att jämförelsen läste en stackplats, [rbp+0x298], som bara någonsin skrevs inuti loopkroppen. Det är den nivån av bevis du vill ha innan du skyller på en kompilator, och det tog mindre tid än att stega igenom ett release-bygge
Varför är High(Int64) ingen säker övre gräns för en Double?
En Double kan inte representera High(Int64): att konvertera 9223372036854775807 till Double avrundar upp till exakt 2^63, ett förbi det största Int64. På Win64 sker den konverteringen inuti själva jämförelsen, så D <= High(Int64) är True för D = 2^63, och den Round eller Trunc som följer ger överspill
Win32 döljer detta av samma skäl det dolde Power-problemet. Jämförelsen körs i 80-bitars Extended-precision med en 64-bitars mantissa, där High(Int64) är exakt och 2^63 korrekt jämförs större. Win64 har ingen bredare typ att falla tillbaka på. Konverteringen utanför området är inte heller vacker: i våra Win64-tester returnerade Round(2^63) Low(Int64), en tyst teckenvändning, oavsett om exInvalidOp var maskad eller inte. Win32 returnerar samma värde när maskad och kastar EInvalidOp när omaskad
| Uttryck | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), exceptioner maskade (Delphi 12+-standard) | 1E100 | +Inf |
Power(10, 100), exOverflow omaskad | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp omaskad | EInvalidOp | Low(Int64) |
HotPDF mötte detta i JSON-läsaren bakom sina dokumentjobbvärden. JSON sätter ingen intervallgräns på tal, och gamla serialiserare gjorde om varje värde med Frac(Value) = 0 till ett heltal med Round, så en helt laglig 1e19 blev antingen ett fel heltal eller en exception, beroende på masken. Sedan v2.770.169 skrivs ett helt tal som ett heltal bara när det ryms i Int64, allt annat behåller sin flyttalstext, och heltalsgettersna returnerar anroparens standardvärde för värden utanför området i stället för ett omslutet värde
const
TwoPow63 = 9223372036854775808.0; // 2^63, exakt i Double och 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
// Anropare avvisar NaN och oändligheter först: JSON har ingen stavning för dem
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral stannar vid 15 siffror
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Den övre gränsen är literalen 9223372036854775808.0 med en strikt <. Den konstanten är 2^63, exakt i både Double och Extended, så jämförelsen betyder samma sak på varje plattform. Den undre gränsen kan använda >= för -2^63 är exakt Low(Int64). Att testa IsNan och IsInfinite först, med kortslutningsevaluering, håller NaN och oändligheter borta från Frac och jämförelserna, som kan kasta EInvalidOp när värden avmaskat den
Hur många siffror ger float-till-text verkligen på Win64?
Färre än du ber om, hos två av tre kompilatorer. Free Pascal 3.3.1:s FloatToStrF(Value, ffGeneral, 17, 0) på Win64 stannar vid 15 signifikanta siffror, så 1/3 kommer tillbaka som 0.333333333333333 och två olika Double-värden kan serialiseras till identisk text. Str(Value:24, Text) följt av Trim framställer 17 signifikanta siffror i vetenskaplig notation, 3.3333333333333331E-001 för samma värde, och skriver alltid en punkt som decimaltecken oavsett locale. Är HotPDF på FPC en del av din byggmatris täcker HotPDF Free Pascal- och Lazarus Win64-stödsanteckningarna resten av plattformsskillnaderna
Delphi accepterar 17-siffrorsbegäran, men de två Delphi-målen är ändå oense om utdatat: FloatToStrF(0.1, ffGeneral, 17, 0) ger 0.10000000000000001 på Win32 och 0.1 på Win64. Win64-RTL:n kan också införa ett sista-siffer-avrundningsfel både vid formatering och vid tolkning, så fler siffror smalnar av gapet utan att garantera att varje Double-bitmönster överlever en text tur och retur. HotPDF:s dokumentation lovar inget sådant, och din ska inte heller det om du inte skeppar en korrekt avrundad formaterare och tolkare av egen hand. Skicka TFormatSettings.Invariant, eller byt ut separatorn själv på äldre Delphi-versioner, så en tysk eller fransk locale inte skriver ett kommatecken in i JSON
Varför slutar Assert.AreEqual kompilera på Win64?
Assert.AreEqual(3, Length(Arr)) på en dynamisk array kompileras för Win32 och faller för Win64 med E2532, "Couldn't infer generic type argument from different argument types", för Length hos en dynamisk array returnerar NativeInt på Win64. Med en Integer-literal på ena sidan och en 64-bitars NativeInt på andra kan DUnitX generiska Assert.AreEqual<T> inte landa på en enda T, och bygget stannar
TList.Count utlöser samma fel sedan Delphi 12, där egenskapen blev NativeInt; Delphi 11 deklarerar den fortfarande som Integer. Length hos en string returnerar Integer på båda plattformar och påverkas inte, vilket är varför felet dyker upp i vissa testunits och inte i andra. Skriv typargumentet explicit, Assert.AreEqual<NativeInt>(3, Length(Arr)), och kompilera testprojektet med dcc64 innan commit. En svit som bara någonsin byggs för Win32 avslöjar inte att dess Win64-bygge är trasigt förrän någon annan prövar det
Win64-porteringschecklista för Delphi numerisk kod
- Sök efter
Power(- ochIntPower(-anrop med heltalsargument; skickaDouble-typade värden eller bygg avgränsade tiopotenser själv - Kör numeriska tester minst en gång med
exOverflowochexInvalidOpborttagna genomSetExceptionMask, på både Win32 och Win64 - Skriv
Int64-övre gränsen som< 9223372036854775808.0, aldrig<= High(Int64), och avvisa NaN och oändligheter före varje jämförelse - Konvertera inte ett tolkat tal till
Int64bara för attFracär 0; JSON-tal kan vara mycket större - Skriv om
while-loopar som läser omCountmedan poster raderas till fastgränsadefor ... downto-loopar - På FPC Win64, använd
Str(Value:24, Text)när du behöver fler än 15 signifikanta siffror - Använd
Assert.AreEqual<NativeInt>förLength- ochCount-asserts, och kompilera tester med dcc64 innan commit - Efter varje ändring av en parser eller renderare, kör hela regressionssviten på Win32 och Win64, inte bara en av dem
Fixarna på bibliotekssidan som beskrivs här finns alla i HotPDF sedan v2.770.169, så SVG-import, XPS-konvertering, transparensrendering och JSON-jobbhantering beter sig nu likadant på Win64 som på Win32. Genererar eller processerar du PDF-filer från Delphi eller C++Builder för båda plattformarna har HotPDF Delphi PDF component-sidan nedladdningarna och den fullständiga funktionslistan