Πώς μεταφράσαμε 10 εκατομμύρια γραμμές κώδικα C++ στο πρότυπο C++14 (και στη συνέχεια σε C++17)

Πριν από λίγο καιρό (το φθινόπωρο του 2016), κατά την ανάπτυξη της επόμενης έκδοσης της τεχνολογικής πλατφόρμας 1C:Enterprise, προέκυψε το ερώτημα εντός της ομάδας ανάπτυξης σχετικά με την υποστήριξη του νέου προτύπου C ++ 14 στον κώδικά μας. Η μετάβαση σε ένα νέο πρότυπο, όπως υποθέσαμε, θα μας επέτρεπε να γράψουμε πολλά πράγματα πιο κομψά, απλά και αξιόπιστα και θα απλοποιούσε την υποστήριξη και τη συντήρηση του κώδικα. Και δεν φαίνεται να υπάρχει τίποτα το εξαιρετικό στη μετάφραση, αν όχι για την κλίμακα της βάσης του κώδικα και τα ειδικά χαρακτηριστικά του κώδικά μας.

Για όσους δεν γνωρίζουν, το 1C:Enterprise είναι ένα περιβάλλον για την ταχεία ανάπτυξη επιχειρηματικών εφαρμογών πολλαπλών πλατφορμών και χρόνου εκτέλεσης για την εκτέλεσή τους σε διαφορετικά OS και DBMS. Σε γενικές γραμμές, το προϊόν περιέχει:

Προσπαθούμε να γράφουμε τον ίδιο κώδικα για διαφορετικά λειτουργικά συστήματα όσο το δυνατόν περισσότερο - η βάση κώδικα διακομιστή είναι 99% κοινή, η βάση κώδικα πελάτη είναι περίπου 95%. Η τεχνολογική πλατφόρμα 1C: Enterprise είναι κυρίως γραμμένη σε C++ και τα κατά προσέγγιση χαρακτηριστικά του κώδικα δίνονται παρακάτω:

  • 10 εκατομμύρια γραμμές κώδικα C++,
  • 14 χιλιάδες αρχεία,
  • 60 χιλιάδες τάξεις,
  • μισό εκατομμύριο μεθόδους.

Και όλα αυτά έπρεπε να μεταφραστούν σε C++14. Σήμερα θα σας πούμε πώς το κάναμε αυτό και τι συναντήσαμε στη διαδικασία.

Πώς μεταφράσαμε 10 εκατομμύρια γραμμές κώδικα C++ στο πρότυπο C++14 (και στη συνέχεια σε C++17)

Αποποίηση ευθυνών

Όλα όσα γράφονται παρακάτω για αργή/γρήγορη εργασία, (όχι) μεγάλη κατανάλωση μνήμης από υλοποιήσεις τυπικών κλάσεων σε διάφορες βιβλιοθήκες σημαίνουν ένα πράγμα: αυτό ισχύει ΓΙΑ ΕΜΑΣ. Είναι πολύ πιθανό οι τυπικές υλοποιήσεις να είναι οι πλέον κατάλληλες για τις εργασίες σας. Ξεκινήσαμε από τις δικές μας εργασίες: λάβαμε δεδομένα που ήταν τυπικά για τους πελάτες μας, εκτελέσαμε τυπικά σενάρια για αυτούς, εξετάσαμε την απόδοση, την ποσότητα μνήμης που καταναλώθηκε κ.λπ., και αναλύσαμε εάν εμείς και οι πελάτες μας ήμασταν ικανοποιημένοι με τέτοια αποτελέσματα ή όχι . Και έδρασαν ανάλογα.

Αυτό που είχαμε

Αρχικά, γράψαμε τον κώδικα για την πλατφόρμα 1C:Enterprise 8 χρησιμοποιώντας το Microsoft Visual Studio. Το έργο ξεκίνησε στις αρχές της δεκαετίας του 2000 και είχαμε μια έκδοση μόνο για Windows. Φυσικά, από τότε ο κώδικας έχει αναπτυχθεί ενεργά, πολλοί μηχανισμοί έχουν ξαναγραφτεί πλήρως. Αλλά ο κώδικας γράφτηκε σύμφωνα με το πρότυπο του 1998 και, για παράδειγμα, οι ορθές αγκύλες μας χωρίστηκαν με κενά έτσι ώστε η μεταγλώττιση να πετύχει, ως εξής:

vector<vector<int> > IntV;

