Ας πούμε ότι μια νυχτερινή υπηρεσία Delphi παράγει ένα XLSX ανά πελάτη, μερικές εκατοντάδες αρχεία, κάποια από αυτά πλάτους 400.000 γραμμών. Κάντε profiling και η έκπληξη σπάνια είναι ο βρόχος συμπλήρωσης κελιών. Είναι η κλήση SaveAs. Με τον προεπιλεγμένο writer, κάθε φύλλο εργασίας σειριοποιείται σε μία ενιαία συμβολοσειρά XML στη μνήμη πριν αυτή η συμβολοσειρά συμπιεστεί στο zip του OOXML, και για ένα ευρύ φύλλο η προσωρινή συμβολοσειρά μπορεί να ξεπεράσει κατά πολύ το μοντέλο κελιών από το οποίο κατασκευάστηκε. Έτσι, μια δουλειά που άνετα κατασκευάζει τα δεδομένα της και παραμένει στα 800 MB θα εκτοξευτεί πάνω από ένα όριο container 2 GB κατά την αποθήκευση, και ο OOM killer καταθέτει την αναφορά σφάλματος στις 03:00 όταν κανείς δεν παρακολουθεί. Το HotXLS, η εγγενής βιβλιοθήκη υπολογιστικών φύλλων της losLab για Delphi και C++Builder, έχει μια ιδιότητα στοχευμένη ακριβώς σε αυτή την αιχμή: την StreamingWrite. Γύρω της βρίσκονται δύο ακόμα μοχλοί που καθορίζουν αν ένας worker παρτίδας παραμένει μέσα στο όριο μνήμης και χρόνου του, δηλαδή τα callbacks εγγραφής σε επίπεδο γραμμής και ο τρόπος με τον οποίο συμπεριφέρεται η δεξαμενή στυλ μέσα σε έναν σφιχτό βρόχο
Τι προσωρινά αποθηκεύει η προεπιλεγμένη διαδρομή αποθήκευσης, και τι αλλάζει η StreamingWrite
Ο προεπιλεγμένος writer XLSX προτιμά την απλότητα. Αποδίδει το XML του φύλλου εργασίας πλήρως, και μετά παραδίδει την τελική συμβολοσειρά στον συμπιεστή zip. Αυτός είναι ο σωστός συμβιβασμός για τη συντριπτική πλειονότητα των βιβλίων εργασίας, όπου το XML ολόκληρου του φύλλου χωράει σε λίγα megabyte. Παύει να είναι σωστός όταν η σειριοποιημένη μορφή ενός φύλλου φτάνει σε εκατοντάδες megabyte. Το XML υπολογιστικών φύλλων είναι εκτενές: κάθε αριθμητικό κελί κοστίζει δεκάδες χαρακτήρες markup, και η συμβολοσειρά που τα κρατά όλα πρέπει να είναι συνεχόμενη. Σε ένα γράφημα μνήμης η υπογραφή είναι δύσκολο να περάσει απαρατήρητη. Ένα μακρύ επίπεδο οροπέδιο όσο γεμίζουν οι γραμμές, μετά μια απότομη τριγωνική αιχμή κατά τη διάρκεια της SaveAs, και μετά η κατάρρευση μόλις γίνει flush το zip
Ορίζοντας Book.StreamingWrite := True αλλάζετε την SaveAs σε έναν writer φύλλου εργασίας που εκπέμπει το XML του φύλλου απευθείας στη ροή zip καθώς παράγεται. Η ενδιάμεση συμβολοσειρά δεν δεσμεύεται ποτέ, και η τριγωνική αιχμή ισοπεδώνεται μέσα στον θόρυβο
Να είστε ακριβείς για το τι πραγματικά σας προσφέρει αυτό, επειδή η υπερεκτίμησή του οδηγεί σε λάθος σχέδια χωρητικότητας. Η σημαία αλλάζει μόνο τη διαδρομή αποθήκευσης. Η κατασκευή του βιβλίου εργασίας εξακολουθεί να δεσμεύει το πλήρες μοντέλο κελιών στη μνήμη, οπότε το οροπέδιο κατά τη φάση συμπλήρωσης είναι ακριβώς τόσο ψηλό όσο πριν. Αυτό που εξαφανίζεται είναι η αιχμή σειριοποίησης που παλιά στοιβαζόταν πάνω σε αυτό το οροπέδιο κατά την αποθήκευση, και για μια δουλειά που γεμίζει 400 χιλιάδες γραμμές αυτή η αιχμή είναι συχνά ολόκληρη η διαφορά ανάμεσα στο να χωρέσετε μέσα σε ένα όριο μνήμης και στο να το ξεπεράσετε. Η ιδιότητα έχει προεπιλογή False για να διατηρήσει την ιστορική συμπεριφορά, οπότε η ενεργοποίησή της είναι μία ρητή γραμμή που γράφετε σκόπιμα
Μια μαζική εξαγωγή με τη σημαία ενεργοποιημένη
Book := TXLSXWorkbook.Create;
try
BoldIdx := Book.Fonts.Add('Calibri', 11, True, False); // δείκτης δεξαμενής, βάσει 0
Sheet := Book.Sheets.Add('Bulk');
for R := 1 to 100000 do
begin
Sheet.Cells[R, 1].Value := R;
Sheet.Cells[R, 2].Value := 'Row ' + IntToStr(R);
Sheet.Cells[R, 3].Value := R * 1.5;
if (R mod 1000) = 0 then
Sheet.Cells[R, 2].FontIndex := BoldIdx + 1; // βάσει 1 στο κελί
end;
Book.StreamingWrite := True; // στέλνει το XML του φύλλου απευθείας στο zip ως ροή
Book.SaveAs('bulk.xlsx');
finally
Book.Free;
end;
Η Cells[R, C] δημιουργεί κελιά κατά παραγγελία, κάτι που κρατά καθαρό το σώμα του βρόχου. Αξίζει να απομνημονεύσετε δύο όρια πλέγματος: 1.048.576 γραμμές και 16.384 στήλες, εκτεθειμένα ως XlsxMaxRow και XlsxMaxCol. Μια τροφοδοσία δεδομένων που ξεπερνά το όριο γραμμών πρέπει να διαχωριστεί σε φύλλα μέσα στον δικό σας κώδικα. Τίποτα παρακάτω δεν παρατηρεί την υπέρβαση ούτε τη διορθώνει για εσάς, και το αρχείο απλώς καταλήγει περικομμένο στο όριο
Συμπλήρωση γραμμών χωρίς επιβάρυνση Variant ανά κελί
Κάθε ανάθεση Cells[R, C].Value πληρώνει το κόστος μιας αναζήτησης κελιού και μιας μετατροπής Variant. Σε δέκα χιλιάδες γραμμές κανείς δεν το προσέχει. Σε ένα εκατομμύριο γραμμές με είκοσι στήλες η καθεμία, αυτή η επιβάρυνση ανά κλήση γίνεται το κυρίαρχο κόστος της φάσης συμπλήρωσης, και ο profiler θα δείξει κατευθείαν εκεί. Οι διεπαφές παρτίδας σάς επιτρέπουν αντ' αυτού να παραδίδετε στον writer μια ολόκληρη γραμμή τη φορά. Η WriteRows οδηγεί ένα callback που παρέχει μία γραμμή ανά κλήση:
procedure TBulkExporter.FillRow(Sender: TObject; SheetIndex, Row, FirstCol,
LastCol: Integer; var Values: Variant; var Skip: Boolean;
var Cancel: Boolean);
begin
if not FReader.Next then
begin
Cancel := True; // η πηγή δεδομένων εξαντλήθηκε: σταματήστε καθαρά
Exit;
end;
Values := VarArrayCreate([FirstCol, LastCol], varVariant);
Values[FirstCol] := FReader.RecordId;
Values[FirstCol + 1] := FReader.CustomerName;
Values[FirstCol + 2] := FReader.Amount;
end;
// συμπληρώστε τις γραμμές 2..100001, στήλες A..C, αντλώντας από τον reader
Sheet.WriteRows(2, 1, 100001, 3, FillRow);
Η σημαία Cancel είναι αυτή που μετατρέπει ένα σταθερό εύρος γραμμών σε «έως N γραμμές», κάτι που είναι το φυσικό σχήμα όταν ο αριθμός γραμμών προέρχεται από ένα ερώτημα που δεν έχετε ολοκληρώσει ακόμα. Η Skip είναι η πιο ελαφριά επιλογή: αφήνει μια μεμονωμένη γραμμή κενή χωρίς να σταματά την εκτέλεση. Πέρα από τη συμπλήρωση κελιών, το callback αποδεικνύεται καλό σπίτι για τις λειτουργικές ανάγκες που διαφορετικά προσαρτώνται σε έναν βρόχο συμπλήρωσης με αδέξιο τρόπο. Ένας μετρητής προόδου που χτυπά κάθε χίλιες γραμμές, ένα token ακύρωσης που ελέγχεται από τον προγραμματιστή εργασιών, ένας περιοριστής ρυθμού στις αναγνώσεις από την πηγαία βάση δεδομένων: όλα αυτά ζουν σε ένα μέρος αντί να είναι σκορπισμένα μέσα στον κώδικα εγγραφής κελιών. Στην πλευρά της ανάγνωσης, οι ForEachRow και ForEachCell αντικατοπτρίζουν το ίδιο μοτίβο, κάτι που έχει σημασία όταν μια δουλειά παρτίδας τόσο καταναλώνει όσο και παράγει μεγάλα αρχεία
Οι δεξαμενές στυλ ανταμείβουν την ανύψωση εκτός βρόχου
Το μοντέλο μορφοποίησης XLSX είναι ένα σύνολο κοινών δεξαμενών. Οι Fonts.Add, Fills.AddSolid και Borders.Add επιστρέφουν όλες έναν δείκτη δεξαμενής βάσει 0, και ένα κελί αναφέρεται σε μια γραμματοσειρά αποθηκεύοντας αυτόν τον δείκτη συν ένα στο FontIndex, όπου το μηδέν είναι δεσμευμένο για την προεπιλογή του βιβλίου εργασίας. Το +1 βρίσκεται ακριβώς εκεί στο παράδειγμα μαζικής εξαγωγής παραπάνω. Ξεχάστε το και το κελί αθόρυβα παίρνει το λάθος στυλ, επειδή ένα off-by-one σε έναν δείκτη δεξαμενής στυλ εξακολουθεί να είναι έγκυρος δείκτης και τίποτα δεν εγείρει σφάλμα
Η πειθαρχία που ακολουθεί είναι να δημιουργείτε κάθε αντικείμενο στυλ πριν από τον βρόχο γραμμών και να αναφέρεστε στον δείκτη του μέσα στον βρόχο. Η Fonts.Add αφαιρεί διπλότυπα από ταυτόσημους ορισμούς, οπότε η κλήση της μία φορά ανά γραμμή απλώς σπαταλά CPU. Η Alignments.Add είναι η παγίδα, επειδή επιστρέφει μια νέα καταχώριση σε κάθε κλήση. Μέσα σε έναν βρόχο 100 χιλιάδων γραμμών αυτό θάβει το styles.xml κάτω από εκατό χιλιάδες διπλότυπες εγγραφές στοίχισης, κάτι που διογκώνει το αρχείο στον δίσκο και επιβραδύνει κάθε επόμενο άνοιγμα στο Excel καθώς τα διπλότυπα αναλύονται ξανά. Χτίστε κάθε στυλ μία φορά έξω από τον βρόχο, και μετά αναφερθείτε στον δείκτη του όσες φορές χρειάζεστε
Ροές, φάκελοι προσωρινών αρχείων και ο βρόχος παρτίδας γύρω από όλα αυτά
Τίποτα από αυτά δεν απαιτεί σύστημα αρχείων. Και οι δύο προσόψεις φέρουν υπερφορτώσεις TStream σε όλη την επιφάνεια IO τους, ανάμεσά τους οι Open, SaveAs, SaveAsCSV, SaveAsHTML και SaveAsODS, οπότε ένας worker παρτίδας μπορεί να αποδώσει απευθείας σε ένα TMemoryStream προορισμένο για αποθήκευση blob ή για μια απόκριση HTTP χωρίς να αγγίξει ποτέ τον δίσκο. Υπάρχει μία αιχμηρή λεπτομέρεια που πρέπει να θυμάστε. Η SaveAs(Stream) γράφει από την τρέχουσα θέση της ροής και δεν την επαναφέρει στην αρχή στη συνέχεια, οπότε ορίστε εσείς οι ίδιοι Position := 0 πριν παραδώσετε τη ροή σε ό,τι την παραδίδει, αλλιώς ο καταναλωτής διαβάζει μηδέν bytes. Η πρόσοψη XLS προσθέτει δύο δικά της κουμπιά ρύθμισης. Η SetTempDir κατευθύνει τα προσωρινά αρχεία του writer BIFF σε έναν τόμο που έχει τον χώρο και το περιθώριο IO για να τα απορροφήσει, κάτι που έχει σημασία σε servers όπου η προεπιλεγμένη διαδρομή προσωρινών αρχείων βρίσκεται σε έναν στενόχωρο δίσκο συστήματος. Η UseSharedFormulas αναδιπλώνει επαναλαμβανόμενα σώματα τύπων σε κοινές ομάδες, μια πραγματική μείωση μεγέθους για το κλασικό σχήμα αναφοράς όπου ένας τύπος αντιγράφεται προς τα κάτω σε ολόκληρη μια στήλη
Ο ίδιος ο βρόχος παρτίδας παραμένει σκόπιμα βαρετός:
for FileName in SourceFiles do
begin
Book := TXLSXWorkbook.Create; // νέο instance: καμία διαρροή κατάστασης
try
Book.StreamingWrite := True;
if Book.Open(FileName) <> 1 then
Continue; // μία κακή είσοδος δεν πρέπει να σκοτώσει την παρτίδα
Book.SaveAsCSV(ChangeFileExt(FileName, '.csv'), 0, ',');
finally
Book.Free;
end;
end;
Ένα νέο instance βιβλίου εργασίας ανά αρχείο κοστίζει microseconds και εξαλείφει ολόκληρη μια κατηγορία σφαλμάτων μόλυνσης μεταξύ αρχείων: τα στυλ, τα defined names και οι ιδιότητες εγγράφου από το αρχείο 17 δεν έχουν κανένα δρόμο να διαρρεύσουν στο αρχείο 18. Το skip-and-continue σε μια αποτυχημένη Open κερδίζει εξίσου τη θέση του, επειδή ένα περικομμένο upload σε μια παρτίδα 600 αρχείων θα πρέπει να σας κοστίζει μία μόνο γραμμή log αντί για την υπόλοιπη εκτέλεση. Αξίζει επίσης να σημειωθεί τι σκόπιμα δεν κάνει το σκέλος CSV. Η SaveAsCSV γράφει τους τύπους ως κυριολεκτικό κείμενο και δεν τους αξιολογεί ποτέ, οπότε μια παρτίδα μετατροπής της οποίας οι καταναλωτές περιμένουν υπολογισμένους αριθμούς πρέπει πρώτα να τρέξει Calculate στα σχετικά κελιά, ή να ξεκινήσει από βιβλία εργασίας που ήδη φέρουν αποθηκευμένα αποτελέσματα από προηγούμενο υπολογισμό
Μοντέλο ταυτοχρονισμού: ένα βιβλίο εργασίας ανά thread
Τα αντικείμενα καμίας από τις δύο προσόψεις δεν είναι thread-safe, και ο σχεδιασμός ποτέ δεν προσποιήθηκε το αντίθετο. Επειδή δεν υπάρχει κοινή global κατάσταση ανάμεσα σε instances, ο κανόνας κλιμάκωσης είναι απλά ένα βιβλίο εργασίας ανά worker thread, χωρίς κανένα διαμοιρασμό ενός βιβλίου εργασίας μεταξύ threads. Μια δεξαμενή N workers, καθένας με το δικό του TXLSXWorkbook, κλιμακώνεται σχεδόν γραμμικά μέχρι η μνήμη να γίνει το ταβάνι, και αυτό το ταβάνι είναι κάτι που μπορείτε να το βάλετε σε αριθμό: το μεγαλύτερο ταυτόχρονο μοντέλο κελιών επί τον αριθμό των workers, συν όποια επιβάρυνση κατά την αποθήκευση έχει ισοπεδώσει η StreamingWrite. Όταν η ουρά γίνεται βαθιά, εφαρμόστε back-pressure στην ουρά εργασιών αντί μέσα στον writer. Ένα thread που λιμοκτονεί και έχει γράψει μισό βιβλίο εργασίας δεν έχει παράγει τίποτα χρήσιμο, ενώ μια εργασία που περίμενε λίγα δευτερόλεπτα για έναν ελεύθερο worker ολοκληρώνεται ανέπαφη
Για την ευρύτερη εικόνα ρύθμισης, συμπεριλαμβανομένων των κοινών τύπων, της παράλειψης γραφικών στην πλευρά της ανάγνωσης και των μοχλών ειδικών για XLS, δείτε τον οδηγό απόδοσης για μεγάλα βιβλία εργασίας. Οι δουλειές παρτίδας των οποίων οι γραμμές προέρχονται απευθείας από ένα ερώτημα καλύπτονται ξεχωριστά στα μοτίβα εξαγωγής βάσης δεδομένων για αναφορές Delphi
Το HotXLS μεταγλωττίζεται μέσα στην υπηρεσία Delphi ή C++Builder σας ως εγγενής Object Pascal χωρίς εξωτερικές εξαρτήσεις· οι εκδόσεις και η αδειοδότηση βρίσκονται στη σελίδα προϊόντος HotXLS Delphi Component