Un article raté sur l'accélération de la réflexion

Je vais d'abord expliquer le titre de l'article. À l'origine, il Ă©tait prĂ©vu de donner un bon conseil fiable pour accĂ©lĂ©rer l'utilisation de la rĂ©flexion sur un exemple simple mais rĂ©aliste. Cependant, lors du benchmarking, il s'est avĂ©rĂ© que la rĂ©flexion ne fonctionne pas aussi lentement que je le pensais, tandis que LINQ est en rĂ©alitĂ© plus lent que dans mes pires cauchemars. De plus, je me suis rendu compte que j'avais Ă©galement commis une erreur dans mes mesures... Les dĂ©tails de cette histoire de vie se trouvent ci-dessous et dans les commentaires. Comme l'exemple est assez simple et mis en Ɠuvre de la maniĂšre dont cela se fait gĂ©nĂ©ralement dans une entreprise, cela a donnĂ© lieu Ă  une dĂ©monstration de vie qui me semble assez intĂ©ressante : l'influence sur la vitesse de travail du sujet principal de l'article Ă©tait imperceptible en raison de la logique externe : Moq, Autofac, EF Core et autres « dĂ©pendances ».

J'ai commencé mon travail sous l'influence de cet article : Pourquoi la réflexion est-elle lente

Comme on peut le voir, l'auteur propose d'utiliser des délégués compilés au lieu d'appeler directement les méthodes des types de réflexion comme un excellent moyen d'accélérer considérablement le fonctionnement de l'application. Il y a aussi, bien sûr, l'émission IL, mais je souhaiterais l'éviter car c'est la méthode la plus coûteuse en termes d'efforts pour accomplir la tùche, qui comporte des risques d'erreurs.

Étant donnĂ© que j'ai toujours eu une opinion similaire sur la vitesse de la rĂ©flexion, je n'avais pas l'intention de remettre en question les conclusions de l'auteur.

Je rencontre souvent l'utilisation naĂŻve de la rĂ©flexion dans l'entreprise. On prend le type. On obtient des informations sur la propriĂ©tĂ©. On appelle la mĂ©thode SetValue, et tout le monde est content. La valeur arrive dans le champ cible, et tout le monde est satisfait. Les gens ne sont pas idiots — les seniors et les leads — Ă©crivent leurs propres extensions sur object, en se basant sur une telle mise en Ɠuvre naĂŻve de mappers « universels » d'un type Ă  un autre. En gĂ©nĂ©ral, le principe est le suivant : on prend tous les champs, on prend toutes les propriĂ©tĂ©s, on les itĂšre : en cas de correspondance des noms des membres de type, on exĂ©cute SetValue. On attrape pĂ©riodiquement des exceptions lorsque l'on Ă©choue Ă  trouver une propriĂ©tĂ© sur l'un des types, mais mĂȘme ici, il existe une solution qui amĂ©liore la performance. Try/catch.

J'ai vu des gens rĂ©inventer des parseurs et des mappeurs sans ĂȘtre complĂštement armĂ©s d'informations sur le fonctionnement des vĂ©los inventĂ©s avant eux. J'ai vu des gens cacher leurs mises en Ɠuvre naĂŻves derriĂšre des stratĂ©gies, des interfaces, des injections, comme si cela excusait la bacchanale qui suivait. J'avais le nez en l'air face Ă  de telles rĂ©alisations. En fait, je n'ai jamais mesurĂ© la vĂ©ritable fuite de performance, et lorsque c'Ă©tait possible, je changeais simplement l'implĂ©mentation pour une version plus « optimale », si je pouvais le faire. C'est pourquoi les premiĂšres mesures dont il est question ci-dessous m'ont sĂ©rieusement dĂ©routĂ©.

Je pense que beaucoup d'entre vous, en lisant Richter ou d'autres idĂ©ologues, ont rencontrĂ© l'affirmation plutĂŽt juste selon laquelle la rĂ©flexion dans le code est un phĂ©nomĂšne qui affecte extrĂȘmement nĂ©gativement les performances de l'application.

L'appel Ă  la rĂ©flexion oblige le CLR Ă  parcourir les assemblies Ă  la recherche de ce qu'il faut, Ă  charger leurs mĂ©tadonnĂ©es, Ă  les parser, etc. De plus, la rĂ©flexion lors du parcours de sĂ©quences entraĂźne l'allocation d'une grande quantitĂ© de mĂ©moire. Nous consommons de la mĂ©moire, le CLR dĂ©ploie le GC et les freezes commencent. Cela doit ĂȘtre remarquablement lent, croyez-moi. Les Ă©normes volumes de mĂ©moire des serveurs de production modernes ou des machines cloud ne protĂšgent pas contre les dĂ©lais Ă©levĂ©s de traitement. En fait, plus il y a de mĂ©moire, plus il est probable que vous remarquiez comment le GC fonctionne. La rĂ©flexion est, en thĂ©orie, un inutile chiffon rouge pour cela.

