Ανεπιτυχές άρθρο σχετικά με την επιτάχυνση της αντανάκλασης

Θα εξηγήσω αμέσως τον τίτλο του άρθρου. Το αρχικό σχέδιο ήταν να δώσει καλές, αξιόπιστες συμβουλές για την επιτάχυνση της χρήσης του προβληματισμού χρησιμοποιώντας ένα απλό αλλά ρεαλιστικό παράδειγμα, αλλά κατά τη συγκριτική αξιολόγηση αποδείχθηκε ότι η αντανάκλαση δεν είναι τόσο αργή όσο νόμιζα, το LINQ είναι πιο αργό από ό,τι στους εφιάλτες μου. Τελικά όμως αποδείχτηκε ότι έκανα και λάθος στις μετρήσεις... Λεπτομέρειες αυτής της ιστορίας ζωής βρίσκονται κάτω από το κόψιμο και στα σχόλια. Δεδομένου ότι το παράδειγμα είναι αρκετά συνηθισμένο και εφαρμόζεται κατ' αρχήν όπως γίνεται συνήθως σε μια επιχείρηση, αποδείχθηκε ότι ήταν μια αρκετά ενδιαφέρουσα, όπως μου φαίνεται, επίδειξη ζωής: ο αντίκτυπος στην ταχύτητα του κύριου θέματος του άρθρου ήταν δεν γίνεται αντιληπτό λόγω εξωτερικής λογικής: Moq, Autofac, EF Core και άλλα "ιμάντες".

Άρχισα να εργάζομαι με την εντύπωση αυτού του άρθρου: Γιατί το Reflection αργεί

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

Λαμβάνοντας υπόψη ότι πάντα είχα παρόμοια άποψη σχετικά με την ταχύτητα του στοχασμού, δεν σκόπευα ιδιαίτερα να αμφισβητήσω τα συμπεράσματα του συγγραφέα.

Συχνά συναντώ αφελή χρήση του προβληματισμού στην επιχείρηση. Λαμβάνεται ο τύπος. Λαμβάνονται πληροφορίες για το ακίνητο. Καλείται η μέθοδος SetValue και όλοι χαίρονται. Η τιμή έφτασε στο πεδίο στόχο, όλοι είναι ευχαριστημένοι. Πολύ έξυπνοι άνθρωποι - ηλικιωμένοι και αρχηγοί ομάδων - γράφουν τις επεκτάσεις τους για να αντικρούσουν, βασιζόμενοι σε μια τόσο αφελή εφαρμογή "καθολικής" αντιστοίχισης από τον έναν τύπο στον άλλο. Η ουσία είναι συνήθως η εξής: παίρνουμε όλα τα πεδία, παίρνουμε όλες τις ιδιότητες, επαναλαμβάνουμε πάνω τους: εάν τα ονόματα των μελών τύπων ταιριάζουν, εκτελούμε το SetValue. Κατά καιρούς πιάνουμε εξαιρέσεις λόγω λαθών όπου δεν βρήκαμε κάποια ιδιότητα σε έναν από τους τύπους, αλλά και εδώ υπάρχει διέξοδος που βελτιώνει την απόδοση. Προσπάθησε να πιάσεις.

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

Νομίζω ότι πολλοί από εσάς, διαβάζοντας τον Richter ή άλλους ιδεολόγους, έχετε συναντήσει μια εντελώς δίκαιη δήλωση ότι ο προβληματισμός στον κώδικα είναι ένα φαινόμενο που έχει εξαιρετικά αρνητικό αντίκτυπο στην απόδοση της εφαρμογής.

Η αντανάκλαση κλήσης αναγκάζει το CLR να περάσει από συγκροτήματα για να βρει αυτό που χρειάζεται, να ανασύρει τα μεταδεδομένα του, να τα αναλύσει κ.λπ. Επιπλέον, η αντανάκλαση κατά τη διέλευση ακολουθιών οδηγεί στην κατανομή μεγάλης ποσότητας μνήμης. Καταναλώνουμε τη μνήμη, το CLR αποκαλύπτει το GC και αρχίζουν οι ζωφόροι. Θα πρέπει να είναι αισθητά αργό, πιστέψτε με. Οι τεράστιες ποσότητες μνήμης σε σύγχρονους διακομιστές παραγωγής ή υπολογιστές cloud δεν εμποδίζουν τις υψηλές καθυστερήσεις επεξεργασίας. Στην πραγματικότητα, όσο περισσότερη μνήμη, τόσο πιο πιθανό είναι να ΣΗΜΕΙΩΣΕΤΕ πώς λειτουργεί το GC. Η αντανάκλαση είναι, θεωρητικά, ένα επιπλέον κόκκινο κουρέλι για αυτόν.

