Aalmada “Los Baños de Doña María de Padilla”

Kuidas kasutada Span ja Memory

(Uuendatud .NET Core 2.1 ametliku väljalaske versioonini)

Sissejuhatus

Span ja Memory on .NET Core 2.1 uued funktsioonid, mis võimaldavad külgneva mälu tugevat tüüpi haldamist sõltumata sellest, kuidas see oli eraldatud. Need võimaldavad koodi hõlpsamini hooldada ja parandavad oluliselt rakenduste jõudlust, vähendades vajalike mälu eraldiste ja koopiate arvu.

Põhjustel, mida teised seletavad palju paremini, võib Span eksisteerida ainult virnas (erinevalt hunnikus olemasolevast). See tähendab, et see ei saa olla väli klassis ega mis tahes nn kastiga struktuuris (konverteeritavaks viitetüübiks). Span kasutab ära viite C # 7.2 uuele funktsioonile ref struct, muutes kompilaatori seda reeglit rakendama.

Järgmisena loon paar kasutusstsenaariumi, et saaksite paremini aru, millal ja kuidas neid uusi funktsioone kasutada.

Kohalikud muutujad

Kujutame ette, et meil on teenus, mis tagastab Foo tüüpi objektide kollektsiooni. Kogumik pärineb mõnest kaugest asukohast, seega tuleb see läbi baitide voo. See tähendab, et peame saama osa baitidest kohalikuks puhvriks, teisendama need oma objektideks ja seda protseduuri kordama kuni kogumise lõpuni.

Järgnevas näites korratakse kogu kollektsiooni ja arvutatakse iga üksuse väljal Täisarv väärtuste summa.

Teadke real 5, et puhver on eraldatud massiivina , kuid salvestatud kui lokaalse muutuja tüüp Span . Raamistik hõlmab kaudseid operaatoreid, mis seda muundamist võimaldavad.

Kohalikud muutujad tavalistes meetodites (milles ei kasutata asünkit, saagikust ega lambdaid) jaotatakse virnas. See tähendab, et Span kasutamisel pole probleeme. Muutuja püsib seni, kuni selle oma ulatus, antud juhul funktsioon ise.

Kui kontrollite rakenduse Stream.Read () meetodi allkirja, märkate, et see aktsepteerib argumenti tüübiga Span . Tavaliselt tähendab see, et meil oleks vaja mälu kopeerida. Mitte Span ga. Kui T on väärtustüüp, saate seda kasutada meetodil MemoryMarshal.Cast (), mis varjab puhvrit teise tüübiga ilma koopiat nõudmata. Lükake Span kanalile Stream.Read () (read 8 ja 15), kuid lugege selle sisu Foo kollektsioonina, kasutades Span (rida 13). Span -ile pääsete juurde ruutkriipsuga operaatori abil või hargnemise ahela abil.

Kuna loendamise lõpus võib loetud üksuste arv olla väiksem kui puhver, siis iterame algse puhvri tükil. Slice () on meetod, mis tagastab sama puhvri, kuid erinevate piiride korral teise Span .

Pange tähele, et peale voo Stream.Read () pole mälukoopiaid. Lihtsalt sama puhvri maskeerimine. Selle tulemuseks on märkimisväärsed jõudluse parandamine võrreldes mäluhalduritega, mis meil varem olid. Kõik see koos tüübi ohutusega ja hõlpsalt hooldatava koodiga.

stackalloc

Eelmise näite märkus, et Span peab asuma virnas, kuid mitte selle sisu. Massiiv jaotatakse hunnikusse.

Kuna sellisel juhul ei pea puhver funktsiooni ületama ja see on suhteliselt väike, saame puhvri virnas jaotada märksõnaga stackalloc.

Lisaks puhvri jaotusele jääb kogu kood muutumatuks. See on veel üks Span kasutamise eelis. See võtab kokku külgneva mälu jaotuse.

ref struktuuri

Need eelmised näited toimivad hästi, kuid mis siis, kui soovite kogu abil teha mitu toimingut? Peaksite seda koodi paljudes teistes kohtades kopeerima, luues hooldusliku õudusunenäo. Mis saaks, kui saaksime kasutada ennustaja silmust? Peame lihtsalt looma struktuuri, mis eraldatakse virnas ja mis rakendab IEnumeratori .

Kahjuks on iga liidest rakendav väärtustüüp "kasti võimeline", mis tähendab, et selle saab teisendada viitetüübiks.

Õnneks ei vaja ennustamine liideste rakendamist. See nõuab ainult meetodi GetEnumerator () rakendamist, mis tagastab objekti eksemplari, mis rakendab kirjutuskaitstud atribuuti Current ja meetodi MoveNext (). Nii on tegelikult rakendatud Span loendamine. Saame sama teha ka meie kollektsiooni puhul.

Pange tähele ridadel 17 ja 18, et vahekaugused ei ole lokaalsed muutujad, vaid loenduri struktuuri väljad. Veendumaks, et see objekt luuakse ainult virnas, pange real 12 tähele, et see kuulutatakse refuuktuuriks. Ilma selleta näitaks kompilaator viga.