Το 2006, με την κυκλοφορία της έκδοσης 8.1 της πλατφόρμας, αρχίσαμε να υποστηρίζουμε Linux και μεταβήκαμε σε μια τυπική βιβλιοθήκη τρίτου κατασκευαστή STLPort. Ένας από τους λόγους της μετάβασης ήταν η δουλειά με φαρδιές γραμμές. Στον κώδικά μας, χρησιμοποιούμε std::wstring, ο οποίος βασίζεται στον τύπο wchar_t, σε όλη την έκταση. Το μέγεθός του στα Windows είναι 2 byte και στο Linux η προεπιλογή είναι 4 byte. Αυτό οδήγησε σε ασυμβατότητα των δυαδικών πρωτοκόλλων μας μεταξύ πελάτη και διακομιστή, καθώς και σε διάφορα μόνιμα δεδομένα. Χρησιμοποιώντας τις επιλογές gcc, μπορείτε να καθορίσετε ότι το μέγεθος του wchar_t κατά τη μεταγλώττιση είναι επίσης 2 byte, αλλά στη συνέχεια μπορείτε να ξεχάσετε τη χρήση της τυπικής βιβλιοθήκης από τον μεταγλωττιστή, επειδή χρησιμοποιεί glibc, το οποίο με τη σειρά του μεταγλωττίζεται για ένα wchar_t 4 byte. Άλλοι λόγοι ήταν η καλύτερη εφαρμογή τυπικών κλάσεων, η υποστήριξη πινάκων κατακερματισμού, ακόμη και η εξομοίωση της σημασιολογίας της μετακίνησης μέσα σε κοντέινερ, τα οποία χρησιμοποιήσαμε ενεργά. Και ένας ακόμη λόγος, όπως λένε τελευταία, ήταν η απόδοση εγχόρδων. Είχαμε τη δική μας τάξη για έγχορδα, γιατί... Λόγω των ιδιαιτεροτήτων του λογισμικού μας, οι λειτουργίες συμβολοσειράς χρησιμοποιούνται πολύ ευρέως και για εμάς αυτό είναι κρίσιμο.

Η συμβολοσειρά μας βασίζεται σε ιδέες βελτιστοποίησης συμβολοσειρών που εκφράστηκαν στις αρχές της δεκαετίας του 2000 Αντρέι Αλεξανδρέσκου. Αργότερα, όταν ο Αλεξανδρέσκου εργάστηκε στο Facebook, μετά από πρόταση του, χρησιμοποιήθηκε μια γραμμή στη μηχανή του Facebook που λειτουργούσε με παρόμοιες αρχές (βλ. βιβλιοθήκη τρέλα).

Η γραμμή μας χρησιμοποίησε δύο κύριες τεχνολογίες βελτιστοποίησης:

  1. Για σύντομες τιμές, χρησιμοποιείται μια εσωτερική προσωρινή μνήμη στο ίδιο το αντικείμενο συμβολοσειράς (δεν απαιτεί πρόσθετη εκχώρηση μνήμης).
  2. Για όλα τα άλλα χρησιμοποιούνται μηχανικοί Αντιγραφή σε εγγραφή. Η τιμή συμβολοσειράς αποθηκεύεται σε ένα μέρος και χρησιμοποιείται ένας μετρητής αναφοράς κατά την εκχώρηση/τροποποίηση.

Για να επιταχύνουμε τη μεταγλώττιση πλατφόρμας, αποκλείσαμε την υλοποίηση ροής από την παραλλαγή STLPort (την οποία δεν χρησιμοποιήσαμε), αυτό μας έδωσε περίπου 20% ταχύτερη μεταγλώττιση. Στη συνέχεια έπρεπε να κάνουμε περιορισμένη χρήση Ώθηση. Το Boost κάνει μεγάλη χρήση της ροής, ιδιαίτερα στα API υπηρεσιών του (για παράδειγμα, για καταγραφή), οπότε έπρεπε να το τροποποιήσουμε για να καταργήσουμε τη χρήση ροής. Αυτό, με τη σειρά του, μας δυσκόλεψε τη μετεγκατάσταση σε νέες εκδόσεις του Boost.

τρίτος τρόπος

Κατά τη μετάβαση στο πρότυπο C++14, εξετάσαμε τις ακόλουθες επιλογές:

  1. Αναβαθμίστε το STLPort που τροποποιήσαμε στο πρότυπο C++14. Η επιλογή είναι πολύ δύσκολη, γιατί... Η υποστήριξη για το STLPort διακόπηκε το 2010 και θα έπρεπε να δημιουργήσουμε μόνοι μας όλο τον κώδικά του.
  2. Μετάβαση σε άλλη υλοποίηση STL συμβατή με C++14. Είναι πολύ επιθυμητό αυτή η υλοποίηση να είναι για Windows και Linux.
  3. Κατά τη μεταγλώττιση για κάθε λειτουργικό σύστημα, χρησιμοποιήστε τη βιβλιοθήκη που είναι ενσωματωμένη στον αντίστοιχο μεταγλωττιστή.

Η πρώτη επιλογή απορρίφθηκε εντελώς λόγω υπερβολικής δουλειάς.

