Win64 Delphi-code kan falen waar dezelfde bron netjes draait op Win32, en de HotPDF Delphi PDF-component trof vijf zulke gevallen aan tijdens een recente verhardingsronde: Power(10, N) die aan de Single-overload bindt, een while-lus die een verouderde TList.Count leest, een High(Int64)-grens die ophoogt naar 2^63, 15-cijferige floattekst op FPC, en test-asserts die stoppen met compileren
Geen daarvan verschijnt als u alleen Win32 bouwt en test, en precies zo zijn ze erin geslopen. De gevallen hieronder komen uit de SVG- en XPS-importeurs van HotPDF, zijn paginarender en zijn JSON-joblezer, en de geciteerde numerieke resultaten zijn gereproduceerd met kleine probe-programma's gebouwd voor Win32 en Win64. Verhuist u een Delphi-codebase naar 64-bit, dan is elk geval een grep waard
Waarom loopt Power(10, 100) alleen op Win64 over?
Op Win64 resolvant System.Math.Power(10, N) met integer-argumenten naar de Single-overload, dus het resultaat wordt in single precision berekend en teruggegeven, en alles boven ruwweg 3.4E38 loopt over. Op Win32 bindt dezelfde aanroep aan de Extended-overload en draait op de x87 FPU met 80-bit precisie, dus Power(10, 100) is gewoon 1E100
System.Math declareert Power voor Extended, Double en Single, plus een matchende IntPower-familie die Power aanroept zodra de exponent een geheel getal is. Op Win64 is Extended slechts een alias voor Double (SizeOf(Extended) = 8), en bij twee integer-argumenten kiest de compiler de Single-versie. Het verraad zit in de precisie, niet alleen in de overflow: op Win64 geeft Power(10, 20) 1.0000000200408773E20 terug, wat exact Single(1E20) is. Een Double-resultaat zou als 1E20 printen. We zagen dezelfde binding bij elke Win64-compiler die we probeerden, van Delphi 10.3 tot en met compilerversie 37.0
Wat er daarna gebeurt, hangt af van de floating-point exception mask. Delphi 12 en later maskeren alle floating-point exceptions by default, dus de overflow is stil: Power(10, 100) geeft +Inf terug en Power(10, -100) geeft 0. Delphi 11 en eerder laten exOverflow ongemaskeerd, en dezelfde aanroep geeft een EOverflow. Applicaties die het masker zelf zetten, en DLL's die in zulke hosts worden geladen, krijgen welk gedrag de host ook koos, en daarom kan een library geen van beide uitkomsten aannemen
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 print 1E20; Win64 print 1.0000000200408773E20 (Single-overload)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproduceer wat Delphi 11, of een host met strikte FP-instellingen, doet
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
exOverflow en exInvalidOp voor de duur van een test uit het masker halen is de goedkoopste manier om te zien wat een oudere compiler of een strenge host ziet. Op een moderne compiler met defaultinstellingen crasht de bug niet, maar produceert hij oneindigheden en nullen, en die zijn veel moeilijker te spotten in een testlog. Zet het vorige masker in finally terug: het masker is per-thread status, en de rest van de testrun erft wat u achterlaat
Hoe de overload de SVG- en XPS-import van HotPDF bereikte
De SVG- en XPS-padlezers van HotPDF delen één cijferscanner, en die scanner schaalde de mantisse met Power(10, Exponent) zodra hij een exponent had gelezen. Elke SVG die aan THotPDF.ImportSVGFormXObject wordt doorgegeven (de ingang achter SVG importeren in PDF als herbruikbare form XObjects), en elke padgeometrie die wordt verwerkt tijdens XPS- en OpenXPS-naar-PDF-conversie, kon dus een coördinaat zoals 1e100 of 5e99 in die aanroep voeren
v2.770.91 had de exponent al geplafonneerd op 100 en weigerde waarden die voorbij 1E300 zouden gaan, wat als genoeg oogde: 1E100 is mijlenver van de Double-limiet van zo'n 1.8E308. Op Win64 liep het alsnog over, want de berekening gebeurde helemaal niet in Double. Sinds v2.770.155 bouwt de scanner de macht van tien zelf, en getallen zoals 1e-100, of een lange mantisse met een grote negatieve exponent, worden gelezen als hun echte waarde in plaats van in te klappen naar 0
Een veilige macht van tien voor begrensde exponenten
Zodra de exponent begrensd is, is de veiligste macht van tien er een die u zelf bouwt met Double-vermenigvuldiging. Een lus van hooguit 100 vermenigvuldigingen kost niets naast het scannen van de tekst eromheen, produceert nooit een tussentijds getal groter dan de eindeschaal, en gedraagt zich identiek op Win32, Win64 en 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;
// Weiger resultaten die het Double-bereik zouden verlaten
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; // overstijgt nooit 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // delen: 1E-100 heeft geen exacte Double
Result := True;
end;
Drie details dragen het gewicht. De bereikcontrole gebruikt twee vergelijkingen in plaats van Abs(Exponent) <= 100, want Abs(Low(Integer)) is nog steeds negatief en zou er moeiteloos doorheen glippen. Negatieve exponenten delen door de schaal in plaats van te vermenigvuldigen met een voorberekende 1E-100, die geen exacte Double heeft en nog een extra afrondingsstap zou toevoegen. En de Log10-voorcontrole weigert resultaten buiten het Double-bereik voordat de vermenigvuldiging de kans krijgt te overlopen
Wees helder over wat de lus prijsgeeft. Machten van tien tot 1E22 zijn exact in Double; daarna rondt elke vermenigvuldiging af, en na 100 ervan zit de schaal enkele units van de laatste plaats verwijderd van de correct afgeronde 1E100. Voor tekencoördinaten is dat onzichtbaar. Voor een algemene tekst-naar-double-conversie die elke waarde bit voor bit moet reproduceren, is het niet goed genoeg, en dan heeft u een correct afgerond conversiealgoritme nodig
Als dcc64 een verouderde TList.Count leest in een while-lus
We zagen de Win64-compiler (dcc64, compilerversie 37.0) code genereren voor een while List.Count > Start do-lus die vanaf het einde van de lijst verwijderde en vergeleek tegen een stacktijdelijke in plaats van Count opnieuw te lezen. De herschrijving die het oploste een for ... downto-lus, waarvan de grenzen per definitie exact één keer worden geëvalueerd
De lus kwam binnen in v2.769.3, dat de transparency-group-code van de renderer leerde soft masks die binnen een groep waren aangemaakt in leven te houden over een render in één of twee passen en ze daarna vrij te geven. De opruiming zat in een finally-blok na een for-lus van één of twee passen, binnen de per-tile-lus. Teruggebracht tot zijn vorm zien voor en na er zo uit:
// De vorm die we verkeerd gecompileerd zagen door dcc64 (compilerversie 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;
// Vervanging: de grenzen worden één keer geëvalueerd, geen tijdelijke die kan verouderen
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count is NativeInt sinds Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
In de gegenereerde Win64-code deelden de Count in de loopconditie en de Count die binnen de body werd gelezen één stackslot. De conditie vergeleek bij binnenkomst tegen dat slot, voordat er iets in was geschreven, en niets verversde hem na Delete. Had een groep geen eigen soft masks aangemaakt, dan draaide de body toch en vroeg een lege lijst om item -1, dus in 64-bit builds faalde elke pagina met zo'n transparency group met een EListError. De Win32-code voor dezelfde bron was correct, en v2.770.1 verving de lus
We hebben dit niet teruggebracht tot een minimale reproductie, en een kleine losse lus zoals DropMasksWhile compileert mogelijk prima; de omliggende try/finally en geneste lussen lijken ertoe te doen. Beschouw het als codegeneratie die we op één compilerversie zagen, niet als een bekend defect van elke Win64-compiler. De praktische les is goedkoper dan de onderliggende oorzaak: een lus waarvan de conditie het aantal van een verzameling opnieuw leest terwijl de body die verzameling krimpt, verdient het om te worden herschreven als een for ... downto met vaste grenzen, en rendererwijzigingen hebben een volledige Win64-testrun nodig, niet alleen Win32
Een crash lokaliseren die alleen een geoptimaliseerde Win64-build toont
De falering reproduceerde alleen in de geoptimaliseerde Win64-build, dus de lokatie kwam van tools buiten de IDE. Een klein probe-programma registreerde een vectored exception handler met AddVectoredExceptionHandler, ving de stack bij de eerste exception met RtlCaptureStackBackTrace, en vertaalde de retouradressen in functienamen met behulp van het gedetailleerde map-bestand dat de linker met -GD wegschrijft. Het disassembleren van die functie toonde toen een vergelijking die een stackslot las, [rbp+0x298], dat alleen ooit binnen de loopbody werd geschreven. Dat is het bewijsniveau dat u wilt voordat u een compiler de schuld geeft, en het kostte minder tijd dan door een release-build stappen
Waarom is High(Int64) geen veilige bovengrens voor een Double?
Een Double kan High(Int64) niet representeren: 9223372036854775807 naar Double converteren rondt ophoogend af op exact 2^63, één voorbij het grootste Int64. Op Win64 gebeurt die conversie binnen de vergelijking zelf, dus D <= High(Int64) is True voor D = 2^63, en de Round of Trunc die volgt loopt over
Win32 verbergt dit om dezelfde reden als waarom het het Power-probleem verborg. De vergelijking draait in 80-bit Extended-precisie met een 64-bit mantisse, waar High(Int64) exact is en 2^63 correct als groter vergelijkt. Win64 heeft geen breder type om op terug te vallen. De buiten-bereik-conversie is ook niet fraai: in onze Win64-tests gaf Round(2^63) Low(Int64) terug, een stille tekenomkering, of exInvalidOp nu gemaskeerd was of niet. Win32 geeft dezelfde waarde terug bij maskering en geeft een EInvalidOp zonder maskering
| Expressie | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), exceptions gemaskeerd (Delphi 12+ default) | 1E100 | +Inf |
Power(10, 100), exOverflow ongemaskeerd | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp ongemaskeerd | EInvalidOp | Low(Int64) |
HotPDF trof dit aan in de JSON-lezer achter zijn documentjobwaarden. JSON stelt geen bereiklimiet aan getallen, en de oude serializer maakte van elke waarde met Frac(Value) = 0 een integer met Round, dus een prima legale 1e19 werd ofwel een verkeerde integer ofwel een exception, afhankelijk van het masker. Sinds v2.770.169 wordt een geheel getal alleen als integer weggeschreven zodra hij in Int64 past, houdt al het restant zijn floating-point tekst, en geven de integer-getters de default van de aanroeper terug bij buiten-bereik-waarden in plaats van een omgeslagen waarde
const
TwoPow63 = 9223372036854775808.0; // 2^63, exact in Double en 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
// Aanroepers wijzen NaN en oneindigheden eerst af: JSON heeft er geen spelling voor
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral stopt op 15 cijfers
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
De bovengrens is de literal 9223372036854775808.0 met een strikte <. Die constante is 2^63, exact in zowel Double als Extended, dus de vergelijking betekent op elk platform hetzelfde. De ondergrens kan >= gebruiken omdat -2^63 exact Low(Int64) is. Eerst IsNan en IsInfinite testen, met kortsluitingsevaluatie, houdt NaN en oneindigheden uit de buurt van Frac en de vergelijkingen, die een EInvalidOp kunnen geven zodra de host haar heeft ontmaskerd
Hoeveel cijfers geeft float-naar-tekst werkelijk op Win64?
Minder dan u vraagt, bij twee compilers van de drie. De FloatToStrF(Value, ffGeneral, 17, 0) van Free Pascal 3.3.1 op Win64 stopt op 15 significante cijfers, dus 1/3 komt terug als 0.333333333333333 en twee verschillende Double-waarden kunnen naar identieke tekst serialiseren. Str(Value:24, Text) gevolgd door Trim levert 17 significante cijfers in wetenschappelijke notatie op, 3.3333333333333331E-001 voor dezelfde waarde, en schrijft altijd een punt als decimaalscheiding ongeacht de locale. Maakt HotPDF op FPC deel uit van uw buildmatrix, dan behandelen de HotPDF Free Pascal en Lazarus Win64-ondersteuningsnotities de rest van de platformverschillen
Delphi accepteert het verzoek om 17 cijfers, maar de twee Delphi-doelen zijn het nog steeds oneens over de uitvoer: FloatToStrF(0.1, ffGeneral, 17, 0) geeft 0.10000000000000001 op Win32 en 0.1 op Win64. De Win64-RTL kan bovendien een laatste-cijfer-afrondingsfout introduceren, zowel bij formatteren als bij parsen, dus meer cijfers verkleinen de kloof zonder te garanderen dat elk Double-bitpatroon een tekstroundtrip overleeft. De documentatie van HotPDF doet die belofte niet, en die van u ook niet tenzij u een correct afgeronde formatter en parser van eigen makelij verscheept. Geef TFormatSettings.Invariant door, of vervang het scheidingsteken zelf op oudere Delphi-versies, zodat een Duitse of Franse locale geen komma in JSON schrijft
Waarom stopt Assert.AreEqual met compileren op Win64?
Assert.AreEqual(3, Length(Arr)) op een dynamische array compileert voor Win32 en faalt voor Win64 met E2532, "Couldn't infer generic type argument from different argument types", omdat Length van een dynamische array op Win64 een NativeInt teruggeeft. Met een Integer-literal aan de ene kant en een 64-bit NativeInt aan de andere, kan de generieke Assert.AreEqual<T> van DUnitX niet op één T uitkomen, en de build stopt
TList.Count triggert dezelfde fout sinds Delphi 12, waar de property NativeInt werd; Delphi 11 declareert haar nog als Integer. Length van een string geeft op beide platforms een Integer terug en is onaangetast, en daarom verschijnt de fout in sommige testunits en niet in andere. Schrijf het type-argument expliciet, Assert.AreEqual<NativeInt>(3, Length(Arr)), en compileer het testproject met dcc64 vóór het committen. Een suite die alleen ooit voor Win32 bouwt, vertelt u niet dat haar Win64-build kapot is totdat iemand anders het probeert
Win64-porteringchecklist voor numerieke Delphi-code
- Zoek naar
Power(- enIntPower(-aanroepen met integer-argumenten; geefDouble-getypeerde waarden door of bouw begrensde machten van tien zelf - Draai numerieke tests ten minste één keer met
exOverflowenexInvalidOpweggehaald viaSetExceptionMask, op zowel Win32 als Win64 - Schrijf de
Int64-bovengrens als< 9223372036854775808.0, nooit<= High(Int64), en wijs NaN en oneindigheden af vóór elke vergelijking - Converteer een geparsed getal niet naar
Int64alleen omdatFrac0 is; JSON-getallen kunnen veel groter zijn - Herschrijf
while-lussen dieCountopnieuw lezen tijdens het verwijderen van items naarfor ... downto-lussen met vaste grenzen - Gebruik op FPC Win64
Str(Value:24, Text)zodra u meer dan 15 significante cijfers nodig heeft - Gebruik
Assert.AreEqual<NativeInt>voorLength- enCount-asserts, en compileer tests met dcc64 vóór het committen - Draai na elke wijziging aan een parser of renderer de volledige regressiesuite op Win32 én Win64, niet maar één van de twee
De library-side fixes die hier worden beschreven zitten allemaal sinds v2.770.169 in HotPDF, dus SVG-import, XPS-conversie, transparency-rendering en JSON-jobafhandeling gedragen zich nu op Win64 hetzelfde als op Win32. Genereert of verwerkt u PDF-bestanden vanuit Delphi of C++Builder voor beide platforms, dan staan de downloads en de volledige functielijst op de pagina van de HotPDF Delphi PDF-component