Ωστόσο, όλοι χρησιμοποιούμε δοχεία IoC και χαρτογράφους ημερομηνίας, η αρχή λειτουργίας των οποίων βασίζεται επίσης στον προβληματισμό, αλλά συνήθως δεν υπάρχουν ερωτήσεις σχετικά με την απόδοσή τους. Όχι, όχι επειδή η εισαγωγή εξαρτήσεων και η αφαίρεση από εξωτερικά μοντέλα περιορισμένου περιβάλλοντος είναι τόσο απαραίτητα που πρέπει να θυσιάσουμε την απόδοση σε κάθε περίπτωση. Όλα είναι πιο απλά - πραγματικά δεν επηρεάζει πολύ την απόδοση.

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

Πώς επιτυγχάνεται αυτό; Ουσιαστικά, αυτό δεν διαφέρει από τη λογική που χρησιμοποιεί η ίδια η πλατφόρμα για τη δημιουργία κώδικα JIT. Όταν μια μέθοδος καλείται για πρώτη φορά, μεταγλωττίζεται (και, ναι, αυτή η διαδικασία δεν είναι γρήγορη), σε επόμενες κλήσεις, ο έλεγχος μεταφέρεται στην ήδη μεταγλωττισμένη μέθοδο και δεν θα υπάρξουν σημαντικές μειώσεις απόδοσης.

Στην περίπτωσή μας, μπορείτε επίσης να χρησιμοποιήσετε τη μεταγλώττιση JIT και στη συνέχεια να χρησιμοποιήσετε τη μεταγλωττισμένη συμπεριφορά με την ίδια απόδοση με τις αντίστοιχες AOT. Οι εκφράσεις θα μας βοηθήσουν σε αυτή την περίπτωση.

Η εν λόγω αρχή μπορεί να διατυπωθεί εν συντομία ως εξής:
Θα πρέπει να αποθηκεύσετε το τελικό αποτέλεσμα του προβληματισμού ως εκπρόσωπος που περιέχει τη μεταγλωττισμένη συνάρτηση. Είναι επίσης λογικό να αποθηκεύετε κρυφά όλα τα απαραίτητα αντικείμενα με πληροφορίες τύπου στα πεδία του τύπου σας, του εργαζόμενου, που είναι αποθηκευμένα έξω από τα αντικείμενα.

Υπάρχει λογική σε αυτό. Η κοινή λογική μας λέει ότι αν κάτι μπορεί να μεταγλωττιστεί και να αποθηκευτεί προσωρινά, τότε θα πρέπει να γίνει.

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

Τώρα για τον κώδικα. Ας δούμε ένα παράδειγμα που βασίζεται στον πρόσφατο πόνο μου που έπρεπε να αντιμετωπίσω σε μια σοβαρή παραγωγή ενός σοβαρού πιστωτικού ιδρύματος. Όλες οι οντότητες είναι πλασματικές έτσι ώστε κανείς να μην μαντέψει.

Υπάρχει κάποια ουσία. Να υπάρχει Επαφή. Υπάρχουν γράμματα με τυποποιημένο σώμα, από τα οποία ο αναλυτής και ο ενυδατωτής δημιουργούν τις ίδιες επαφές. Έφτασε ένα γράμμα, το διαβάσαμε, το αναλύσαμε σε ζεύγη κλειδιών-τιμών, δημιουργήσαμε μια επαφή και το αποθηκεύσαμε στη βάση δεδομένων.

Είναι στοιχειώδες. Ας υποθέσουμε ότι μια επαφή έχει τις ιδιότητες Πλήρες Όνομα, Ηλικία και Τηλέφωνο Επικοινωνίας. Αυτά τα δεδομένα διαβιβάζονται στην επιστολή. Η επιχείρηση θέλει επίσης υποστήριξη για να μπορεί να προσθέτει γρήγορα νέα κλειδιά για την αντιστοίχιση ιδιοτήτων οντοτήτων σε ζεύγη στο σώμα του γράμματος. Σε περίπτωση που κάποιος έκανε τυπογραφικό λάθος στο πρότυπο ή εάν πριν από την κυκλοφορία είναι απαραίτητο να ξεκινήσει επειγόντως η χαρτογράφηση από έναν νέο συνεργάτη, προσαρμοζόμενο στη νέα μορφή. Στη συνέχεια, μπορούμε να προσθέσουμε μια νέα αντιστοίχιση ως φθηνή επιδιόρθωση δεδομένων. Ένα παράδειγμα ζωής δηλαδή.