Σκεφτήκαμε τη δεύτερη επιλογή για κάποιο χρονικό διάστημα. θεωρείται ως υποψήφιος libc++, αλλά εκείνη την εποχή δεν λειτουργούσε στα Windows. Για να μεταφέρετε το libc++ στα Windows, θα πρέπει να κάνετε πολλή δουλειά - για παράδειγμα, γράψτε μόνοι σας οτιδήποτε έχει να κάνει με νήματα, συγχρονισμό νημάτων και ατομικότητα, αφού το libc++ χρησιμοποιείται σε αυτές τις περιοχές POSIX API.

Και επιλέξαμε τον τρίτο δρόμο.

Μετάβαση

Έτσι, έπρεπε να αντικαταστήσουμε τη χρήση του STLPort με τις βιβλιοθήκες των αντίστοιχων μεταγλωττιστών (Visual Studio 2015 για Windows, gcc 7 για Linux, clang 8 για macOS).

Ευτυχώς, ο κώδικάς μας γράφτηκε κυρίως σύμφωνα με οδηγίες και δεν χρησιμοποίησε κάθε είδους έξυπνα κόλπα, έτσι η μετάβαση σε νέες βιβλιοθήκες προχώρησε σχετικά ομαλά, με τη βοήθεια σεναρίων που αντικατέστησαν τα ονόματα των τύπων, κλάσεων, χώρων ονομάτων και περιλαμβάνει στην πηγή αρχεία. Η μετεγκατάσταση επηρέασε 10 αρχεία πηγής (από 000). Το wchar_t αντικαταστάθηκε από το char14_t. αποφασίσαμε να εγκαταλείψουμε τη χρήση του wchar_t, γιατί Το char000_t παίρνει 16 byte σε όλα τα λειτουργικά συστήματα και δεν χαλάει τη συμβατότητα κώδικα μεταξύ Windows και Linux.

Υπήρχαν κάποιες μικρές περιπέτειες. Για παράδειγμα, στο STLPort ένας επαναλήπτης θα μπορούσε να μεταδοθεί σιωπηρά σε έναν δείκτη σε ένα στοιχείο και σε ορισμένα σημεία στον κώδικά μας χρησιμοποιήθηκε αυτό. Σε νέες βιβλιοθήκες δεν ήταν πλέον δυνατό να γίνει αυτό, και αυτά τα αποσπάσματα έπρεπε να αναλυθούν και να ξαναγραφτούν χειροκίνητα.

Έτσι, η μετεγκατάσταση κώδικα έχει ολοκληρωθεί, ο κώδικας έχει μεταγλωττιστεί για όλα τα λειτουργικά συστήματα. Ήρθε η ώρα για δοκιμές.

Οι δοκιμές μετά τη μετάβαση έδειξαν πτώση στην απόδοση (σε ορισμένα σημεία έως και 20-30%) και αύξηση στην κατανάλωση μνήμης (έως 10-15%) σε σύγκριση με την παλιά έκδοση του κώδικα. Αυτό οφειλόταν, ειδικότερα, στην υποβέλτιστη απόδοση των τυπικών χορδών. Ως εκ τούτου, έπρεπε και πάλι να χρησιμοποιήσουμε τη δική μας, ελαφρώς τροποποιημένη γραμμή.

Αποκαλύφθηκε επίσης ένα ενδιαφέρον χαρακτηριστικό της υλοποίησης κοντέινερ σε ενσωματωμένες βιβλιοθήκες: κενό (χωρίς στοιχεία) std::map και std::set από ενσωματωμένες βιβλιοθήκες εκχωρούν μνήμη. Και λόγω των χαρακτηριστικών υλοποίησης, σε ορισμένα σημεία του κώδικα δημιουργούνται πολλά άδεια δοχεία αυτού του τύπου. Τα τυπικά δοχεία μνήμης κατανέμονται λίγο, για ένα ριζικό στοιχείο, αλλά για εμάς αυτό αποδείχθηκε κρίσιμο - σε ορισμένα σενάρια, η απόδοσή μας έπεσε σημαντικά και η κατανάλωση μνήμης αυξήθηκε (σε σύγκριση με το STLPort). Επομένως, στον κώδικά μας αντικαταστήσαμε αυτούς τους δύο τύπους κοντέινερ από τις ενσωματωμένες βιβλιοθήκες με την υλοποίησή τους από το Boost, όπου αυτά τα κοντέινερ δεν είχαν αυτή τη δυνατότητα και αυτό έλυσε το πρόβλημα με την επιβράδυνση και την αυξημένη κατανάλωση μνήμης.

