Hindi matagumpay na artikulo tungkol sa pagpapabilis ng pagmuni-muni

Ipapaliwanag ko kaagad ang pamagat ng artikulo. Ang orihinal na plano ay upang magbigay ng mahusay, maaasahang payo kung paano mapabilis ang paggamit ng pagmuni-muni gamit ang isang simple ngunit makatotohanang halimbawa, ngunit sa panahon ng pag-benchmark ay lumabas na ang pagmuni-muni ay hindi kasingbagal ng iniisip ko, ang LINQ ay mas mabagal kaysa sa aking mga bangungot. Pero sa huli ay nagkamali din pala ako sa mga sukat... Ang mga detalye ng kwento ng buhay na ito ay nasa ilalim ng hiwa at sa mga komento. Dahil ang halimbawa ay medyo pangkaraniwan at ipinatupad sa prinsipyo tulad ng karaniwang ginagawa sa isang negosyo, ito ay naging isang kawili-wili, tulad ng sa tingin ko, pagpapakita ng buhay: ang epekto sa bilis ng pangunahing paksa ng artikulo ay hindi napapansin dahil sa external logic: Moq, Autofac, EF Core at iba pang "bandings".

Nagsimula akong magtrabaho sa ilalim ng impresyon ng artikulong ito: Bakit mabagal ang Reflection

Tulad ng nakikita mo, iminumungkahi ng may-akda ang paggamit ng mga pinagsama-samang delegado sa halip na direktang tawagan ang mga pamamaraan ng uri ng pagmuni-muni bilang isang mahusay na paraan upang lubos na mapabilis ang aplikasyon. Mayroong, siyempre, ang paglabas ng IL, ngunit nais kong iwasan ito, dahil ito ang pinaka-malakas na paraan upang maisagawa ang gawain, na puno ng mga pagkakamali.

Isinasaalang-alang na palagi akong may hawak na katulad na opinyon tungkol sa bilis ng pagmuni-muni, hindi ko partikular na nilayon na tanungin ang mga konklusyon ng may-akda.

Madalas akong nakatagpo ng walang muwang na paggamit ng pagmuni-muni sa negosyo. Yung tipong kinukuha. Kinukuha ang impormasyon tungkol sa ari-arian. Tinatawag ang SetValue method at lahat ay nagagalak. Ang halaga ay dumating sa target na larangan, lahat ay masaya. Mga napakatalino na tao - mga nakatatanda at pinuno ng koponan - isulat ang kanilang mga extension upang tumutol, batay sa gayong walang muwang na pagpapatupad ng mga "unibersal" na mapper ng isang uri sa isa pa. Ang kakanyahan ay kadalasang ito: kinukuha namin ang lahat ng mga patlang, kinukuha ang lahat ng mga pag-aari, inuulit ang mga ito: kung ang mga pangalan ng uri ng mga miyembro ay tumutugma, ipapatupad namin ang SetValue. Paminsan-minsan ay nakakakuha kami ng mga pagbubukod dahil sa mga pagkakamali kung saan hindi kami nakahanap ng ilang pag-aari sa isa sa mga uri, ngunit kahit na dito ay may isang paraan out na nagpapabuti sa pagganap. Subukan/huli.

Nakita ko ang mga tao na muling nag-imbento ng mga parser at mapper nang hindi ganap na armado ng impormasyon tungkol sa kung paano gumagana ang mga machine na nauna sa kanila. Nakita ko ang mga tao na itinago ang kanilang mga walang muwang na pagpapatupad sa likod ng mga diskarte, sa likod ng mga interface, sa likod ng mga iniksyon, na para bang ipagpaumanhin nito ang kasunod na bacchanalia. Napaangat ang ilong ko sa mga ganitong realisasyon. Sa katunayan, hindi ko sinukat ang tunay na pagtagas ng pagganap, at, kung maaari, binago ko lang ang pagpapatupad sa isang mas "pinakamainam" kung maaari kong makuha ang aking mga kamay dito. Samakatuwid, ang mga unang sukat na tinalakay sa ibaba ay seryosong nalito sa akin.