Υλοποιούμε, δημιουργούμε δοκιμές. Εργα.

Δεν θα δώσω τον κώδικα: υπάρχουν πολλές πηγές και είναι διαθέσιμες στο GitHub μέσω του συνδέσμου στο τέλος του άρθρου. Μπορείτε να τα φορτώσετε, να τα βασανίσετε αγνώριστα και να τα μετρήσετε, όπως θα επηρέαζε στην περίπτωσή σας. Θα δώσω μόνο τον κώδικα δύο μεθόδων προτύπου που διακρίνουν τον ενυδατικό, ο οποίος υποτίθεται ότι ήταν γρήγορος, από τον ενυδατικό που υποτίθεται ότι ήταν αργός.

Η λογική είναι η εξής: η μέθοδος template λαμβάνει ζεύγη που δημιουργούνται από τη βασική λογική ανάλυσης. Το επίπεδο LINQ είναι ο αναλυτής και η βασική λογική του hydrator, που κάνει ένα αίτημα στο πλαίσιο της βάσης δεδομένων και συγκρίνει κλειδιά με ζεύγη από τον αναλυτή (για αυτές τις συναρτήσεις υπάρχει κώδικας χωρίς LINQ για σύγκριση). Στη συνέχεια, τα ζεύγη περνούν στην κύρια μέθοδο ενυδάτωσης και οι τιμές των ζευγών ορίζονται στις αντίστοιχες ιδιότητες της οντότητας.

"Fast" (Πρόθεμα Fast στα σημεία αναφοράς):

 protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var setterMapItem in _proprtySettersMap)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == setterMapItem.Key);
                setterMapItem.Value(contact, correlation?.Value);
            }
            return contact;
        }

