Malsukcesa artikolo pri akcelado de pripensado

Mi tuj klarigos la titolon de la artikolo. La originala plano estis doni bonajn, fidindajn konsilojn pri kiel plirapidigi la uzon de pripensado uzante simplan sed realisman ekzemplon, sed dum benchmarking montriĝis, ke pripensado ne estas tiel malrapida kiel mi pensis, LINQ estas pli malrapida ol en miaj koŝmaroj. Sed finfine montriĝis, ke mi ankaŭ eraris en la mezuradoj... Detaloj de ĉi tiu vivrakonto estas sub la tranĉo kaj en la komentoj. Ĉar la ekzemplo estas sufiĉe ordinara kaj efektivigita principe kiel oni kutime faras en entrepreno, ĝi montriĝis sufiĉe interesa, kiel ŝajnas al mi, pruvo de la vivo: la efiko al la rapideco de la ĉefa temo de la artikolo estis ne rimarkebla pro ekstera logiko: Moq, Autofac, EF Core kaj aliaj "bandings".

Mi eklaboris sub la impreso de ĉi tiu artikolo: Kial Reflektado estas malrapida

Kiel vi povas vidi, la aŭtoro sugestas uzi kompilitajn delegitojn anstataŭ rekte voki reflektotipajn metodojn kiel bonega maniero por ege akceli la aplikaĵon. Estas, kompreneble, IL-emisio, sed mi ŝatus eviti ĝin, ĉar ĉi tio estas la plej labor-intensa maniero plenumi la taskon, kiu estas plena de eraroj.

Konsiderante, ke mi ĉiam havis similan opinion pri la rapideco de pripensado, mi ne precipe intencis pridubi la konkludojn de la aŭtoro.

Mi ofte renkontas naivan uzon de pripensado en la entrepreno. La tipo estas prenita. Informoj pri la posedaĵo estas prenitaj. La metodo SetValue estas vokita kaj ĉiuj ĝojas. La valoro alvenis en la celkampon, ĉiuj estas feliĉaj. Tre inteligentaj homoj - maljunuloj kaj teamgvidantoj - skribas siajn etendaĵojn por oponi, surbaze de tia naiva efektivigo "universalaj" mapistoj de unu tipo al alia. La esenco kutime estas ĉi tio: ni prenas ĉiujn kampojn, prenas ĉiujn ecojn, ripetas super ili: se la nomoj de la tipo-membroj kongruas, ni ekzekutas SetValue. De tempo al tempo ni kaptas esceptojn pro eraroj, kie ni ne trovis iun posedaĵon en unu el la tipoj, sed eĉ ĉi tie estas elirejo, kiu plibonigas rendimenton. Provu/kapti.

Mi vidis homojn reinventi analizantojn kaj mapistojn sen esti plene armitaj kun informoj pri kiel funkcias la maŝinoj kiuj venis antaŭ ili. Mi vidis homojn kaŝi siajn naivajn realigojn malantaŭ strategioj, malantaŭ interfacoj, malantaŭ injektoj, kvazaŭ tio senkulpigus la postan bakanalion. Mi levis la nazon ĉe tiaj konstatoj. Fakte, mi ne mezuris la realan rendimentan likon, kaj, se eble, mi simple ŝanĝis la efektivigon al pli "optimuma" se mi povus akiri miajn manojn sur ĝin. Tial, la unuaj mezuradoj priparolataj malsupre serioze konfuzis min.

Mi pensas, ke multaj el vi, legante Richter aŭ aliajn ideologojn, trovis tute justan deklaron, ke reflektado en kodo estas fenomeno kiu havas ege negativan efikon al la agado de la aplikaĵo.

Voki reflektadon devigas la CLR ekzameni asembleojn por trovi tiun, kiun ili bezonas, eltiri siajn metadatenojn, analizi ilin ktp. Krome, reflektado dum krucado de sekvencoj kondukas al la asigno de granda kvanto de memoro. Ni eluzas memoron, CLR malkovras la GC kaj frisoj komenciĝas. Ĝi devus esti rimarkeble malrapida, kredu min. La grandegaj kvantoj da memoro sur modernaj produktaj serviloj aŭ nubaj maŝinoj ne malhelpas altajn prokrastajn prokrastojn. Fakte, ju pli da memoro, des pli verŝajne vi RIMARKOS kiel funkcias la GC. Reflektado estas, en teorio, ekstra ruĝa ĉifono por li.

