Articulu senza successu nantu à l'accelerazione di a riflessione

Spiegheraghju subitu u titulu di l'articulu. U pianu uriginale era di dà cunsiglii boni è affidabili nantu à cumu accelerà l'usu di a riflessione cù un esempiu simplice ma realistu, ma durante u benchmarking hè risultatu chì a riflessione ùn hè micca cusì lenta cum'è pensava, LINQ hè più lento chì in i mo incubi. Ma à a fine hè risultatu chì aghju fattu ancu un sbagliu in e misurazioni... I dettagli di sta storia di vita sò sottu à u tagliu è in i cumenti. Siccomu l'esempiu hè abbastanza cumunu è implementatu in principiu cum'è di solitu fattu in una impresa, hè diventatu assai interessante, cum'è mi pari, dimustrazione di a vita: l'impattu nantu à a vitezza di u sughjettu principale di l'articulu hè statu. micca notu per via di logica esterna: Moq, Autofac, EF Core è altri "bandings".

Aghju cuminciatu à travaglià sottu l'impressione di questu articulu: Perchè a riflessione hè lenta

Comu pudete vede, l'autore suggerisce l'usu di delegati compilati invece di chjamà direttamente i metudi di tipu di riflessione cum'è un modu grandile per accelerà assai l'applicazione. Ci hè, sicuru, l'emissione di IL, ma vogliu evitari, postu chì questu hè u modu più intensivu di travagliu per fà u compitu, chì hè pienu di errore.

Cunsiderendu chì aghju sempre avutu una opinione simili nantu à a rapidità di riflessione, ùn aghju micca particularmente intesu à interrogà e cunclusioni di l'autore.

Aghju spessu scontru l'usu ingenu di a riflessione in l'impresa. U tipu hè pigliatu. L'infurmazione nantu à a pruprietà hè presa. U metudu SetValue hè chjamatu è tutti si rallegranu. U valore hè ghjuntu in u campu di destinazione, tutti sò felici. E persone assai intelligenti - anziani è capi di squadra - scrivenu e so estensioni à l'ughjettu, basatu annantu à una implementazione cusì ingenua mappe "universali" di un tipu à l'altru. L'essenza hè di solitu questu: pigliamu tutti i campi, pigliamu tutte e pruprietà, iterate nantu à elli: se i nomi di i membri di u tipu currispondenu, eseguimu SetValue. Da u tempu à u tempu cattumu eccezzioni per i sbagli induve ùn avemu micca truvatu qualchi pruprietà in unu di i tipi, ma ancu quì ci hè una manera di esce chì migliurà u rendiment. Pruvate / catturà.

Aghju vistu a ghjente reinventà parsers è mappers senza esse cumplettamente armati cù infurmazioni nantu à cumu funziona e macchine chì sò venute prima di elli. Aghju vistu a ghjente ammuccià e so implementazioni ingenu daretu à strategie, daretu à l'interfacce, daretu à l'injections, cum'è s'ellu scusava i baccanali successivi. Aghju vultatu u nasu à tali realizazioni. In fattu, ùn aghju micca misuratu a perdita di rendiment reale, è, se pussibule, aghju cambiatu solu l'implementazione à una più "ottima" se puderia mette in manu. Dunque, i primi misurazioni discututi quì sottu m'hà cunfusu seriamente.

Pensu chì parechji di voi, leghjendu Richter o altri ideologi, anu scontru una dichjarazione cumplettamente ghjusta chì a riflessione in codice hè un fenomenu chì hà un impattu estremamente negativu nantu à u rendiment di l'applicazione.

Chjama a riflessione forza u CLR à passà per l'assemblee per truvà quellu chì anu bisognu, tirà e so metadata, analizà, etc. Inoltre, a riflessione mentre traversa e sequenze porta à l'attribuzione di una grande quantità di memoria. Avemu aduprà a memoria, CLR scopre u GC è i frisi cumincianu. Duverebbe esse notevolmente lento, credimi. L'enorme quantità di memoria nantu à i servitori di produzzione muderni o macchine di nuvola ùn impediscenu micca ritardi elevati di trasfurmazioni. In fatti, a più memoria, u più prubabile chì avete AVVISU cumu funziona u GC. A riflessione hè, in teoria, un straccio rossu extra per ellu.

