Κώδικας Delphi Win64 μπορεί να αποτύχει εκεί όπου η ίδια πηγή τρέχει καθαρά σε Win32, και το HotPDF Delphi PDF component χτύπησε πέντε τέτοιες περιπτώσεις σε πρόσφατο πέρασμα hardening: το Power(10, N) που δένει στο overload Single, βρόχο while που διαβάζει βρώμικο TList.Count, όριο High(Int64) που στρογγυλοποιεί προς τα πάνω σε 2^63, κείμενο float 15 ψηφίων σε FPC, και asserts test που σταματούν τη μεταγλώττιση
Κανένα από αυτά δεν εμφανίζεται αν χτίζετε και τεστάρετε μόνο Win32, που είναι ακριβώς ο τρόπος που γλίστρησαν μέσα. Οι περιπτώσεις παρακάτω προέρχονται από τους importers SVG και XPS του HotPDF, τον renderer σελίδων του και τον reader JSON job του, και τα αριθμητικά αποτελέσματα που παραθέτονται αναπαράχθηκαν με μικρά probe προγράμματα χτισμένα για Win32 και Win64. Αν μετακινείτε codebase Delphi σε 64 bit, το καθένα αξίζει ένα grep
Γιατί το Power(10, 100) κάνει overflow μόνο σε Win64;
Σε Win64, το System.Math.Power(10, N) με ακέραια ορίσματα λύνεται στο overload Single, οπότε το αποτέλεσμα υπολογίζεται και επιστρέφεται σε single precision και οτιδήποτε πάνω από περίπου 3.4E38 κάνει overflow. Σε Win32 η ίδια κλήση δένει στο overload Extended και τρέχει στη FPU x87 με ακρίβεια 80 bit, οπότε το Power(10, 100) είναι απλώς 1E100
Το System.Math δηλώνει Power για Extended, Double και Single, συν matching οικογένεια IntPower που καλεί το Power όταν ο εκθέτης είναι ακέραιος αριθμός. Σε Win64, το Extended είναι μόνο alias για Double (SizeOf(Extended) = 8), και για δύο ακέραια ορίσματα ο μεταγλωττιστής διαλέγει την εκδοχή Single. Το αποκάλυμμα είναι η ακρίβεια, όχι μόνο το overflow: σε Win64, το Power(10, 20) επιστρέφει 1.0000000200408773E20, που είναι ακριβώς Single(1E20). Αποτέλεσμα Double θα τυπωνόταν ως 1E20. Είδαμε το ίδιο δέσιμο με κάθε μεταγλωττιστή Win64 που δοκιμάσαμε, από Delphi 10.3 έως έκδοση μεταγλωττιστή 37.0
Αυτό που συμβαίνει μετά εξαρτάται από τη μάσκα εξαιρέσεων κινητής υποδιαστολής. Το Delphi 12 και μεταγενέστερα κρύβουν όλες τις εξαιρέσεις κινητής υποστολής by default, οπότε το overflow είναι αθόρυβο: το Power(10, 100) επιστρέφει +Inf και το Power(10, -100) επιστρέφει 0. Το Delphi 11 και νωρίτερα αφήνουν το exOverflow αποκάλυπτο, και η ίδια κλήση πετά EOverflow. Εφαρμογές που θέτουν οι ίδιες τη μάσκα, και DLLs φορτωμένα σε τέτοιους hosts, παίρνουν όποια συμπεριφορά διάλεξε ο host, γι αυτό μια βιβλιοθήκη δεν μπορεί να υποθέσει ούτε το ένα ούτε το άλλο αποτέλεσμα
uses
System.SysUtils, System.Math;
procedure ShowPowerOverload;
var
N: Integer;
OldMask: TArithmeticExceptionMask;
begin
N := 20;
// Το Win32 τυπώνει 1E20· το Win64 τυπώνει 1.0000000200408773E20 (overload Single)
Writeln(FloatToStrF(Power(10, N), ffGeneral, 17, 0));
// Αναπαραγωγή ό,τι κάνει το Delphi 11, ή host με αυστηρές ρυθμίσεις FP
OldMask := GetExceptionMask;
SetExceptionMask(OldMask - [exOverflow, exInvalidOp]);
try
N := 100;
Writeln(Power(10, N)); // Win64: EOverflow· Win32: 1E100
finally
SetExceptionMask(OldMask);
end;
end;
Το αποκάλυμμα του exOverflow και του exInvalidOp για τη διάρκεια ενός test είναι ο φθηνότερος τρόπος να δείτε ό,τι βλέπει παλαιότερος μεταγλωττιστής ή αυστηρός host. Σε σύγχρονο μεταγλωττιστή με προεπιλεγμένες ρυθμίσεις το bug δεν καταρρέει, παράγει infinities και μηδενικά, και εκείνα είναι πολύ πιο δύσκολο να εντοπιστούν σε log test. Επαναφέρετε την προηγούμενη μάσκα σε finally: η μάσκα είναι κατάσταση ανά νήμα, και η υπόλοιπη εκτέλεση test κληρονομεί ό,τι αφήσετε πίσω
Πώς το overload έφτασε στην εισαγωγή SVG και XPS του HotPDF
Οι readers διαδρομών SVG και XPS του HotPDF μοιράζονται έναν σαρωτή αριθμών, και εκείνος ο σαρωτής κλιμακώσε τον mantissa με Power(10, Exponent) μόλις είχε διαβάσει εκθέτη. Οποιοδήποτε SVG περασμένο στο THotPDF.ImportSVGFormXObject (το σημείο εισόδου πίσω από την εισαγωγή SVG σε PDF ως επαναχρησιμοποιήσιμα form XObjects), και οποιαδήποτε γεωμετρία διαδρομής που χειρίζεται κατά την μετατροπή XPS και OpenXPS σε PDF, μπορούσε επομένως να ταΐσει συντεταγμένη όπως 1e100 ή 5e99 σε εκείνη την κλήση
Το v2.770.91 είχε ήδη περικόψει τον εκθέτη στα 100 και απέρριπτε τιμές που θα περνούσαν το 1E300, που φαινόταν αρκετό: το 1E100 δεν φτάνει ούτε κοντά στο όριο Double περίπου 1.8E308. Σε Win64 εξακολουθούσε να κάνει overflow, επειδή ο υπολογισμός δεν γινόταν ποτέ σε Double καθόλου. Από το v2.770.155 ο σαρωτής χτίζει ο ίδιος τη δύναμη του δέκα, και αριθμοί όπως 1e-100, ή μακρύς mantissa με μεγάλο αρνητικό εκθέτη, διαβάζονται στην πραγματική τους τιμή αντί να καταρρέουν σε 0
Μια ασφαλής δύναμη του δέκα για οριοθετημένους εκθέτες
Όταν ο εκθέτης είναι οριοθετημένος, η ασφαλέστερη δύναμη του δέκα είναι αυτή που χτίζετε μόνοι σας με πολλαπλασιασμό Double. Βρόχος το πολύ 100 πολλαπλασιασμών δεν κοστίζει τίποτα δίπλα στη σάρωση του κειμένου γύρω του, δεν παράγει ποτέ ενδιάμεσο μεγαλύτερο από την τελική κλίμακα, και συμπεριφέρεται πανομοιότυπα σε Win32, Win64 και 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;
// Απορρίψτε αποτελέσματα που θα έφευγαν από το εύρος 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; // δεν υπερβαίνει ποτέ το 1E100
if Exponent >= 0 then
Scaled := Value * Scale
else
Scaled := Value / Scale; // διαίρεση: το 1E-100 δεν έχει ακριβές Double
Result := True;
end;
Τρεις λεπτομέρειες κουβαλάνε το βάρος. Ο έλεγχος εύρους χρησιμοποιεί δύο συγκρίσεις αντί για Abs(Exponent) <= 100, επειδή το Abs(Low(Integer)) εξακολουθεί να είναι αρνητικό και θα πέρνασε κατευθείαν. Οι αρνητικοί εκθέτες διαιρούν με την κλίμακα αντί να πολλαπλασιάζουν με προϋπολογισμένο 1E-100, που δεν έχει ακριβές Double και θα πρόσθετε ένα βήμα στρογγυλοποίησης ακόμα. Και ο προέλεγχος Log10 απορρίπτει αποτελέσματα έξω από το εύρος Double πριν ο πολλαπλασιασμός προλάβει να κάνει overflow
Να είστε σαφείς για το τι παρατά ο βρόχος. Δυνάμεις του δέκα έως το 1E22 είναι ακριβείς σε Double· μετά από εκεί κάθε πολλαπλασιασμός στρογγυλοποιεί, και μετά από 100 από εκείνους η κλίμακα κάθεται λίγες μονάδες στην τελευταία θέση μακριά από το σωστά στρογγυλοποιημένο 1E100. Για συντεταγμένες σχεδίασης αυτό είναι αόρατο. Για μετατροπή γενικής χρήσης text-to-double που πρέπει να αναπαράγει κάθε τιμή bit προς bit, δεν είναι αρκετό, και θέλετε αλγόριθμο σωστά στρογγυλοποιημένης μετατροπής αντί
Όταν το dcc64 διαβάζει βρώμικο TList.Count σε βρόχο while
Παρατηρήσαμε τον μεταγλωττιστή Win64 (dcc64, έκδοση μεταγλωττιστή 37.0) να παράγει κώδικα για βρόχο while List.Count > Start do που διέγραφε από το τέλος της λίστας και συνέκρινε απέναντι σε προσωρινό stack αντί να ξαναδιαβάσει το Count. Η επανεγγραφή που το διόρθωσε ήταν βρόχος for ... downto, του οποίου τα όρια αποτιμώνται ακριβώς μία φορά εξ ορισμού
Ο βρόχος έφτασε στο v2.769.3, που δίδαξε στον κώδικα ομάδων διαφάνειας του renderer να κρατά ζωντανά soft masks που δημιουργήθηκαν μέσα από render δύο περασμάτων και να τα ελευθερώνει μετά. Ο καθαρισμός καθόταν σε block finally μετά από βρόχο for ενός ή δύο περασμάτων, μέσα στον βρόχο ανά tile. Αναχθεί στο σχήμα του, το πριν και το μετά μοιάζουν έτσι:
// Το σχήμα που είδαμε να μεταγλωττίζεται λάθος από το dcc64 (έκδοση μεταγλωττιστή 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;
// Αντικατάσταση: τα όρια αποτιμώνται μία φορά, κανένα προσωρινό να γίνει βρώμικο
procedure DropMasksFrom(Masks: TList; Start: Integer);
var
Idx: NativeInt; // το TList.Count είναι NativeInt από το Delphi 12
begin
for Idx := Masks.Count - 1 downto Start do
begin
TObject(Masks[Idx]).Free;
Masks.Delete(Idx);
end;
end;
Στον παραγόμενο κώδικα Win64, το Count στη συνθήκη του βρόχου και το Count που διαβαζόταν μέσα στο σώμα μοιράζονταν μία θέση stack. Η συνθήκη συνέκρινε απέναντι σε εκείνη τη θέση στην είσοδο, πριν οτιδήποτε την είχε γράψει, και τίποτα δεν την ανανέωνε μετά το Delete. Όταν μια ομάδα δεν είχε δημιουργήσει δικά της soft masks, το σώμα έτρεχε ούτως ή άλλως και ζητούσε από κενή λίστα το item -1, οπότε σε builds 64 bit κάθε σελίδα που περιείχε τέτοια ομάδα διαφάνειας απέτυχε με EListError. Ο κώδικας Win32 για την ίδια πηγή ήταν σωστός, και το v2.770.1 αντικατέστησε τον βρόχο
Δεν το έχουμε ανάγει σε ελάχιστη αναπαραγωγή, και μικρός αυτόνομος βρόχος όπως ο DropMasksWhile μπορεί κάλλιστα να μεταγλωττίζεται σωστά· τα περιβάλλοντα try/finally και οι εμφωλευμένοι βρόχοι φαίνεται να μετράνε. Μεταχειριστείτε το ως παραγωγή κώδικα που παρατηρήσαμε σε μία έκδοση μεταγλωττιστή, όχι ως γνωστό ελάττωμα κάθε μεταγλωττιστή Win64. Το πρακτικό μάθημα είναι φθηνότερο από τη ρίζα: βρόχος του οποίου η συνθήκη ξαναδιαβάζει το πλήθος μιας συλλογής ενώ το σώμα μικραίνει εκείνη τη συλλογή αξίζει επανεγγραφή ως σταθερών ορίων for ... downto, και αλλαγές renderer θέλουν πλήρη εκτέλεση test Win64, όχι μόνο Win32
Εντοπισμός crash που δείχνει μόνο βελτιστοποιημένο build Win64
Η αποτυχία αναπαράγονταν μόνο στο βελτιστοποιημένο build Win64, οπότε η τοποθεσία ήρθε από εργαλεία έξω από το IDE. Ένα μικρό probe πρόγραμμα κατέγραψε vectored exception handler με AddVectoredExceptionHandler, συνέλαβε τη στοίβα στην πρώτη εξαίρεση με RtlCaptureStackBackTrace, και μετέφρασε τις διευθύνσεις επιστροφής σε ονόματα συναρτήσεων χρησιμοποιώντας το αναλυτικό map αρχείο που γράφει ο linker με -GD. Η αποσυναρμολόγηση εκείνης της συνάρτησης έδειξε μετά τη σύγκριση να διαβάζει θέση stack, [rbp+0x298], που γράφονταν μόνο μέσα στο σώμα του βρόχου. Εκείνο είναι το επίπεδο αποδείξεων που θέλετε πριν κατηγορήσετε έναν μεταγλωττιστή, και πήρε λιγότερο χρόνο από το stepping μέσα από release build
Γιατί το High(Int64) δεν είναι ασφαλές άνω όριο για Double;
Ένα Double δεν μπορεί να αναπαραστήσει το High(Int64): η μετατροπή του 9223372036854775807 σε Double στρογγυλοποιεί προς τα πάνω σε ακριβώς 2^63, ένα πάνω από τον μεγαλύτερο Int64. Σε Win64 εκείνη η μετατροπή συμβαίνει μέσα στην ίδια τη σύγκριση, οπότε το D <= High(Int64) είναι True για D = 2^63, και το Round ή Trunc που ακολουθεί κάνει overflow
Το Win32 κρύβει αυτό για τον ίδιο λόγο που έκρυβε το πρόβλημα Power. Η σύγκριση τρέχει σε ακρίβεια Extended 80 bit με mantissa 64 bit, όπου το High(Int64) είναι ακριβές και το 2^63 συγκρίνεται σωστά ως μεγαλύτερο. Το Win64 δεν έχει ευρύτερο τύπο να πέσει πίσω. Η εκτός εύρους μετατροπή δεν είναι όμορφη ούτε αυτή: στα Win64 test μας το Round(2^63) επέστρεφε Low(Int64), αθόρυβη αντιστροφή πρόσημου, είτε το exInvalidOp ήταν κρυμμένο είτε όχι. Το Win32 επιστρέφει την ίδια τιμή όταν είναι κρυμμένο και πετά EInvalidOp όταν είναι αποκαλυμμένο
| Έκφραση | Win32 | Win64 |
|---|---|---|
Power(10, N), N = 20 | 1E20 | 1.0000000200408773E20 |
Power(10, 100), εξαιρέσεις κρυμμένες (προεπιλογή Delphi 12+) | 1E100 | +Inf |
Power(10, 100), exOverflow αποκαλυμμένο | 1E100 | EOverflow |
D <= High(Int64), D = 2^63 | False | True |
Round(2^63), exInvalidOp αποκαλυμμένο | EInvalidOp | Low(Int64) |
Το HotPDF συνάντησε αυτό στον reader JSON πίσω από τις τιμές document job του. Το JSON δεν θέτει όριο εύρους σε αριθμούς, και ο παλιός serializer γύρνε κάθε τιμή με Frac(Value) = 0 σε ακέραιο με Round, οπότε ένα απολύτως νόμιμο 1e19 γινόταν είτε λάθος ακέραιος είτε exception, ανάλογα με τη μάσκα. Από το v2.770.169 ακέραιος αριθμός γράφεται ως ακέραιος μόνο όταν χωράει σε Int64, όλα τα άλλα κρατούν το κινητού κειμένου τους, και οι getters ακεραίων επιστρέφουν την προεπιλογή του καλούντος για εκτός εύρους τιμές αντί για τυλιγμένη
const
TwoPow63 = 9223372036854775808.0; // 2^63, ακριβές σε Double και 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
// Οι καλούντες απορρίπτουν πρώτα NaN και infinities: το JSON δεν έχει ορθογραφία για εκείνα
if TryDoubleToInt64(Value, R) then
Result := IntToStr(R)
else
begin
{$IFDEF FPC}
Str(Value:24, Result); // το ffGeneral FPC Win64 σταματά στα 15 ψηφία
Result := Trim(Result);
{$ELSE}
Result := FloatToStrF(Value, ffGeneral, 17, 0, TFormatSettings.Invariant);
{$ENDIF}
end;
end;
Το άνω όριο είναι το literal 9223372036854775808.0 με αυστηρό <. Εκείνη η σταθερά είναι το 2^63, ακριβές σε Double και Extended, οπότε η σύγκριση σημαίνει το ίδιο πράγμα σε κάθε πλατφόρμα. Το κάτω όριο μπορεί να χρησιμοποιήσει >= επειδή το -2^63 είναι ακριβώς Low(Int64). Το test IsNan και IsInfinite πρώτα, με αποτίμηση short-circuit, κρατά NaN και infinities μακριά από Frac και τις συγκρίσεις, που μπορούν να πετάξουν EInvalidOp όταν ο host το έχει αποκαλύψει
Πόσα ψηφία δίνει πραγματικά η μετατροπή float-σε-κείμενο σε Win64;
Λιγότερα από όσα ζητάτε, σε δύο μεταγλωττιστές στους τρεις. Το FloatToStrF(Value, ffGeneral, 17, 0) του Free Pascal 3.3.1 σε Win64 σταματά στα 15 σημαντικά ψηφία, οπότε το 1/3 επιστρέφει ως 0.333333333333333 και δύο διαφορετικές τιμές Double μπορούν να σειριοποιηθούν σε πανομοιότυπο κείμενο. Το Str(Value:24, Text) ακολουθούμενο από Trim παράγει 17 σημαντικά ψηφία σε επιστημονική σημείωση, 3.3333333333333331E-001 για την ίδια τιμή, και γράφει πάντα τελεία ως δεκαδικό διαχωριστικό ανεξαρτήτως locale. Αν το HotPDF σε FPC είναι μέρος του build matrix σας, οι σημειώσεις υποστήριξης HotPDF Free Pascal και Lazarus Win64 καλύπτουν τις υπόλοιπες διαφορές πλατφόρμας
Το Delphi δέχεται το αίτημα 17 ψηφίων, αλλά οι δύο στόχοι Delphi εξακολουθούν να διαφωνούν στην έξοδο: το FloatToStrF(0.1, ffGeneral, 17, 0) δίνει 0.10000000000000001 σε Win32 και 0.1 σε Win64. Το RTL Win64 μπορεί επίσης να εισάγει σφάλμα στρογγυλοποίησης τελευταίου ψηφίου τόσο κατά τη μορφοποίηση όσο και κατά την ανάλυση, οπότε περισσότερα ψηφία στενεύουν το κενό χωρίς να εγγυώνται ότι κάθε bit μοτίβο Double επιβιώνει από text round trip. Η τεκμηρίωση του HotPDF δεν κάνει τέτοια υπόσχεση, και η δική σας δεν πρέπει ούτε αυτή εκτός αν παραδίδετε δικό σας σωστά στρογγυλοποιημένο formatter και parser. Περάστε TFormatSettings.Invariant, ή αντικαταστήστε τον διαχωριστικό μόνοι σας σε παλαιότερες εκδόσεις Delphi, ώστε γερμανικό ή γαλλικό locale να μην γράψει κόμμα μέσα σε JSON
Γιατί το Assert.AreEqual σταματά να μεταγλωττίζεται σε Win64;
Το Assert.AreEqual(3, Length(Arr)) πάνω σε δυναμικό πίνακα μεταγλωττίζεται για Win32 και αποτυγχάνει για Win64 με E2532, "Couldn't infer generic type argument from different argument types", επειδή το Length δυναμικού πίνακα επιστρέφει NativeInt σε Win64. Με literal Integer στη μία πλευρά και 64-bit NativeInt στην άλλη, το γενικό Assert.AreEqual<T> του DUnitX δεν μπορεί να κατασταλάξει σε μοναδικό T, και το build σταματά
Το TList.Count πυροδοτεί το ίδιο σφάλμα από το Delphi 12, όπου η ιδιότητα έγινε NativeInt· το Delphi 11 εξακολουθεί να το δηλώνει ως Integer. Το Length string επιστρέφει Integer και στις δύο πλατφόρμες και δεν επηρεάζεται, που είναι ο λόγος το σφάλμα εμφανίζεται σε μερικά test units και όχι σε άλλα. Γράψτε το όρισμα τύπου ρητά, Assert.AreEqual<NativeInt>(3, Length(Arr)), και μεταγλωττίστε το test project με dcc64 πριν το commit. Σουίτα που χτίζει μόνο για Win32 δεν θα σας πει ότι το build Win64 της είναι σπασμένο μέχρι κάποιος άλλος να το δοκιμάσει
Λίστα ελέγχου μεταφοράς Win64 για αριθμητικό κώδικα Delphi
- Ψάξτε για κλήσεις
Power(καιIntPower(με ακέραια ορίσματα· περάστε τιμές τύπουDoubleή χτίστε οριοθετημένες δυνάμεις του δέκα μόνοι σας - Τρέξτε αριθμητικά test τουλάχιστον μία φορά με
exOverflowκαιexInvalidOpαφαιρεμένα μέσωSetExceptionMask, και σε Win32 και σε Win64 - Γράψτε το άνω όριο
Int64ως< 9223372036854775808.0, ποτέ<= High(Int64), και απορρίψτε NaN και infinities πριν από οποιαδήποτε σύγκριση - Μην μετατρέψετε parsed αριθμό σε
Int64μόνο επειδή τοFracείναι 0· αριθμοί JSON μπορούν να είναι πολύ μεγαλύτεροι - Ξαναγράψτε βρόχους
whileπου ξαναδιαβάζουνCountενώ διαγράφουν items ως βρόχους σταθερών ορίωνfor ... downto - Σε FPC Win64, χρησιμοποιήστε
Str(Value:24, Text)όταν θέλετε πάνω από 15 σημαντικά ψηφία - Χρησιμοποιήστε
Assert.AreEqual<NativeInt>για assertsLengthκαιCount, και μεταγλωττίστε τα tests με dcc64 πριν το commit - Μετά από κάθε αλλαγή σε parser ή renderer, τρέξτε την πλήρη regression σουίτα σε Win32 και Win64, όχι μόνο σε έναν από τους δύο
Οι διορθώσεις πλευράς βιβλιοθήκης που περιγράφησαν εδώ είναι όλες στο HotPDF από το v2.770.169, οπότε η εισαγωγή SVG, η μετατροπή XPS, το rendering διαφάνειας και ο χειρισμός JSON job συμπεριφέρονται πλέον το ίδιο σε Win64 όπως σε Win32. Αν παράγετε ή επεξεργάζεστε PDF αρχεία από Delphi ή C++Builder και για τις δύο πλατφόρμες, η σελίδα HotPDF Delphi PDF component έχει τις λήψεις και την πλήρη λίστα χαρακτηριστικών