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
object
vagydynamic
típusú változónak értékül adni. - Nem lehet osztályokban és struktúrákban tagváltozónak vagy tulajdonságnak felhasználni.
Async
metó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
StackOverFlow
kivétel keletkezik. Ökölszabály szerint érdemes a pár KiB maximális memóriaméretben gondolkodni. - Mivel könnyen
StackOverFlow
kivé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