Όπως μπορούμε να δούμε, χρησιμοποιείται μια στατική συλλογή με ιδιότητες ρυθμιστή - μεταγλωττισμένα λάμδα που καλούν την οντότητα ρυθμιστή. Δημιουργήθηκε από τον παρακάτω κώδικα:

        static FastContactHydrator()
        {
            var type = typeof(Contact);
            foreach (var property in type.GetProperties())
            {
                _proprtySettersMap[property.Name] = GetSetterAction(property);
            }
        }

        private static Action<Contact, string> GetSetterAction(PropertyInfo property)
        {
            var setterInfo = property.GetSetMethod();
            var paramValueOriginal = Expression.Parameter(property.PropertyType, "value");
            var paramEntity = Expression.Parameter(typeof(Contact), "entity");
            var setterExp = Expression.Call(paramEntity, setterInfo, paramValueOriginal).Reduce();
            
            var lambda = (Expression<Action<Contact, string>>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Σε γενικές γραμμές είναι ξεκάθαρο. Διασχίζουμε τις ιδιότητες, δημιουργούμε πληρεξούσιους για αυτούς που καλούν ρυθμιστές και τους αποθηκεύουμε. Τότε καλούμε όταν χρειάζεται.

"Αργό" (Πρόθεμα Slow στα σημεία αναφοράς):

        protected override Contact GetContact(PropertyToValueCorrelation[] correlations)
        {
            var contact = new Contact();
            foreach (var property in _properties)
            {
                var correlation = correlations.FirstOrDefault(x => x.PropertyName == property.Name);
                if (correlation?.Value == null)
                    continue;

                property.SetValue(contact, correlation.Value);
            }
            return contact;
        }

Εδώ παρακάμπτουμε αμέσως τις ιδιότητες και καλούμε απευθείας το SetValue.

Για λόγους σαφήνειας και ως αναφορά, εφάρμοσα μια αφελή μέθοδο που γράφει τις τιμές των ζευγών συσχέτισής τους απευθείας στα πεδία οντοτήτων. Πρόθεμα – Εγχειρίδιο.

Τώρα ας πάρουμε το BenchmarkDotNet και ας εξετάσουμε την απόδοση. Και ξαφνικά... (σπόιλερ - αυτό δεν είναι το σωστό αποτέλεσμα, λεπτομέρειες είναι παρακάτω)

Ανεπιτυχές άρθρο σχετικά με την επιτάχυνση της αντανάκλασης

Τι βλέπουμε εδώ; Οι μέθοδοι που φέρουν θριαμβευτικά το πρόθεμα Fast αποδεικνύονται πιο αργές σχεδόν σε όλα τα περάσματα από τις μεθόδους με το πρόθεμα Slow. Αυτό ισχύει τόσο για την κατανομή όσο και για την ταχύτητα της εργασίας. Από την άλλη πλευρά, μια όμορφη και κομψή υλοποίηση χαρτογράφησης χρησιμοποιώντας μεθόδους LINQ που προορίζονται για αυτό όπου είναι δυνατόν, αντίθετα μειώνει σημαντικά την παραγωγικότητα. Η διαφορά είναι σειράς. Η τάση δεν αλλάζει με διαφορετικούς αριθμούς περασμάτων. Η μόνη διαφορά είναι στην κλίμακα. Με το LINQ είναι 4 - 200 φορές πιο αργό, υπάρχουν περισσότερα σκουπίδια στην ίδια περίπου κλίμακα.

ΕΠΙΚΑΙΡΟΠΟΙΗΜΕΝΟ

Δεν πίστευα στα μάτια μου, αλλά το πιο σημαντικό, ο συνάδελφός μας δεν πίστευε ούτε στα μάτια μου ούτε στον κώδικά μου - Ντμίτρι Τιχόνοφ 0x1000000. Έχοντας επανεξετάσει τη λύση μου, ανακάλυψε έξοχα και επισήμανε ένα σφάλμα που έχασα λόγω μιας σειράς αλλαγών στην υλοποίηση, από την αρχική έως την τελική. Μετά τη διόρθωση του σφάλματος που βρέθηκε στη ρύθμιση Moq, όλα τα αποτελέσματα μπήκαν στη θέση τους. Σύμφωνα με τα αποτελέσματα της επανάληψης της δοκιμής, η κύρια τάση δεν αλλάζει - το LINQ εξακολουθεί να επηρεάζει την απόδοση περισσότερο από την αντανάκλαση. Ωστόσο, είναι ωραίο που η εργασία με τη μεταγλώττιση των εκφράσεων δεν γίνεται μάταια και το αποτέλεσμα είναι ορατό τόσο σε κατανομή όσο και σε χρόνο εκτέλεσης. Η πρώτη εκκίνηση, όταν αρχικοποιούνται στατικά πεδία, είναι φυσικά πιο αργή για τη μέθοδο «γρήγορη», αλλά στη συνέχεια η κατάσταση αλλάζει.

Εδώ είναι το αποτέλεσμα της επανάληψης δοκιμής:

Ανεπιτυχές άρθρο σχετικά με την επιτάχυνση της αντανάκλασης

Συμπέρασμα: όταν χρησιμοποιείτε τον προβληματισμό σε μια επιχείρηση, δεν υπάρχει ιδιαίτερη ανάγκη να καταφύγετε σε κόλπα - το LINQ θα καταναλώσει περισσότερο την παραγωγικότητα. Ωστόσο, σε μεθόδους υψηλού φορτίου που απαιτούν βελτιστοποίηση, μπορείτε να αποθηκεύσετε τον προβληματισμό με τη μορφή αρχικοποιητών και να εκχωρήσετε μεταγλωττιστές, οι οποίοι στη συνέχεια θα παρέχουν "γρήγορη" λογική. Με αυτόν τον τρόπο μπορείτε να διατηρήσετε τόσο την ευελιξία της αντανάκλασης όσο και την ταχύτητα της εφαρμογής.

Ο κωδικός αναφοράς είναι διαθέσιμος εδώ. Οποιοσδήποτε μπορεί να ελέγξει τα λόγια μου:
HabraReflectionTests

ΥΓ: ο κώδικας στις δοκιμές χρησιμοποιεί IoC και στα σημεία αναφοράς χρησιμοποιεί μια ρητή κατασκευή. Γεγονός είναι ότι στην τελική υλοποίηση έκοψα όλους τους παράγοντες που θα μπορούσαν να επηρεάσουν την απόδοση και να κάνουν το αποτέλεσμα θορυβώδες.

PPS: Ευχαριστώ τον χρήστη Ντμίτρι Τιχόνοφ @0x1000000 γιατί ανακάλυψα το σφάλμα μου στη ρύθμιση του Moq, το οποίο επηρέασε τις πρώτες μετρήσεις. Εάν κάποιος από τους αναγνώστες έχει αρκετό κάρμα, παρακαλώ κάντε like. Ο άντρας σταμάτησε, ο άντρας διάβασε, ο άντρας έκανε διπλό έλεγχο και υπέδειξε το λάθος. Νομίζω ότι αυτό αξίζει σεβασμού και συμπάθειας.

PPPS: χάρη στον σχολαστικό αναγνώστη που έφτασε στο κάτω μέρος του στυλ και του σχεδιασμού. Είμαι υπέρ της ομοιομορφίας και της ευκολίας. Η διπλωματία της παρουσίασης αφήνει πολλά περιθώρια, αλλά έλαβα υπόψη την κριτική. Ζητώ το βλήμα.

Πηγή: www.habr.com

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