Epäonnistunut artikkeli heijastuksen kiihdyttämisestä

Selitän heti artikkelin otsikon. Alkuperäinen suunnitelma oli antaa hyviä, luotettavia neuvoja reflektoinnin käytön nopeuttamiseen yksinkertaisella mutta realistisella esimerkillä, mutta benchmarkingissa kävi ilmi, että reflektio ei ole niin hidasta kuin luulin, LINQ on hitaampaa kuin painajaisissani. Mutta lopulta kävi ilmi, että tein myös virheen mitoissa... Yksityiskohdat tästä elämäntarinasta on leikkeen alla ja kommenteissa. Koska esimerkki on varsin arkipäiväinen ja periaatteessa toteutettu kuten yrityksissä yleensä tehdään, siitä tuli minusta mielestäni varsin mielenkiintoinen osoitus elämästä: vaikutus artikkelin pääaiheen nopeuteen oli ei havaittavissa ulkoisen logiikan takia: Moq, Autofac, EF Core ja muut "nauhat".

Aloitin työskentelyn tämän artikkelin vaikutelman alla: Miksi Heijastus on hidas

Kuten näette, kirjoittaja ehdottaa koottujen edustajien käyttöä heijastustyyppisten menetelmien suoran kutsumisen sijaan loistavana tapana nopeuttaa sovellusta huomattavasti. Tietenkin on IL-päästöjä, mutta haluaisin välttää sen, koska tämä on työvoimavaltaisin tapa suorittaa tehtävä, joka on täynnä virheitä.

Ottaen huomioon, että minulla on aina ollut samanlainen käsitys pohdinnan nopeudesta, en erityisesti aikonut kyseenalaistaa kirjoittajan johtopäätöksiä.

Tapaan usein naiivia pohdinnan käyttöä yrityksessä. Tyyppi on otettu. Tietoja kiinteistöstä otetaan. SetValue-menetelmä kutsutaan ja kaikki iloitsevat. Arvo on saapunut kohdekenttään, kaikki ovat tyytyväisiä. Erittäin älykkäät ihmiset - seniorit ja tiimin johtajat - kirjoittavat laajennuksiaan objektiin perustuen tällaiseen naiiviin toteutukseen "universaalit" yhden tyyppiset kartoittajat. Olennainen on yleensä tämä: otamme kaikki kentät, otamme kaikki ominaisuudet, iteroidaan niiden yli: jos tyypin jäsenten nimet täsmäävät, suoritamme SetValue-arvon. Ajoittain saamme kiinni virheistä johtuvia poikkeuksia, joissa emme löytäneet jotakin ominaisuutta yhdestä tyypistä, mutta tässäkin on suorituskykyä parantava ulospääsy. Yritä saada kiinni.

Olen nähnyt ihmisten keksivän jäsentimiä ja kartoittajia uudelleen ilman, että heillä olisi täysin varusteltua tietoa siitä, miten niitä edeltäneet koneet toimivat. Olen nähnyt ihmisten piilottavan naiivit toteutuksensa strategioiden taakse, rajapintojen taakse, injektioiden taakse, ikään kuin tämä antaisi anteeksi myöhemmän bakkanaalian. Käänsin nenäni ylös sellaisista oivalluksista. Itse asiassa en mitannut todellista suorituskykyvuotoa, ja jos mahdollista, vaihdoin toteutuksen "optimaalisempaan", jos sain sen käsiini. Siksi ensimmäiset alla käsitellyt mittaukset hämmentyivät minua vakavasti.

Luulen, että monet teistä, Richteriä tai muita ideologeja lukiessa, ovat törmänneet täysin rehelliseen väitteeseen, että koodissa heijastuminen on ilmiö, jolla on erittäin negatiivinen vaikutus sovelluksen suorituskykyyn.

Heijastuksen kutsuminen pakottaa CLR:n käymään kokoonpanojen läpi löytääkseen tarvitsemansa, hakeakseen metatiedot, jäsentääkseen niitä jne. Lisäksi heijastus sekvenssien kulkiessa johtaa suuren muistimäärän varaamiseen. Käytämme muistia, CLR paljastaa GC:n ja friisit alkavat. Sen pitäisi olla huomattavan hidas, usko minua. Nykyaikaisten tuotantopalvelimien tai pilvilaitteiden valtavat muistimäärät eivät estä suuria käsittelyviiveitä. Itse asiassa, mitä enemmän muistia, sitä todennäköisemmin HUOMAA, kuinka GC toimii. Heijastus on teoriassa hänelle ylimääräinen punainen rätti.