Tamen ni ĉiuj uzas IoC-ujojn kaj datmapistojn, kies funkcia principo ankaŭ baziĝas sur pripensado, sed kutime ne estas demandoj pri ilia agado. Ne, ne ĉar la enkonduko de dependecoj kaj abstraktado de eksteraj limigitaj kuntekstaj modeloj estas tiel necesaj, ke ni devas ĉiukaze oferi rendimenton. Ĉio estas pli simpla - ĝi vere ne multe influas rendimenton.

La fakto estas, ke la plej oftaj kadroj, kiuj baziĝas sur reflekta teknologio, uzas ĉiajn lertaĵojn por labori kun ĝi pli optimume. Kutime ĉi tio estas kaŝmemoro. Tipe ĉi tiuj estas Esprimoj kaj delegitoj kompilitaj de la esprimarbo. La sama aŭtomapper konservas konkurencivan vortaron kiu kongruas tipojn kun funkcioj kiuj povas konverti unu en alian sen voki reflektadon.

Kiel tio estas atingita? Esence, ĉi tio ne diferencas de la logiko, kiun la platformo mem uzas por generi JIT-kodon. Kiam metodo estas vokita por la unua fojo, ĝi estas kompilita (kaj, jes, ĉi tiu procezo ne estas rapida); en postaj vokoj, kontrolo estas transdonita al la jam kompilita metodo, kaj ne estos signifaj rendimentaj malkreskoj.

En nia kazo, vi ankaŭ povas uzi JIT-kompilon kaj poste uzi la kompilitan konduton kun la sama agado kiel ĝiaj AOT-ekvivalentoj. Esprimoj venos al nia helpo en ĉi tiu kazo.

La koncerna principo povas esti mallonge formulita jene:
Vi devus konservi la finan rezulton de pripensado kiel delegito enhavanta la kompilitan funkcion. Ankaŭ havas sencon konservi ĉiujn necesajn objektojn kun tipinformoj en la kampoj de via tipo, la laboristo, kiuj estas konservitaj ekster la objektoj.

Estas logiko en ĉi tio. Komuna prudento diras al ni, ke se io povas esti kompilita kaj konservita, tiam ĝi devus esti farita.

Rigardante antaŭen, oni devas diri, ke la kaŝmemoro en laborado kun reflektado havas siajn avantaĝojn, eĉ se oni ne uzas la proponitan metodon por kompili esprimojn. Efektive, ĉi tie mi simple ripetas la tezojn de la aŭtoro de la artikolo, al kiu mi referencas supre.

Nun pri la kodo. Ni rigardu ekzemplon, kiu baziĝas sur mia lastatempa doloro, kiun mi devis alfronti en serioza produktado de serioza kredita institucio. Ĉiuj estaĵoj estas fikciaj, por ke neniu divenus.

Estas iu esenco. Estu Kontakto. Estas literoj kun normigita korpo, el kiuj la analizanto kaj hidratilo kreas ĉi tiujn samajn kontaktojn. Alvenis letero, ni legis ĝin, analizis ĝin en ŝlosilvalorajn parojn, kreis kontakton kaj konservis ĝin en la datumbazo.

Ĝi estas elementa. Ni diru, ke kontakto havas la ecojn Plena Nomo, Aĝo kaj Kontakta Telefono. Ĉi tiuj datumoj estas transdonitaj en la letero. La komerco ankaŭ volas subtenon por povi rapide aldoni novajn ŝlosilojn por mapado de entaj propraĵoj en parojn en la korpo de la letero. Se iu faris tajperarojn en la ŝablono aŭ se antaŭ la liberigo necesas urĝe lanĉi mapadon de nova partnero, adaptiĝante al la nova formato. Tiam ni povas aldoni novan mapan korelacion kiel malmultekosta datumfikso. Tio estas, vivekzemplo.

Ni efektivigas, kreas testojn. Verkoj.

Mi ne provizos la kodon: estas multaj fontoj, kaj ili haveblas ĉe GitHub per la ligilo ĉe la fino de la artikolo. Vi povas ŝarĝi ilin, torturi ilin preter rekono kaj mezuri ilin, kiel ĝi influus en via kazo. Mi nur donos la kodon de du ŝablonaj metodoj, kiuj distingas la hidratigilon, kiu laŭsupoze estis rapida, de la hidratigilo, kiu laŭsupoze estis malrapida.

La logiko estas kiel sekvas: la ŝablonmetodo ricevas parojn generitajn de la baza analizistologiko. La LINQ-tavolo estas la analizilo kaj la baza logiko de la hydrator, kiu faras peton al la datumbaza kunteksto kaj komparas ŝlosilojn kun paroj de la analizilo (por ĉi tiuj funkcioj ekzistas kodo sen LINQ por komparo). Poste, la paroj estas pasigitaj al la ĉefa hidratiga metodo kaj la valoroj de la paroj estas fiksitaj al la respondaj propraĵoj de la ento.