Vahemike loomine toimub nüüd loenduri konstruktoris, kuid on väga sarnane esimese näitega (minu teada ei ole stackallocit sel juhul võimalik kasutada). Loend on nüüd jagatud jaotisteks Current ja MoveNext (). foreach kutsub meetodit MoveNext (), et liikuda kollektsioonide järgmise üksuseni, ja kutsub selle saamiseks selle saamiseks üles Hetke.

Pange tähele, et praegune tagastab kirjutuskaitstud viite tüüpi Foo. See tähendab, et ta pääseb üksusele juurde ilma seda kopeerimata. See on ka funktsioon C # 7.2, mida saab kasutada rakenduste jõudluse märkimisväärseks parandamiseks.

Väärtuse tüübid

Eelmine kood lubab foreachi kasutamist, kuid kui soovite ka LINQi kasutamist lubada, pole liideste rakendamisest pääsu.

Teadke real 19, et puhver on endiselt väljana salvestatud, kuid nüüd tüüpi Memory .

Mälu on Span tehas, mis võib asuda hunnikus. Sellel on omadus Span, mis loob uue Span eksemplari, mis kehtib nimetatud ulatuses. Seda kasutatakse liinidel 33 ja 45.

Loendur ei saa olla refuuktuur, kuna see rakendab liidest. Jätan selle konstruktsiooniks, kuna see toimib sel juhul paremini. Liidese meetodile helistamine on jõudluse eest trahv, kuna see on virtuaalne kõne. Struktuurid ei luba pärimist, nii et .NET on võimeline neid kõnesid optimeerima, muutes need pisut kiiremaks.

Atribuut Praegune tagastab nüüd viite asemel Foo, mis tähendab, et mälukoopia on olemas. Võite lisada ülekoormuse, mis tagastab viite, kuid IEnumeratori abil toimuva kõne korral kasutatakse seda selgesõnaliselt.

Etendus

Kuidas need näited toimivad? BenchmarkDotNet muudab kõigi nende stsenaariumide toimivuse võrdlemise väga lihtsaks.

Nende võrdlusaluste koodi leiate aadressilt https://github.com/aalmada/SpanSample/blob/master/SpanSample/EnumerationBenchmarks.cs

Võrdlusaluste jaoks laiendasin esimese näite puhver Span <3 iteratsiooni kolmeks variandiks: kasutades jutumärki, kasutades GetEnumerator () ja kasutades indeksihalduriga silmuse jaoks. Huvitav näha, et jutlustamisel on sama jõudlus, kuid GetEnumeratori () kasutamine on kaks korda aeglasem.

Kõige tõhusam on kasutada for-silmuse kasutamist puhvriga, mis on eraldatud stackalloci abil. See võtab kõige vähem aega (1,6 ms) ega eralda hunnikule mälu. See on seatud võrdluse võrdlusaluseks, mida on lihtsam võrrelda.

Ref-struct-loenduri kasutamine on aeglasem 4,2 ms (2,67 korda aeglasem kui toores stackalloc-loendus), kuid korduvkasutatava ja hõlpsamini hooldatava koodiga. See on karistus loendusloogika kaheks funktsiooniks jagamise eest.

IEnumeratori kasutamine muudab selle 24,0 ms-ga oluliselt aeglasemaks (15,11 korda aeglasem kui toore stackalloki loendus). Sellel juhul on sama karistus kui eelneval, pluss liideste kasutamine, väärtuse viiteta tagastamine ja kui kogu loendis ei ole ühte span <>.

Ehkki siin seda pole näidatud, on mõni neist lahendustest palju kiirem kui ilma Span kasutamata. Need väärtused näitavad, et peaksite oma rakendustes kaaluma mitmeid loendamise stsenaariume, sõltuvalt sellest, kas eelistate paindlikkust või jõudlust. Kui olete API arendaja, peaksite need kõik avalikustama, et kasutaja saaks ise oma valiku teha.

Järeldus

Span ja Memory on uued funktsioonid, mis võivad .NET-rakenduste mälukoopiate arvu märkimisväärselt vähendada, võimaldades jõudluse täiustamist, ilma et ohverdaksite tüübi turvalisust ja koodi loetavust.

Selle artikli lähtekoodi saate alla laadida ja etalonid omaenda süsteemis käitada.

Rohkem infot

Plaanin kirjutada sellel teemal veel paar artiklit, kuid nende linkide kohta leiate palju lisateavet:

  • Tere tulemast Mads Torgerseni võistlusele nr 7.2 ja Span
  • C # - Kõik Spani kohta: Stephen Toubi uue .NET-põhialuse uurimine
  • Näitaja Adam Sitnik
  • C # 7.2: Jared Parsonsi arusaam spanist
  • Krzysztof Cwalina et al
  • Lisage Stephen Toub jt poolt corefxile esialgsed Span / Bufferil põhinevad API-d