Cependant, nous utilisons tous des conteneurs IoC et des mappeurs de données, dont le fonctionnement est également basé sur la réflexion, mais il y a généralement peu de questions sur leurs performances. Non, pas parce que l'injection de dépendances et l'abstraction des modÚles d'un contexte externe limité sont des choses si nécessaires que nous devons sacrifier la performance dans tous les cas. C'est plus simple - cela n'affecte vraiment pas beaucoup les performances.

Le fait est que les frameworks les plus courants, qui reposent sur la technologie de rĂ©flexion, utilisent toutes sortes d'astuces pour fonctionner de maniĂšre plus optimale avec elle. En gĂ©nĂ©ral, c'est du cache. En gĂ©nĂ©ral, ce sont des expressions et des dĂ©lĂ©guĂ©s compilĂ©s Ă  partir d'un arbre d'expressions. L'automapper lui-mĂȘme maintient un dictionnaire concurrent qui associe des types Ă  des fonctions qui peuvent se convertir les uns dans les autres sans appeler la rĂ©flexion.

Comment cela est-il accompli ? En substance, cela ne diffĂšre pas de la logique utilisĂ©e par la plateforme elle-mĂȘme pour gĂ©nĂ©rer du code JIT. Lors du premier appel de la mĂ©thode, celle-ci est compilĂ©e (et oui, ce processus n'est pas rapide), lors des appels suivants, le contrĂŽle est donnĂ© Ă  la mĂ©thode dĂ©jĂ  compilĂ©e, et ici il n'y aura pas de chute de performance particuliĂšre.

Dans notre cas, nous pouvons Ă©galement utiliser la compilation JIT et ensuite utiliser le comportement compilĂ© avec les mĂȘmes performances que ses analogues AOT. Les expressions nous aideront dans ce cas.

On peut résumer le principe en question de la maniÚre suivante :
Il est conseillĂ© de mettre en cache le rĂ©sultat final du travail de la rĂ©flexion sous la forme d'un dĂ©lĂ©guĂ© contenant la fonction compilĂ©e. Tous les objets nĂ©cessaires contenant des informations sur les types devraient Ă©galement ĂȘtre mis en cache dans les champs de votre type – le worker.

Il y a une logique dans cela. Le bon sens nous dit que si quelque chose peut ĂȘtre compilĂ© et mis en cache, il vaut mieux le faire.

Pour anticiper, il convient de dire que le cache dans le travail avec la rĂ©flexion a ses avantages, mĂȘme sans utiliser la mĂ©thode de compilation d'expressions proposĂ©e. En fait, je vais simplement rĂ©pĂ©ter les thĂšses de l'auteur de l'article auquel je fais rĂ©fĂ©rence ci-dessus.

Maintenant, parlons du code. Examinons un exemple basé sur ma récente douleur à laquelle j'ai dû faire face dans une production sérieuse d'une grande organisation de crédit. Tous les entités sont fictifs, afin que personne ne devine.

Il y a une certaine entité. Appelons-la Contact. Il y a des courriels avec un corps standardisé, à partir desquels le parser et le hydrator créent ces contacts. Un email est arrivé, nous l'avons lu, décomposé en paires clé-valeur, nous avons créé un contact et l'avons enregistré dans la base de données.

C'est Ă©lĂ©mentaire. Supposons qu'un contact ait des propriĂ©tĂ©s telles que Nom, Âge et numĂ©ro de tĂ©lĂ©phone. Ces donnĂ©es sont transmises dans l'email. De plus, l'entreprise souhaite que les supports puissent rapidement ajouter des clĂ©s pour mapper les propriĂ©tĂ©s des entitĂ©s sur des paires dans le corps de l'email. Dans le cas oĂč quelqu'un aurait fait une faute de frappe dans le modĂšle ou si, avant la sortie, il est nĂ©cessaire de lancer rapidement le mappage avec un nouveau partenaire tout en s'adaptant Ă  un nouveau format. Nous pourrons alors ajouter cette nouvelle corrĂ©lation de mappage comme un correctif de donnĂ©es Ă  faible coĂ»t. C'est-Ă -dire, un exemple de la vie rĂ©elle.

Nous rĂ©alisons et crĂ©ons des tests. Ça fonctionne.

Je ne fournirai pas le code : il y a beaucoup de sources, et elles sont disponibles sur GitHub via le lien Ă  la fin de l'article. Vous pouvez les tĂ©lĂ©charger, les malmener au point de ne pas les reconnaĂźtre et mesurer comment cela agirait dans votre cas. Je fournirai seulement le code de deux mĂ©thodes template, qui distinguent le hydrateur qui devait ĂȘtre rapide de celui qui devait ĂȘtre lent.

La logique est la suivante : la mĂ©thode template reçoit des paires formĂ©es par la logique de base du parseur. Le niveau LINQ sert de parseur et de logique de base pour le hydrateur, Ă©tablissant une requĂȘte au contexte de la base de donnĂ©es et faisant correspondre les clĂ©s avec les paires du parseur (pour ces fonctions, il y a du code sans LINQ pour comparaison). Ensuite, les paires sont transmises Ă  la mĂ©thode principale d'hydratation et les valeurs des paires sont dĂ©finies dans les propriĂ©tĂ©s correspondantes de l'entitĂ©.

«Rapide» (Préfixe Fast dans les benchmarks) :

 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;
        }

