A tömbök kapcsán emlÃtésre került a stackalloc kulcsszó. Ez C# 8 óta használható unsafe kontextuson kÃvül is az alábbi formában:
Span<int> x = stackalloc int[100];
Fontos, hogy x tÃpusa esetén explicit meg kell adnunk, hogy Span<T> tÃpusú a változónk. Erre azért van szükség, mert ha a var kulcszóval deklarálnánk x változót, akkor a tÃpusa egy int* pointer lenne a stackalloc miatt. De mi is az a Span<T> ?
A Span egy összefüggÅ‘ memóriaterületet jelöl adott tÃpusból, ami a stack-en helyezkedik el a heap-el szemben. EbbÅ‘l adódóan nagyon gyors hozzáférést biztosÃt a benne tárolt elemekhez. A ReadOnlySpan<T> a létrejötte után nem engedi a benne tárolt elemek módosÃtását.
Mivel a Span tÃpus a stack-en helyezkedik el, rendelkezik néhány limitációval:
- Nem lehet bedobozolni és nem lehet
objectvagydynamictÃpusú változónak értékül adni. - Nem lehet osztályokban és struktúrákban tagváltozónak vagy tulajdonságnak felhasználni.
Asyncmetódusokban nem használhatóak, deTaskésTask<TResult>tÃpusokban gond nélkül működnek.- Nem használhatóak iterátorok implementációjában
A Span tÃpus leginkább arra jó, hogy gyorsan tudjunk műveleteket végezni tömbökben tárolt adatokkal anélkül, hogy az eredmények tárolására egy új tömböt allokálnánk. Egy tömb allokálása esetén az idÅ‘veszteséget az okozza, hogy a GC dönthet úgy, hogy töredezettség mentesÃti a memóriát, mert nincs hely az eredmények tárolására. Ezt a problémát szünteti meg a Span azzal az egyszerűséggel, hogy az eredményeket stack-ben tárolja.
A limitációkból kiderül, hogy leginkább metódusokban tudjuk használni átmeneti változónak a tÃpust.
Amitől a Span igazán érdekes lesz, hogy hasonlóan viselkedik a tömbökhöz és rendelkezik egy Slice metódussal. Ezzel lényegében egy memóriaterületet ragadhatunk ki. Az egy paraméteres változatában a kezdő indextől a Span végéig az összes elem fel lesz használva. A két paraméteres változatban a második paraméter azt határozza meg, hogy hány darab elem legyen felhasználva.
Span<T> Slice (int start);
Span<T> Slice (int start, int length);
A Slice használatára egy példa:
using System;
class Program
{
static void Main()
{
var contentLength = "Content-Length: 132";
var length = GetContentLength(contentLength.ToCharArray());
Console.WriteLine($"Content length: {length}");
}
private static int GetContentLength(ReadOnlySpan<char> span)
{
var slice = span.Slice(16);
return int.Parse(slice);
}
}
A fenti példában a contentLength szöveg egy HTTP fejléc információ, ami megmondja a böngészőnek, hogy konkrétan a dokumentum amit le fog tölteni, mekkora byte-ban kifejezve.
Ha egy klienst programozunk, akkor ez egy feldolgozandó információ. Lényegében ezt valósÃtja meg a GetContentLength metódus. AmitÅ‘l azonban ez a megoldás jobb lesz a hagyományos megoldásoktól az az, hogy nem keletkezik "sok" átmeneti változó. A string tÃpus implementációjából adódik, hogy minden egyes műveletvégzés esetén újabb példányt ad vissza.
A fenti példakódban egyszer keletkezik egy másolat az eredeti szövegbÅ‘l a ToCharArray() hÃvással. A többi allokáció a Stack-en történk. A GetContentLength() metódusban a Slice(16) hÃvás a 16. karaktertÅ‘l adja vissza a memória területet, ami éppen csak a számot leÃró karaktereket tartalmazza. Ezzel pedig az int tÃpus Parse metódusa már tud mit kezdeni.
Kérdés már csak az, hogy miért érdemes a stackalloc-ot alkalmazni Span esetén a szimpla tömbökkel szemben? A válasz abban keresendő, hogy a stack-en tárolt adatok a processzor belső cache memóriájában helyezkednek el egy metódus végrehajtása közben, ezért ennek az elérése nagyságrendileg nagyobb, mint a memóriáé, de ennek azért vannak limitációi, mégpedig:
- A stackalloc-al foglalható memóriaméret függ a processzortól és ha túl nagyot foglalunk, akkor
StackOverFlowkivétel keletkezik. Ökölszabály szerint érdemes a pár KiB maximális memóriaméretben gondolkodni. - Mivel könnyen
StackOverFlowkivétel keletkezhet, ezért ha a méret a felhasználói bevitel eredménye, akkor érdemes ellenőrizve foglalni a memóriát:
void SpanWithUserInput(int size)
{
const int maxSize = 1025;
Span<int> span = size <= maxSize ? stackalloc int[size] : new int[size];
}
A kódrészletben látható, hogy a Span nem csak a stackalloc kifejezéssel hozható létre. Létrehozható a new operátor segÃtségével is, de utóbbi esetben a tömb a heap-en fog elhelyezkedni és úgy fog viselkedni, mint egy közönséges tömb. Ez a fajta allokációs megoldás egyfajta kompatibilitásként került bevezetésre.
AsSpan(), ToArray() és listák
A Span<T> és a hagyományos tömbök világa összekapcsolható, mégpedig két fontos metódussal. Az AsSpan() hÃvással egy tömböt tudunk átalakÃtani Span<T> tÃpusba. Ez az átalakÃtás a tömb tartalmát nem allokálja újra, vagyis nem történik memória másolás, csupán a kollekció tÃpusa kerül megváltoztatásra.
A másik fontos metódus a ToArray(), aminek a segÃtségével egy Span<T> tÃpusú változó tartalmát tudjuk tömbre átalakÃtani. Ez az átalakÃtás nem a LINQ ToArray() metódusa, de működésében megegyezik.
Mivel a Span<T> egy alacsonyrendű tÃpus, amit kifejezetten optimalizációra találtak ki, ezért a LINQ műveletek nem működnek rajta és a Span<T> az IEnumerable<T> interfészt sem valósÃtja meg.
Listák esetén is lehetÅ‘ségünk van Span<T> tÃpusra átalakÃtásra, méghozzá a System.Runtime.InteropServices névtérben található CollectionsMarshal statikus osztály AsSpan metódusával. Ez a metódus hasonlóan a tömbökön elérhetÅ‘ AsSpan() metódushoz az elemeket nem allokálja újra, helyette hozzáférést biztosÃt a lista belsejében található tömbhöz. Ezen metódus használata közben érdemes különös körültekintéssel eljárni.
Egészen addig, amÃg a listánkból gyártott Span<T> használatban van, az eredeti listához nem célszerű elemeket hozzáadnunk, vagy eltávolÃtanunk, mert ez ugyan nem jár kivétellel, de a változások nem realizálódnak a Span<T> tÃpusban.
using System.Runtime.InteropServices;
int[] tomb = { 1, 2, 3, 4 };
Span<int> span = tomb.AsSpan(); //span-be konvertálás másolás nélkül
int[] tomb2 = span.ToArray(); //tömbbé visszaalakÃtás másolással
var list = new List<int>() { 1, 2, 3, 4 };
Span<int> listspan = CollectionsMarshal.AsSpan(list); //lista átalakÃtása