Sa tingin ko marami sa inyo, na nagbabasa ng Richter o iba pang mga ideologist, ay nakatagpo ng isang ganap na patas na pahayag na ang pagmuni-muni sa code ay isang phenomenon na may lubhang negatibong epekto sa pagganap ng application.

Pinipilit ng pagtawag sa reflection ang CLR na dumaan sa mga assemblies para mahanap ang kailangan nila, hilahin ang kanilang metadata, i-parse ang mga ito, atbp. Bilang karagdagan, ang pagmuni-muni habang binabagtas ang mga pagkakasunud-sunod ay humahantong sa paglalaan ng isang malaking halaga ng memorya. Gumagamit kami ng memorya, natuklasan ng CLR ang GC at magsisimula ang mga friezes. Dapat itong kapansin-pansing mabagal, maniwala ka sa akin. Ang malaking halaga ng memorya sa mga modernong production server o cloud machine ay hindi pumipigil sa matataas na pagkaantala sa pagproseso. Sa katunayan, mas maraming memorya, mas malamang na MAPANSIN mo kung paano gumagana ang GC. Ang pagninilay ay, sa teorya, isang dagdag na pulang basahan para sa kanya.

Gayunpaman, lahat tayo ay gumagamit ng mga IoC container at date mappers, ang prinsipyo ng pagpapatakbo nito ay nakabatay din sa reflection, ngunit kadalasan ay walang mga tanong tungkol sa kanilang performance. Hindi, hindi dahil ang pagpapakilala ng mga dependency at abstraction mula sa panlabas na limitadong mga modelo ng konteksto ay lubhang kailangan na kailangan nating isakripisyo ang pagganap sa anumang kaso. Ang lahat ay mas simple - talagang hindi ito nakakaapekto sa pagganap.

Ang katotohanan ay ang pinakakaraniwang mga balangkas na nakabatay sa teknolohiya ng pagmuni-muni ay gumagamit ng lahat ng uri ng mga trick upang gumana dito nang mas mahusay. Kadalasan ito ay isang cache. Kadalasan ito ay Mga Expression at mga delegado na pinagsama-sama mula sa puno ng expression. Ang parehong automapper ay nagpapanatili ng isang mapagkumpitensyang diksyunaryo na tumutugma sa mga uri na may mga function na maaaring mag-convert sa isa't isa nang hindi tumatawag sa reflection.

Paano ito nakakamit? Sa esensya, hindi ito naiiba sa lohika na ginagamit mismo ng platform upang makabuo ng JIT code. Kapag ang isang pamamaraan ay tinawag sa unang pagkakataon, ito ay pinagsama-sama (at, oo, ang prosesong ito ay hindi mabilis); sa kasunod na mga tawag, ang kontrol ay inililipat sa naka-compile na paraan, at walang mga makabuluhang drawdown sa pagganap.

Sa aming kaso, maaari mo ring gamitin ang JIT compilation at pagkatapos ay gamitin ang pinagsama-samang gawi na may parehong pagganap sa mga katapat nitong AOT. Ang mga ekspresyon ay tutulong sa amin sa kasong ito.

Ang prinsipyong pinag-uusapan ay maaaring madaling ibalangkas tulad ng sumusunod:
Dapat mong i-cache ang huling resulta ng pagmuni-muni bilang isang delegado na naglalaman ng pinagsama-samang function. Makatuwiran din na i-cache ang lahat ng kinakailangang bagay na may impormasyon ng uri sa mga field ng iyong uri, ang manggagawa, na naka-imbak sa labas ng mga bagay.

May lohika dito. Sinasabi sa atin ng sentido komun na kung ang isang bagay ay maaaring i-compile at i-cache, dapat itong gawin.

Sa hinaharap, dapat sabihin na ang cache sa pagtatrabaho sa pagmuni-muni ay may mga pakinabang nito, kahit na hindi mo ginagamit ang iminungkahing paraan ng pag-compile ng mga expression. Actually, dito ko lang inuulit ang mga theses ng author ng article na tinutukoy ko sa taas.