Όπως συμβαίνει συχνά μετά από αλλαγές μεγάλης κλίμακας σε μεγάλα έργα, η πρώτη επανάληψη του πηγαίου κώδικα δεν λειτούργησε χωρίς προβλήματα και εδώ, ειδικότερα, ήταν χρήσιμη η υποστήριξη για τον εντοπισμό σφαλμάτων επαναλήπτες στην υλοποίηση των Windows. Βήμα-βήμα προχωρήσαμε και μέχρι την άνοιξη του 2017 (έκδοση 8.3.11 1C:Enterprise) η μετεγκατάσταση ολοκληρώθηκε.

Αποτελέσματα της

Η μετάβαση στο πρότυπο C++14 μας πήρε περίπου 6 μήνες. Τις περισσότερες φορές, ένας προγραμματιστής (αλλά με πολύ υψηλή εξειδίκευση) εργάστηκε στο έργο και στο τελικό στάδιο συμμετείχαν εκπρόσωποι ομάδων υπεύθυνων για συγκεκριμένους τομείς - UI, σύμπλεγμα διακομιστών, εργαλεία ανάπτυξης και διαχείρισης κ.λπ.

Η μετάβαση απλοποίησε πολύ τη δουλειά μας για τη μετάβαση στις πιο πρόσφατες εκδόσεις του προτύπου. Έτσι, η έκδοση 1C:Enterprise 8.3.14 (σε ανάπτυξη, η κυκλοφορία έχει προγραμματιστεί για τις αρχές του επόμενου έτους) έχει ήδη μεταφερθεί στο πρότυπο C++17.

Μετά τη μετεγκατάσταση, οι προγραμματιστές έχουν περισσότερες επιλογές. Αν νωρίτερα είχαμε τη δική μας τροποποιημένη έκδοση του STL και έναν χώρο ονομάτων std, τώρα έχουμε τυπικές κλάσεις από τις ενσωματωμένες βιβλιοθήκες μεταγλωττιστών στον χώρο ονομάτων std, στον χώρο ονομάτων stdx - οι γραμμές και τα κοντέινερ μας βελτιστοποιημένα για τις εργασίες μας, στο boost - το τελευταία έκδοση του boost. Και ο προγραμματιστής χρησιμοποιεί εκείνες τις κλάσεις που είναι βέλτιστα κατάλληλες για την επίλυση των προβλημάτων του.

Η «εγγενής» εφαρμογή των κατασκευαστών κίνησης βοηθά επίσης στην ανάπτυξη (μετακινούν κατασκευαστές) για έναν αριθμό τάξεων. Εάν μια κλάση έχει έναν κατασκευαστή κίνησης και αυτή η κλάση τοποθετείται σε ένα κοντέινερ, τότε το STL βελτιστοποιεί την αντιγραφή στοιχείων μέσα στο κοντέινερ (για παράδειγμα, όταν το κοντέινερ επεκτείνεται και είναι απαραίτητο να αλλάξει χωρητικότητα και να εκχωρηθεί εκ νέου η μνήμη).

Πετάξτε στην αλοιφή

Ίσως η πιο δυσάρεστη (αλλά όχι κρίσιμη) συνέπεια της μετανάστευσης είναι ότι βρισκόμαστε αντιμέτωποι με αύξηση του όγκου αρχεία obj, και το πλήρες αποτέλεσμα της κατασκευής με όλα τα ενδιάμεσα αρχεία άρχισε να καταλαμβάνει 60–70 GB. Αυτή η συμπεριφορά οφείλεται στις ιδιαιτερότητες των σύγχρονων τυπικών βιβλιοθηκών, οι οποίες έχουν γίνει λιγότερο επικριτικές για το μέγεθος των δημιουργούμενων αρχείων υπηρεσίας. Αυτό δεν επηρεάζει τη λειτουργία της μεταγλωττισμένης εφαρμογής, αλλά προκαλεί μια σειρά από ενοχλήσεις στην ανάπτυξη, ειδικότερα, αυξάνει τον χρόνο μεταγλώττισης. Οι απαιτήσεις για ελεύθερο χώρο στο δίσκο σε διακομιστές κατασκευής και σε μηχανήματα προγραμματιστών αυξάνονται επίσης. Οι προγραμματιστές μας εργάζονται σε πολλές εκδόσεις της πλατφόρμας παράλληλα και εκατοντάδες gigabyte ενδιάμεσων αρχείων μερικές φορές δημιουργούν δυσκολίες στη δουλειά τους. Το πρόβλημα είναι δυσάρεστο, αλλά όχι κρίσιμο· προς το παρόν αναβάλαμε τη λύση του. Εξετάζουμε την τεχνολογία ως μία από τις επιλογές για την επίλυσή της οικοδόμηση ενότητας (συγκεκριμένα, η Google το χρησιμοποιεί κατά την ανάπτυξη του προγράμματος περιήγησης Chrome).

Πηγή: www.habr.com

Προσθέστε ένα σχόλιο