Käytämme kuitenkin kaikki IoC-säilöjä ja päivämääräkartoittimia, joiden toimintaperiaate myös perustuu reflektioon, mutta niiden toimivuudesta ei yleensä ole kyseenalaista. Ei, koska riippuvuuksien käyttöönotto ja abstraktio ulkoisista rajoitetuista kontekstimalleista ovat niin tarpeellisia, että meidän on joka tapauksessa uhrattava suorituskyky. Kaikki on yksinkertaisempaa - se ei todellakaan vaikuta suorituskykyyn paljon.

Tosiasia on, että yleisimmät heijastusteknologiaan perustuvat puitteet käyttävät kaikenlaisia ​​temppuja työskennelläkseen sen kanssa optimaalisesti. Yleensä tämä on välimuisti. Tyypillisesti nämä ovat lausekkeita ja delegaatteja, jotka on koottu lausekepuusta. Sama automapper ylläpitää kilpailukykyistä sanakirjaa, joka yhdistää tyyppejä funktioilla, jotka voivat muuntaa toisiksi ilman reflektiota.

Miten tämä saavutetaan? Pohjimmiltaan tämä ei eroa logiikasta, jota alusta itse käyttää JIT-koodin luomiseen. Kun menetelmää kutsutaan ensimmäistä kertaa, se käännetään (ja kyllä, tämä prosessi ei ole nopea), seuraavissa kutsuissa ohjaus siirtyy jo käännettyyn menetelmään, eikä siinä tapahdu merkittäviä suorituskyvyn heikkenemiä.

Meidän tapauksessamme voit myös käyttää JIT-käännöstä ja sitten käyttää käännettyä käyttäytymistä samalla suorituskyvyllä kuin sen AOT-vastineet. Ilmaisut tulevat avuksemme tässä tapauksessa.

Kyseinen periaate voidaan muotoilla lyhyesti seuraavasti:
Sinun tulisi tallentaa reflektoinnin lopputulos välimuistiin delegaatin, joka sisältää käännetyn funktion. On myös järkevää tallentaa välimuistiin kaikki tarvittavat objektit tyyppitiedoilla tyyppisi kenttiin, jotka on tallennettu objektien ulkopuolelle.

Tässä on logiikkaa. Maalaisjärki kertoo meille, että jos jotain voidaan kääntää ja tallentaa välimuistiin, se tulee tehdä.

Tulevaisuudessa on sanottava, että välimuistilla heijastuksen kanssa työskentelyssä on etunsa, vaikka et käyttäisi ehdotettua lausekkeiden käännösmenetelmää. Itse asiassa toistan tässä vain artikkelin kirjoittajan teesit, johon viittaan edellä.

Nyt koodista. Katsotaanpa esimerkkiä, joka perustuu äskettäiseen kipuun, jonka jouduin kohtaamaan vakavan luottolaitoksen vakavassa tuotannossa. Kaikki entiteetit ovat kuvitteellisia, jotta kukaan ei arvaisi.

Siinä on jokin olemus. Olkoon Yhteydenotto. On kirjaimia standardoidulla rungolla, joista jäsentäjä ja hydraattori luovat samat kontaktit. Kirje saapui, luimme sen, jäsensimme sen avain-arvo-pareiksi, loimme yhteystiedon ja tallensimme sen tietokantaan.

Se on alkeellista. Oletetaan, että yhteystiedolla on ominaisuudet Koko nimi, Ikä ja Yhteyshenkilön puhelinnumero. Nämä tiedot välitetään kirjeessä. Yritys haluaa myös tukea voidakseen lisätä nopeasti uusia avaimia kokonaisuuden ominaisuuksien yhdistämiseksi pareiksi kirjeen runkoon. Jos joku on tehnyt kirjoitusvirheen malliin tai ennen julkaisua on tarpeen käynnistää pikaisesti kartoitus uudelta kumppanilta sopeutuen uuteen muotoon. Sitten voimme lisätä uuden kartoituskorrelaation halvaksi datakorjaukseksi. Eli esimerkki elämästä.

Toteutamme, luomme testejä. Toimii.

En anna koodia: lähteitä on paljon, ja ne ovat saatavilla GitHubissa artikkelin lopussa olevan linkin kautta. Voit ladata niitä, kiduttaa niitä tuntemattomaksi ja mitata ne, kuten se vaikuttaisi sinun tapauksessasi. Annan vain koodin kahdelle mallimenetelmälle, jotka erottavat hydraattorin, jonka piti olla nopea, kosteuttajasta, jonka piti olla hidas.

Logiikka on seuraava: mallimenetelmä vastaanottaa parit, jotka on generoitu perusjäsennyslogiikalla. LINQ-kerros on jäsentäjä ja hydraattorin peruslogiikka, joka tekee pyynnön tietokantakontekstiin ja vertaa avaimia jäsentimen pareihin (näille funktioille on vertailua varten koodi ilman LINQ:ta). Seuraavaksi parit siirretään päähydraatiomenetelmään ja parien arvot asetetaan kokonaisuuden vastaaviin ominaisuuksiin.