Tuttavia, avemu tutti aduprà cuntenituri IoC è data mappers, u principiu di u funziunamentu di u quali hè dinù basatu nantu à a riflessione, ma ùn ci sò di solitu nudda dumanni circa u so funziunamentu. No, micca perchè l'intruduzioni di dependenzii è l'astrazione da i mudelli di cuntestu limitatu esternu sò cusì necessarii chì avemu da sacrificà a prestazione in ogni casu. Tuttu hè più simplice - ùn affetta micca assai u rendiment.

U fattu hè chì i quadri più cumuni chì sò basati nantu à a tecnulugia di riflessione utilizanu ogni tipu di trucchi per travaglià cun ellu più ottimali. Di solitu questu hè un cache. Di genere, questi sò Espressioni è delegati compilati da l'arburu di l'espressione. U stessu automapper mantene un dizziunariu cumpetitivu chì currisponde à i tipi cù funzioni chì ponu cunvertisce unu in l'altru senza chjamà riflessione.

Cumu hè questu questu? Essenzialmente, questu ùn hè micca sfarente da a logica chì a piattaforma stessa usa per generà codice JIT. Quandu un metudu hè chjamatu per a prima volta, hè cumpilatu (è, iè, stu prucessu ùn hè micca veloce); in i chjami successivi, u cuntrollu hè trasferitu à u metudu digià compilatu, è ùn ci sarà micca scontri di rendiment significativu.

In u nostru casu, pudete ancu aduprà a compilazione JIT è dopu aduprà u cumpurtamentu cumpilatu cù u listessu rendimentu cum'è i so contraparti AOT. L'espressioni venenu à u nostru aiutu in questu casu.

U principiu in questione pò esse formulatu brevemente cusì:
Duvete cache u risultatu finali di a riflessione cum'è delegatu chì cuntene a funzione compilata. Hè ancu sensu di cache tutti l'uggetti necessarii cù l'infurmazioni di tipu in i campi di u vostru tipu, u travagliu, chì sò almacenati fora di l'uggetti.

Ci hè logica in questu. U sensu cumunu ci dice chì se qualcosa pò esse compilatu è cache, allora deve esse fattu.

In u futuru, deve esse dettu chì u cache in u travagliu cù a riflessione hà i so vantaghji, ancu s'ellu ùn utilizate micca u metudu prupostu di cumpilà espressioni. In verità, quì sò solu ripetiri e tesi di l'autore di l'articulu à quale mi riferite sopra.

Avà circa u codice. Fighjemu un esempiu chì hè basatu annantu à u mo dulore recente chì aghju avutu affruntà in una pruduzzione seria di una istituzione di creditu seriu. Tutte e entità sò fittizie per chì nimu ùn hà da invintà.

Ci hè una certa essenza. Chì ci sia u Cuntattu. Ci sò lettere cù un corpu standardizatu, da quale u parser è l'hydrator creanu sti stessi cuntatti. Una lettera hè ghjunta, l'avemu lettu, l'avete analizatu in coppie chjave-valore, criatu un cuntattu è salvatu in a basa di dati.

Hè elementari. Diciamu chì un cuntattu hà e pruprietà Nome Completu, Età è Telefono di Cuntattu. Questa data hè trasmessa in a lettera. L'affari vole ancu supportu per pudè aghjustà rapidamente e chjavi novi per mapping proprietà di l'entità in coppie in u corpu di a lettera. In casu chì qualchissia hà fattu un typo in u mudellu o se prima di a liberazione hè necessariu di lancià urgentemente a mappatura da un novu cumpagnu, adattendu à u novu formatu. Allora pudemu aghjunghje una nova correlazione di mappatura cum'è una correlazione di dati economica. Questu hè, un esempiu di vita.

Implementemu, criemu testi. Opere.

Ùn furnisce micca u codice: ci sò parechje fonti, è sò dispunibuli nantu à GitHub via u ligame à a fine di l'articulu. Pudete caricalli, torturalli fora di ricunniscenza è misurà, cum'è affettanu in u vostru casu. Daraghju solu u codice di dui metudi di mudelli chì distinguenu l'hydrator, chì duvia esse veloce, da l'hydrator, chì duvia esse lentu.

A logica hè a siguenti: u metudu di u mudellu riceve coppie generate da a logica parser basica. A capa LINQ hè u parser è a logica basica di l'hydrator, chì face una dumanda à u cuntestu di a basa di dati è paragunate e chjave cù coppie da u parser (per queste funzioni ci hè un codice senza LINQ per paragunà). In seguitu, i coppie sò passati à u metudu di idratazione principale è i valori di e coppie sò stabiliti à e proprietà currispundenti di l'entità.