"Rapida" (Prefikso Rapida en komparnormoj):

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

Kiel ni povas vidi, estas uzata statika kolekto kun setter-ecoj - kompilitaj lambdoj, kiuj nomas la setter-entaĵon. Kreite per la sekva kodo:

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

Ĝenerale estas klare. Ni trairas la proprietojn, kreas delegitojn por ili, kiuj vokas setters, kaj konservas ilin. Tiam ni vokas kiam necese.

"Malrapida" (Prefikso Malrapida en komparnormoj):

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

Ĉi tie ni tuj preterpasas la ecojn kaj vokas SetValue rekte.

Por klareco kaj kiel referenco, mi efektivigis naivan metodon, kiu skribas la valorojn de iliaj korelaciaj paroj rekte en la entajn kampojn. Prefikso – Manlibro.

Nun ni prenu BenchmarkDotNet kaj ekzamenu la agadon. Kaj subite... (spoiler - ĉi tio ne estas la ĝusta rezulto, detaloj estas sube)

Malsukcesa artikolo pri akcelado de pripensado

Kion ni vidas ĉi tie? Metodoj kiuj triumfe portas la Rapidan prefikson montriĝas pli malrapidaj en preskaŭ ĉiuj paŝoj ol metodoj kun la Malrapida prefikso. Ĉi tio validas kaj por asigno kaj rapideco de laboro. Aliflanke, bela kaj eleganta efektivigo de mapado uzante LINQ-metodojn destinitajn por tio ĉie, kie eblas, male, multe reduktas produktivecon. La diferenco estas de ordo. La tendenco ne ŝanĝas kun malsamaj nombroj da enirpermesiloj. La sola diferenco estas en skalo. Kun LINQ ĝi estas 4 - 200 fojojn pli malrapida, estas pli da rubo sur proksimume la sama skalo.

Ĝisdatigita

Mi ne kredis miajn okulojn, sed pli grave, nia kolego kredis nek miajn okulojn nek mian kodon - Dmitrij Tiĥonov 0x1000000. Duoble kontrolinte mian solvon, li brile malkovris kaj atentigis eraron, kiun mi maltrafis pro kelkaj ŝanĝoj en la efektivigo, komencante ĝis fina. Post ripari la trovitan cimon en la agordo de Moq, ĉiuj rezultoj ekfunkciis. Laŭ la retestrezultoj, la ĉefa tendenco ne ŝanĝiĝas - LINQ ankoraŭ influas rendimenton pli ol reflektadon. Tamen, estas agrable, ke la laboro kun Expression-kompilo ne estas vane farita, kaj la rezulto estas videbla kaj en asigno kaj ekzekuttempo. La unua lanĉo, kiam senmovaj kampoj estas pravigitaj, estas nature pli malrapida por la "rapida" metodo, sed tiam la situacio ŝanĝiĝas.

Jen la rezulto de la retesto:

Malsukcesa artikolo pri akcelado de pripensado

Konkludo: kiam oni uzas reflektadon en entrepreno, ne necesas speciale recurri al lertaĵoj - LINQ pli manĝos produktivecon. Tamen, en metodoj de alta ŝarĝo, kiuj postulas optimumigon, vi povas ŝpari reflektadon en formo de inicialigiloj kaj delegitaj kompililoj, kiuj tiam provizos "rapidan" logikon. Tiel vi povas konservi kaj la flekseblecon de reflektado kaj la rapidecon de la aplikaĵo.

La komparnorma kodo haveblas ĉi tie. Ĉiu povas kontroli miajn vortojn:
HabraReflectionTests

PS: la kodo en la testoj uzas IoC, kaj en la benchmarks ĝi uzas eksplicitan konstruaĵon. La fakto estas, ke en la fina efektivigo mi fortranĉis ĉiujn faktorojn, kiuj povus influi rendimenton kaj fari la rezulton brua.

PPS: Dankon al la uzanto Dmitrij Tiĥonov @0x1000000 por malkovri mian eraron pri agordo de Moq, kiu influis la unuajn mezuradojn. Se iu el la legantoj havas sufiĉan karmon, bonvolu ŝati ĝin. La viro haltis, la viro legis, la viro duoble kontrolis kaj montris la eraron. Mi pensas, ke ĉi tio estas inda je respekto kaj simpatio.

PPPS: danke al la zorgema leganto, kiu atingis la fundon de la stilo kaj dezajno. Mi estas por unuformeco kaj komforto. La diplomatio de la prezento lasas multon por deziri, sed mi konsideris la kritikon. Mi petas la ĵetaĵon.

fonto: www.habr.com

Aldoni komenton