"Fast" (etuliite Fast vertailuarvoissa):

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

Kuten näemme, käytetään staattista kokoelmaa setteriominaisuuksilla - käännettyjä lambdaja, jotka kutsuvat setterientiteetin. Luotu seuraavalla koodilla:

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

Yleisesti ottaen asia on selvä. Käymme ominaisuudet läpi, luomme niille delegaatteja, jotka kutsuvat asettajia, ja tallennamme ne. Sitten soitetaan tarvittaessa.

"Hidas" (etuliite Hidas vertailuarvoissa):

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

Täällä ohitamme välittömästi ominaisuudet ja kutsumme SetValuetta suoraan.

Selvyyden vuoksi ja viitteeksi otin käyttöön naiivin menetelmän, joka kirjoittaa niiden korrelaatioparien arvot suoraan entiteettikenttiin. Etuliite – Manuaalinen.

Otetaan nyt BenchmarkDotNet ja tutkitaan suorituskykyä. Ja yhtäkkiä... (spoileri - tämä ei ole oikea tulos, tiedot alla)

Epäonnistunut artikkeli heijastuksen kiihdyttämisestä

Mitä me täällä näemme? Menetelmät, joissa on voittoisa Fast-etuliite, osoittautuvat hitaammiksi melkein kaikissa siirroissa kuin menetelmät, joissa on Slow-etuliite. Tämä koskee sekä työnjakoa että työn nopeutta. Toisaalta tähän tarkoitukseen tarkoitettu LINQ-menetelmien kaunis ja elegantti toteutus, päinvastoin, heikentää huomattavasti tuottavuutta. Ero on järjestyksessä. Trendi ei muutu eri läpimenomäärillä. Ainoa ero on mittakaavassa. LINQ:lla se on 4 - 200 kertaa hitaampi, roskaa on enemmän suunnilleen samassa mittakaavassa.

PÄIVITETTY

En uskonut silmiäni, mutta mikä tärkeintä, kollegamme ei uskonut silmiäni tai koodiani - Dmitri Tikhonov 0x1000000. Tarkastettuaan ratkaisuni kahdesti, hän löysi ja huomautti loistavasti virheen, jonka missasin useiden toteutusmuutosten vuoksi, alusta loppuun. Moq-asetuksissa löydetyn virheen korjaamisen jälkeen kaikki tulokset loksahtivat paikoilleen. Uudelleentestaustulosten mukaan päätrendi ei muutu - LINQ vaikuttaa edelleen suorituskykyyn enemmän kuin heijastukseen. On kuitenkin mukavaa, että Expression-käännöksen kanssa ei tehdä turhaa työtä, vaan tulos näkyy sekä allokaatiossa että suoritusajassa. Ensimmäinen käynnistys, kun staattiset kentät alustetaan, on luonnollisesti hitaampi "fast" menetelmälle, mutta sitten tilanne muuttuu.

Tässä uusintatestin tulos:

Epäonnistunut artikkeli heijastuksen kiihdyttämisestä

Johtopäätös: käytettäessä reflektiota yrityksessä ei ole erityistä tarvetta turvautua temppuihin - LINQ syö tuottavuutta enemmän. Suuren kuormituksen menetelmissä, jotka vaativat optimointia, voit kuitenkin säästää heijastuksia alustinten ja delegaatin kääntäjien muodossa, jotka sitten tarjoavat "nopeaa" logiikkaa. Näin voit säilyttää sekä heijastuksen joustavuuden että sovelluksen nopeuden.

Vertailukoodi löytyy täältä. Kuka tahansa voi tarkistaa sanani:
HabraReflectionTestit

PS: testien koodi käyttää IoC:tä ja benchmarkissa eksplisiittistä rakennetta. Tosiasia on, että lopullisessa toteutuksessa leikkasin pois kaikki tekijät, jotka voivat vaikuttaa suorituskykyyn ja tehdä tuloksesta meluisan.

PPS: Kiitos käyttäjälle Dmitri Tikhonov @0x1000000 siitä, että huomasin virheeni Moq:n asettamisessa, mikä vaikutti ensimmäisiin mittauksiin. Jos jollain lukijoista on tarpeeksi karmaa, tykkää. Mies pysähtyi, mies luki, mies tarkasti ja osoitti virheen. Mielestäni tämä on kunnioituksen ja myötätunnon arvoinen.

PPPS: kiitos huolelliselle lukijalle, joka perehtyi tyyliin ja muotoiluun. Kannatan yhtenäisyyttä ja mukavuutta. Esityksen diplomatia jättää paljon toivomisen varaa, mutta otin kritiikin huomioon. Pyydän ammusta.

Lähde: will.com

Lisää kommentti