"Fast" (Prefix Fast in 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;
        }

Comu pudemu vede, una cullizzioni statica cù proprietà di setter hè aduprata - lambdas compilati chì chjamanu l'entità setter. Creatu da u codice seguente:

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

In generale hè chjaru. Traversemu e pruprietà, creanu delegati per elli chì chjamanu setters, è salvemu. Allora chjamemu quandu hè necessariu.

"Slow" (Prefissu Slow in 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;
        }

Quì sguassate immediatamente e proprietà è chjamate SetValue direttamente.

Per chiarezza è cum'è riferimentu, aghju implementatu un metudu ingenu chì scrive i valori di e so coppie di correlazione direttamente in i campi di l'entità. Prefissu - Manuale.

Avà pigliamu BenchmarkDotNet è esaminà u rendiment. È di colpu ... (spoiler - questu ùn hè micca u risultatu currettu, i dettagli sò quì sottu)

Articulu senza successu nantu à l'accelerazione di a riflessione

Chì vedemu quì ? I metudi chì portanu triunfalmente u prefissu Fast si sò più lenti in quasi tutti i passaggi cà i metudi cù u prefissu Slow. Questu hè veru per l'attribuzione è a rapidità di u travagliu. Per d 'altra banda, una bella è elegante implementazione di mapping utilizendu metudi LINQ destinati à questu induve pussibule, à u cuntrariu, riduce assai a produtividade. A diferenza hè di ordine. A tendenza ùn cambia micca cù parechji numeri di passaghji. L'unica diferenza hè in scala. Cù LINQ hè 4 - 200 volte più lento, ci hè più basura à circa a listessa scala.

Messu à ghjornu

Ùn aghju micca cridutu à i mo ochji, ma più importantemente, u nostru cullegu ùn hà micca cridutu nè à i mo ochji nè à u mo codice - Dmitry Tikhonov 0x1000000. Dopu avè verificatu a mo suluzione, hà scupertu brillanti è indicò un errore chì aghju mancatu per via di una quantità di cambiamenti in l'implementazione, iniziali à finali. Dopu avè riparatu u bug truvatu in a cunfigurazione di Moq, tutti i risultati sò stati in u locu. Sicondu i risultati di retest, a tendenza principale ùn cambia micca - LINQ affetta sempre u rendiment più cà a riflessione. In ogni casu, hè bellu chì u travagliu cù a compilazione di Expression ùn hè micca fattu in vain, è u risultatu hè visibile sia in l'assignazione sia in u tempu d'esekzione. U primu lanciu, quandu i campi statichi sò inizializzati, hè naturalmente più lento per u metudu "rapidu", ma dopu a situazione cambia.

Eccu u risultatu di u retest:

Articulu senza successu nantu à l'accelerazione di a riflessione

Conclusioni: quandu si usa a riflessione in una impresa, ùn ci hè micca bisognu particulare di ricurdà à i trucchi - LINQ manghjarà più a produtividade. In ogni casu, in i metudi d'alta carica chì necessitanu ottimisazione, pudete salvà a riflessione in forma di inizializzatori è compilatori delegati, chì furnisceranu logica "rapida". Questu modu pudete mantene a flessibilità di riflessione è a rapidità di l'applicazione.

U codice di benchmark hè dispunibule quì. Qualchissia pò verificà e mo parolle:
HabraReflectionTests

PS: u codice in i testi usa IoC, è in i benchmarks usa una custruzzione esplicita. U fattu hè chì in l'implementazione finale aghju tagliatu tutti i fatturi chì puderanu affettà u rendiment è facenu u risultatu rumoroso.

PPS: Grazie à l'utilizatori Dmitry Tikhonov @0x1000000 per scopre u mo errore in a stallazione di Moq, chì hà affettatu e prime misure. Sì qualchissia di i lettori hà abbastanza karma, per piacè piace. L'omu si firmò, l'omu hà lettu, l'omu hà verificatu duie volte è hà indicatu l'errore. Pensu chì questu hè degnu di rispettu è simpatia.

PPPS: grazia à u lettore meticulosu chì hè ghjuntu à u fondu di u stilu è u disignu. Sò per uniformità è cunvenzione. A diplomazia di a presentazione lassa assai per esse desideratu, ma aghju pigliatu a critica in contu. Dumandu u prughjettu.

Source: www.habr.com

Add a comment