Ngayon tungkol sa code. Tingnan natin ang isang halimbawa na batay sa aking kamakailang sakit na kinailangan kong harapin sa isang seryosong produksyon ng isang seryosong institusyon ng kredito. Lahat ng entity ay kathang-isip lang para walang manghuhula.

Mayroong ilang kakanyahan. Magkaroon ng Contact. May mga titik na may isang standardized na katawan, kung saan ang parser at hydrator ay lumikha ng parehong mga contact. Dumating ang isang liham, binasa namin ito, na-parse ito sa mga pares ng key-value, gumawa ng contact, at na-save ito sa database.

elementary palang. Sabihin nating may mga katangian ang isang contact Buong Pangalan, Edad at Telepono ng Contact. Ang data na ito ay ipinadala sa liham. Nais din ng negosyo ang suporta upang mabilis na makapagdagdag ng mga bagong key para sa pagmamapa ng mga katangian ng entity sa mga pares sa katawan ng liham. Kung sakaling may gumawa ng typo sa template o kung bago ang release ay kinakailangan na agarang ilunsad ang pagmamapa mula sa isang bagong partner, na umaangkop sa bagong format. Pagkatapos ay maaari tayong magdagdag ng bagong ugnayan sa pagmamapa bilang murang datafix. Ibig sabihin, halimbawa sa buhay.

Nagpapatupad kami, gumagawa ng mga pagsubok. Gumagana.

Hindi ko ibibigay ang code: maraming source, at available ang mga ito sa GitHub sa pamamagitan ng link sa dulo ng artikulo. Maaari mong i-load ang mga ito, pahirapan sila nang hindi makilala at sukatin ang mga ito, dahil makakaapekto ito sa iyong kaso. Ibibigay ko lamang ang code ng dalawang pamamaraan ng template na makilala ang hydrator, na dapat ay mabilis, mula sa hydrator, na dapat ay mabagal.

Ang lohika ay ang mga sumusunod: ang paraan ng template ay tumatanggap ng mga pares na nabuo ng pangunahing lohika ng parser. Ang layer ng LINQ ay ang parser at ang pangunahing lohika ng hydrator, na gumagawa ng isang kahilingan sa konteksto ng database at naghahambing ng mga susi sa mga pares mula sa parser (para sa mga function na ito ay mayroong code na walang LINQ para sa paghahambing). Susunod, ang mga pares ay ipinapasa sa pangunahing paraan ng hydration at ang mga halaga ng mga pares ay nakatakda sa mga kaukulang katangian ng entity.

"Mabilis" (Prefix Mabilis sa mga benchmark):

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

Tulad ng nakikita natin, ginagamit ang isang static na koleksyon na may mga katangian ng setter - pinagsama-samang mga lambdas na tumatawag sa setter entity. Nilikha ng sumusunod na code:

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

Sa pangkalahatan ito ay malinaw. Binabaybay namin ang mga property, gumawa ng mga delegado para sa kanila na tumatawag sa mga setter, at i-save ang mga ito. Pagkatapos ay tumatawag kami kapag kinakailangan.

"Mabagal" (Prefix Mabagal sa mga benchmark):

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

Dito, agad naming nilalampasan ang mga katangian at direktang tumawag sa SetValue.

Para sa kalinawan at bilang isang sanggunian, nagpatupad ako ng isang walang muwang na pamamaraan na nagsusulat ng mga halaga ng kanilang mga pares ng ugnayan nang direkta sa mga patlang ng entity. Prefix – Manwal.

Ngayon kunin natin ang BenchmarkDotNet at suriin ang pagganap. At biglang... (spoiler - hindi ito ang tamang resulta, nasa ibaba ang mga detalye)

Hindi matagumpay na artikulo tungkol sa pagpapabilis ng pagmuni-muni