Comme nous le voyons, une collection statique de setters de propriĂ©tĂ©s est utilisĂ©e – des lambdas compilĂ©es qui appellent le setter de l'entitĂ©. Elles sont créées par le code suivant :

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

        private static Action 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>)Expression.Lambda(setterExp, paramEntity, paramValueOriginal);

            return lambda.Compile();
        }

Dans l'ensemble, c'est clair. Nous parcourons les propriétés, créons des délégués qui appellent les setters et les sauvegardons. Ensuite, nous les appelons quand cela est nécessaire.

«Lent» (Préfixe Slow dans les benchmarks) :

        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;
        }

Ici, nous parcourons immédiatement les propriétés et appelons directement SetValue.

Pour visualiser et comme rĂ©fĂ©rence, j'ai mis en Ɠuvre une mĂ©thode naĂŻve qui Ă©crit les valeurs de leurs paires de corrĂ©lation directement dans les champs de l'entitĂ©. PrĂ©fixe – Manual.

Prenons maintenant BenchmarkDotNet et examinons la performance. Et soudain
 (spoiler – ce n'est pas le bon rĂ©sultat, plus de dĂ©tails ci-dessous)

Un article raté sur l'accélération de la réflexion

Que voyons-nous ici ? Les mĂ©thodes arborant fiĂšrement le prĂ©fixe Fast se rĂ©vĂšlent souvent plus lentes que celles avec le prĂ©fixe Slow dans presque tous les passages. Cela est vrai tant pour l'allocation que pour la vitesse d'exĂ©cution. D'un autre cĂŽtĂ©, une implĂ©mentation jolie et Ă©lĂ©gante de mappage utilisant partout oĂč c'est possible les mĂ©thodes LINQ appropriĂ©es consomme de maniĂšre significative de la performance. La diffĂ©rence est d'un ordre de grandeur. La tendance ne change pas avec le nombre de passages. La diffĂ©rence n'est qu'une question d'Ă©chelle. Avec LINQ, c'est de 4 Ă  200 fois plus lent, et il y a environ la mĂȘme ampleur de dĂ©chets.

Mise Ă  jour

Je n'en croyais pas mes yeux, mais ce qui est plus important, ni mes yeux ni mon code n'ont cru notre collĂšgue — Dmitry Tikhonov 0x1000000. En vĂ©rifiant ma solution, il a brillamment dĂ©couvert et signalĂ© l'erreur que j'avais manquĂ©e Ă  cause des modifications apportĂ©es de la mise en Ɠuvre initiale Ă  la finale. AprĂšs avoir corrigĂ© le bug trouvĂ© dans la configuration de Moq, tous les rĂ©sultats se sont remis en place. Selon les rĂ©sultats du nouveau test, la tendance principale ne change pas : LINQ influencera toujours la performance plus que la rĂ©flexion. Pourtant, il est agrĂ©able de constater que le travail avec la compilation des Expressions n'est pas vain, et le rĂ©sultat est visible tant en allocation qu'en temps d'exĂ©cution. Le premier lancement, lorsque les champs statiques sont initialisĂ©s, est logiquement plus lent pour la mĂ©thode « rapide », mais la situation change par la suite.

Voici le résultat du nouveau test :

Un article raté sur l'accélération de la réflexion

Conclusion : lors de l'utilisation de la rĂ©flexion en entreprise, il n'est pas nĂ©cessaire de recourir Ă  des astuces particuliĂšres – LINQ dĂ©vorera la performance davantage. Cependant, dans les mĂ©thodes hautement sollicitĂ©es nĂ©cessitant une optimisation, il est possible de conserver la rĂ©flexion sous forme d'initialisateurs et de compilateurs de dĂ©lĂ©guĂ©s, qui garantiront ensuite la logique « rapide ». Ainsi, vous pouvez Ă  la fois prĂ©server la flexibilitĂ© de la rĂ©flexion et la rapiditĂ© d'exĂ©cution de l'application.

Le code avec le benchmark est disponible ici. Tous ceux qui le souhaitent peuvent vérifier mes propos :
HabraReflectionTests

PS : le code dans les tests utilise l'Ioc, tandis que dans les benchmarks, il utilise une construction explicite. En fait, dans l'implémentation finale, j'ai éliminé tous les facteurs susceptibles d'affecter la performance et de brouiller les résultats.

PPS : Merci à l'utilisateur Dmitry Tikhonov @0x1000000 pour avoir découvert mon erreur dans la configuration de Moq, qui a eu un impact sur les premiÚres mesures. Si certains lecteurs ont assez de karma, merci de le liker. Cette personne a pris le temps d'étudier, de vérifier et de signaler l'erreur. Je considÚre cela comme digne de respect et de sympathie.

PPPS : merci à ce lecteur pointilleux qui a critiqué le style et la présentation. Je suis pour l'uniformité et la commodité. La diplomatie dans la présentation laisse à désirer, mais j'ai pris en compte la critique. Je vous remercie.

Source : habr.com

Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS đŸ”„ Acheter un hĂ©bergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster