Codul Delphi Win64 poate eșua acolo unde aceeași sursă rulează curat pe Win32, iar componenta HotPDF Delphi PDF a lovit cinci asemenea cazuri în timpul unei treceri recente de întărire: Power(10, N) legat la overload-ul Single, o buclă while care citește un TList.Count învechit, o margine High(Int64) care rotunjește în sus la 2^63, text float de 15 cifre pe FPC și asserții de test care încetează să se compileze
Niciunul nu apare dacă doar construiți și testați Win32, exact felul în care au scăpat înăuntru. Cazurile de mai jos vin din importatoarele SVG și XPS ale HotPDF, din renderer-ul lui de pagini și din cititorul lui de joburi JSON, iar rezultatele numerice citate au fost reproduse cu mici programe sondă construite pentru Win32 și Win64. Dacă mutați o bază de cod Delphi pe 64 de biți, fiecare merită un grep
De ce dă Power(10, 100) overflow doar pe Win64?
Pe Win64, System.Math.Power(10, N) cu argumente întregi se rezolvă la overload-ul Single, deci rezultatul e calculat și întors în precizie single și orice peste aproximativ 3.4E38 dă overflow. Pe Win32 același apel se leagă la overload-ul Extended și rulează pe FPU x87 cu precizie pe 80 de biți, deci Power(10, 100) e pur și simplu 1E100
System.Math declară Power pentru Extended, Double și Single, plus o familie IntPower potrivită pe care Power o apelează când exponentul e număr întreg. Pe Win64, Extended e doar un alias pentru Double (SizeOf(Extended) = 8), iar pentru două argumente întregi compilatorul alege versiunea Single. Trădarea e precizia, nu doar overflow-ul: pe Win64, Power(10, 20) întoarce 1.0000000200408773E20, care e exact Single(1E20). Un rezultat Double s-ar tipări ca 1E20. Am văzut aceeași legare cu fiecare compilator Win64 pe care l-am încercat, de la Delphi 10.3 până la versiunea de compilator 37.0
Ce se întâmplă apoi depinde de masca de excepții în virgulă mobilă. Delphi 12 și mai nou mască toate excepțiile FP implicit, deci overflow-ul e tăcut: Power(10, 100) întoarce +Inf, iar Power(10, -100) întoarce 0. Delphi 11 și mai vechi lasă exOverflow nemascat, iar același apel ridică EOverflow. Aplicațiile care setează masca singure, iar DLL-urile încărcate în asemenea gazde, primesc comportamentul pe care l-a ales gazda, motiv pentru care o bibliotecă nu poate presupune niciunul dintre rezultate
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Win32 tipărește 1E20; Win64 tipărește 1.0000000200408773E20 (overload Single)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Reproduce ce face Delphi 11, sau un host cu setări FP stricte
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow; Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Demascarea lui exOverflow și exInvalidOp pe durata unui test e calea cea mai ieftină de a vedea ce vede un compilator mai vechi sau o gazdă strictă. Pe un compilator modern cu setările implicite bug-ul nu se blochează, ci produce infinități și zerouri, iar acelea sunt mult mai greu de observat într-un log de test. Restaurați masca anterioară în finally: masca e stare per thread, iar restul rulării de test moștenește ce lăsați în urmă
Cum a ajuns overload-ul în importul SVG și XPS din HotPDF
Cititoarele de căi SVG și XPS din HotPDF partajează un singur scanner de numere, iar scannerul acela scala mantisa cu Power(10, Exponent) odată ce citise un exponent. Orice SVG pasat către THotPDF.ImportSVGFormXObject (punctul de intrare din spatele importului SVG în PDF ca form XObject-uri refolosibile) și orice geometrie de cale tratată în timpul conversiei XPS și OpenXPS în PDF puteau deci hrăni apelului acela o coordonată precum 1e100 sau 5e99
v2.770.91 plafonase deja exponentul la 100 și respinsese valorile care ar trece de 1E300, ceea ce părea de ajuns: 1E100 e oriunde altundeva decât lângă limita Double de aproximativ 1.8E308. Pe Win64 dădea totuși overflow, pentru că calculul nu se întâmpla deloc în Double. Din v2.770.155 scanner-ul construiește puterea lui 10 singur, iar numere precum 1e-100, sau o mantisă lungă cu exponent mare negativ, se citesc la valoarea lor reală în loc să se colapseze în 0
O putere sigură a lui 10 pentru exponenți mărginiți
Când exponentul e mărginit, cea mai sigură putere a lui 10 e una pe care o construiți singuri cu înmulțiri Double. O buclă de cel mult 100 de înmulțiri nu costă nimic pe lângă scanarea textului din jur, nu produce niciodată un intermediar mai mare decât scara finală și se poartă identic pe Win32, Win64 și 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;
// Respinge rezultatele care ar părăsi intervalul 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; // nu depășește niciodată 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // împarte: 1E-100 n-are Double exact
Result := True;
end;
Trei detalii cară greutatea. Verificarea de interval folosește două comparații în loc de Abs(Exponent) <= 100, pentru că Abs(Low(Integer)) e încă negativ și ar trece direct prin ea. Exponenții negativi împart la scară în loc să înmulțească cu un 1E-100 precalculat, care n-are un Double exact și ar adăuga încă un pas de rotunjire. Iar pre-verificarea cu Log10 refuză rezultatele din afara intervalului Double înainte ca înmulțirea să aibă șansa de a da overflow
Fii clar la ce renunță bucla. Puterile lui 10 până la 1E22 sunt exacte în Double; dincolo de asta fiecare înmulțire rotunjește, iar după 100 dintre ele scara stă la câteva unități din ultima poziție față de 1E100 corect rotunjit. Pentru coordonate de desenare asta e invizibil. Pentru o conversie generală de la text la double care trebuie să reproducă fiecare valoare bit cu bit, nu e de ajuns, și aveți nevoie de un algoritm de conversie corect rotunjit în schimb
Când dcc64 citește un TList.Count învechit într-o buclă while
Am observat compilatorul Win64 (dcc64, versiune de compilator 37.0) genera cod pentru o buclă while List.Count > Start do care ștergea de la capătul listei și compara contra unui temporar pe stack în loc să recitească Count. Rescrierea care l-a reparat a fost o buclă for ... downto, ale cărei margini sunt evaluate prin definiție exact o dată
Bucla a ajuns în v2.769.3, care a învățat codul de transparency-group al renderer-ului să țină în viață soft mask-urile create în interiorul unui grup peste o randare în două treceri și să le elibereze după. Curățarea stătea într-un bloc finally după o buclă for de una sau două treceri, în interiorul buclei per-tile. Redusă la forma ei, înainte și după arată așa:
// Forma pe care am văzut-o compilată greșit de dcc64 (versiune compilator 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;
// Înlocuire: marginile sunt evaluate o dată, niciun temporar care să îmbătrânească
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // TList.Count e NativeInt din Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
În codul Win64 generat, Count-ul din condiția buclei și Count-ul citit în interiorul corpului partajau un singur slot de stack. Condiția compara contra slotului aceluia la intrare, înainte ca ceva să-l fi scris, și nimic nu-l reîmprospăta după Delete. Când un grup nu crease niciun soft mask propriu, corpul rula oricum și cerea unei liste goale elementul -1, deci în build-urile pe 64 de biți fiecare pagină care conținea un asemenea transparency group eșua cu EListError. Codul Win32 pentru aceeași sursă era corect, iar v2.770.1 a înlocuit bucla
Nu am redus asta la o reproducere minimală, iar o buclă mică de sine stătătoare precum DropMasksWhile s-ar putea foarte bine să se compileze corect; try/finally-ul din jur și buclele imbricate par să conteze. Tratați-o ca generare de cod observată pe o singură versiune de compilator, nu ca pe un defect cunoscut al oricărui compilator Win64. Lecția practică e mai ieftină decât cauza rădăcină: o buclă a cărei condiție recitește numărul unei colecții în timp ce corpul o micșorează merită rescrisă ca for ... downto cu margini fixe, iar schimbările la renderer au nevoie de o rulare completă de teste Win64, nu doar Win32
Localizarea unei blocări pe care o arată doar un build Win64 optimizat
Eșecul se reproducea doar în build-ul Win64 optimizat, deci locația a venit din unelte din afara IDE. Un mic program sondă a înregistrat un handler de excepții vectored cu AddVectoredExceptionHandler, a capturat stack-ul la prima excepție cu RtlCaptureStackBackTrace și a tradus adresele de retur în nume de funcții folosind fișierul de mapă detaliat pe care linker-ul îl scrie cu -GD. Dezasamblarea funcției aceleia a arătat apoi comparația citind un slot de stack, [rbp+0x298], scris doar în interiorul corpului buclei. Asta e nivelul de evidență pe care îl vreți înainte să dați vina pe un compilator, și a luat mai puțin timp decât pas cu pas printr-un build de release
De ce High(Int64) nu e o margine superioară sigură pentru un Double?
Un Double nu poate reprezenta High(Int64): convertirea lui 9223372036854775807 în Double rotunjește în sus la exact 2^63, unul peste cel mai mare Int64. Pe Win64 conversia aceea se întâmplă în interiorul comparației în sine, deci D <= High(Int64) e True pentru D = 2^63, iar Round-ul sau Trunc-ul care urmează dă overflow
Win32 ascunde asta din același motiv pentru care a ascuns problema Power. Comparația rulează în precizie Extended pe 80 de biți, cu mantisă pe 64 de biți, unde High(Int64) e exact, iar 2^63 compară corect ca mai mare. Win64 nu are un tip mai lat la care să cadă înapoi. Conversia din afara intervalului nu e nici ea drăguță: în testele noastre Win64 Round(2^63) întorcea Low(Int64), o inversare de semn tăcută, indiferent dacă exInvalidOp era mascat sau nu. Win32 întoarce aceeași valoare când e mascat și ridică EInvalidOp când e nemascat
| Expresie | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), excepții mascate (implicit Delphi 12+) | 1E100 | +Inf |
Power(10, 100), exOverflow nemascat | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp nemascat | EInvalidOp | Low(Int64) |
HotPDF a întâlnit asta în cititorul JSON din spatele valorilor de job de document ale lui. JSON nu pune nicio limită de interval pe numere, iar vechiul serializer transforma orice valoare cu Frac(Value) = 0 într-un întreg cu Round, deci un 1e19 perfect legal devenea ori un întreg greșit, ori o excepție, în funcție de mască. Din v2.770.169 un număr întreg e scris ca întreg doar când încap în Int64, tot restul își păstrează textul în virgulă mobilă, iar getter-ele de întreg întorc implicitul apelantului pentru valorile din afara intervalului, în locul unuia cu wrap
const
TwoPow63 = 9223372036854775808.0; // 2^63, exact în Double și 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
// Apelanții resping întâi NaN și infinitățile: JSON nu are ortografie pentru ele
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // FPC Win64 ffGeneral se oprește la 15 cifre
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Marginea superioară e literalul 9223372036854775808.0 cu un < strict. Constanta aceea e 2^63, exactă și în Double, și în Extended, deci comparația înseamnă același lucru pe orice platformă. Marginea inferioară poate folosi >= pentru că -2^63 e exact Low(Int64). Testarea lui IsNan și IsInfinite mai întâi, cu evaluare short-circuit, ține NaN-ul și infinitățile departe de Frac și de comparații, care pot ridica EInvalidOp când gazda l-a demascat
Câte cifre îți dă de fapt float-spre-text pe Win64?
Mai puține decât ceri, la două compilatoare din trei. FloatToStrF(Value, ffGeneral, 17, 0) din Free Pascal 3.3.1 pe Win64 se oprește la 15 cifre semnificative, deci 1/3 revine ca 0.333333333333333 și două valori Double diferite se pot serializa în text identic. Str(Value:24, Text) urmat de Trim produce 17 cifre semnificative în notație științifică, 3.3333333333333331E-001 pentru aceeași valoare, și scrie mereu un punct ca separator zecimal, indiferent de locale. Dacă HotPDF pe FPC face parte din matricea de build, notele de suport HotPDF Free Pascal și Lazarus Win64 acoperă restul diferențelor de platformă
Delphi acceptă cererea de 17 cifre, dar cele două ținte Delphi încă dezacordă la output: FloatToStrF(0.1, ffGeneral, 17, 0) dă 0.10000000000000001 pe Win32 și 0.1 pe Win64. RTL-ul Win64 poate introduce și el o eroare de rotunjire a ultimei cifre atât la formatare, cât și la parsare, deci mai multe cifre strâng decalajul fără să garanteze că fiecare tipar de biți Double supraviețuiește unui dus-întors prin text. Documentația HotPDF nu face o asemenea promisiune, iar a dvs. nu ar trebui să o facă, decât dacă livrați un formatter și un parser corect rotunjite ale dvs. Pasați TFormatSettings.Invariant sau înlocuiți separatorul singuri pe versiunile Delphi mai vechi, astfel încât un locale german sau francez să nu scrie o virgulă în JSON
De ce Assert.AreEqual încetează să se compileze pe Win64?
Assert.AreEqual(3, Length(Arr)) pe un tablou dinamic compilează pentru Win32 și eșuează pentru Win64 cu E2532, „Couldn't infer generic type argument from different argument types”, pentru că Length al unui tablou dinamic întoarce NativeInt pe Win64. Cu un literal Integer de o parte și un NativeInt pe 64 de biți de cealaltă, Assert.AreEqual<T>-ul generic din DUnitX nu se poate hotărî pe un singur T, iar build-ul se oprește
TList.Count declanșează aceeași eroare din Delphi 12, unde proprietatea a devenit NativeInt; Delphi 11 o declară încă ca Integer. Length al unui string întoarce Integer pe ambele platforme și nu e afectat, motiv pentru care eroarea apare în unele unități de test și nu în altele. Scrieți argumentul de tip explicit, Assert.AreEqual<NativeInt>(3, Length(Arr)), și compilați proiectul de teste cu dcc64 înainte de comitere. O suită care construiește doar pentru Win32 nu vă va spune că build-ul ei Win64 e stricat până când nu încearcă altcineva
Lista de verificare de portare Win64 pentru codul numeric Delphi
- Căutați apeluri
Power(șiIntPower(cu argumente întregi; pasați valori de tipDoublesau construiți singuri puteri mărginite ale lui 10 - Rulați testele numerice cel puțin o dată cu
exOverflowșiexInvalidOpeliminate prinSetExceptionMask, atât pe Win32, cât și pe Win64 - Scrieți marginea superioară
Int64ca< 9223372036854775808.0, niciodată<= High(Int64)și respingeți NaN și infinitățile înaintea oricărei comparații - Nu convertiți un număr parsat în
Int64doar pentru căFrace 0; numerele JSON pot fi mult mai mari - Rescrieți buclele
whilecare recitescCountîn timp ce șterg elemente ca buclefor ... downtocu margini fixe - Pe FPC Win64, folosiți
Str(Value:24, Text)când aveți nevoie de mai mult de 15 cifre semnificative - Folosiți
Assert.AreEqual<NativeInt>pentru asserții peLengthșiCountși compilați testele cu dcc64 înainte de comitere - După orice schimbare la un parser sau la un renderer, rulați suita completă de regresie pe Win32 și Win64, nu doar pe una dintre ele
Reparările din partea bibliotecii descrise aici sunt toate în HotPDF din v2.770.169, deci importul SVG, conversia XPS, randarea transparenței și tratarea joburilor JSON se poartă acum la fel pe Win64 ca pe Win32. Dacă generați sau procesați fișiere PDF din Delphi sau C++Builder pentru ambele platforme, pagina componentei HotPDF Delphi PDF are descărcările și lista completă de funcții