Ano ang nakikita natin dito? Ang mga pamamaraan na matagumpay na nagtataglay ng Fast prefix ay nagiging mas mabagal sa halos lahat ng pass kaysa sa mga pamamaraan na may Slow prefix. Ito ay totoo para sa parehong paglalaan at bilis ng trabaho. Sa kabilang banda, ang isang maganda at eleganteng pagpapatupad ng pagmamapa gamit ang mga pamamaraan ng LINQ na inilaan para dito hangga't maaari, sa kabaligtaran, ay lubos na nakakabawas sa produktibidad. Ang pagkakaiba ay sa kaayusan. Ang trend ay hindi nagbabago sa iba't ibang bilang ng mga pass. Ang pagkakaiba lang ay nasa sukat. Sa LINQ ito ay 4 - 200 beses na mas mabagal, mayroong mas maraming basura sa humigit-kumulang sa parehong sukat.

Nai-update

Hindi ako naniniwala sa aking mga mata, ngunit higit sa lahat, ang aming kasamahan ay hindi naniniwala sa aking mga mata o sa aking code - Dmitry Tikhonov 0x1000000. Ang pagkakaroon ng pag-double-check sa aking solusyon, napakatalino niyang natuklasan at itinuro ang isang error na napalampas ko dahil sa ilang pagbabago sa pagpapatupad, simula hanggang pangwakas. Pagkatapos ayusin ang nahanap na bug sa setup ng Moq, lahat ng resulta ay nahulog sa lugar. Ayon sa mga resulta ng retest, ang pangunahing kalakaran ay hindi nagbabago - ang LINQ ay higit na nakakaapekto sa pagganap kaysa sa pagmuni-muni. Gayunpaman, maganda na ang gawain sa pagsasama-sama ng Expression ay hindi ginagawa nang walang kabuluhan, at ang resulta ay makikita pareho sa paglalaan at oras ng pagpapatupad. Ang unang paglunsad, kapag nasimulan ang mga static na field, ay natural na mas mabagal para sa "mabilis" na paraan, ngunit pagkatapos ay nagbabago ang sitwasyon.

Narito ang resulta ng muling pagsusuri:

Hindi matagumpay na artikulo tungkol sa pagpapabilis ng pagmuni-muni

Konklusyon: kapag gumagamit ng pagmuni-muni sa isang negosyo, walang partikular na pangangailangan na gumamit ng mga trick - mas kakainin ng LINQ ang pagiging produktibo. Gayunpaman, sa mga high-load na pamamaraan na nangangailangan ng pag-optimize, maaari mong i-save ang reflection sa anyo ng mga initializer at delegate compiler, na magbibigay ng "mabilis" na lohika. Sa ganitong paraan maaari mong mapanatili ang parehong flexibility ng pagmuni-muni at ang bilis ng application.

Ang benchmark code ay magagamit dito. Sinuman ay maaaring suriin ang aking mga salita:
HabraReflectionTests

PS: ang code sa mga pagsubok ay gumagamit ng IoC, at sa mga benchmark ay gumagamit ito ng isang tahasang konstruksyon. Ang katotohanan ay na sa huling pagpapatupad ay pinutol ko ang lahat ng mga kadahilanan na maaaring makaapekto sa pagganap at gawing maingay ang resulta.

PPS: Salamat sa gumagamit Dmitry Tikhonov @0x1000000 para sa pagtuklas ng aking error sa pag-set up ng Moq, na nakaapekto sa mga unang sukat. Kung ang sinuman sa mga nagbabasa ay may sapat na karma, mangyaring i-like ito. Huminto ang lalaki, binasa ng lalaki, nag-double check ang lalaki at itinuro ang pagkakamali. Sa tingin ko ito ay karapat-dapat sa paggalang at pakikiramay.

PPPS: salamat sa maselang mambabasa na nakarating sa ilalim ng estilo at disenyo. Ako ay para sa pagkakapareho at kaginhawahan. Ang diplomasya ng pagtatanghal ay nag-iiwan ng maraming nais, ngunit isinasaalang-alang ko ang pagpuna. Humihingi ako ng projectile.

Pinagmulan: www.habr.com

Magdagdag ng komento