A C# 8.0 egyik újdonsága a stackalloc kulcsszó unsafe kontextuson kívüli engedélyezése volt. Ezzel egy Span<T> típust tudunk létrehozni, ami referencia struktúraként van implementálva. Ennek ugye előnye az, hogy nincs heap allokációnk, ebből adódóan a stack-el együtt be tud kerülni a processzor belső cache memóriájába, ahonnan nagyságrendekkel gyorsabban tudjuk olvasni az adatot, mint ha a heap-ről tennénk ezt meg. Azonban ennek megvan az ára, mégpedig a referencia struktúrákra vonatozó limitációk.
Ezek közül a legfájóbb, hogy nem alkalmazhatóak Task-on belül. De mi van akkor, ha mégis a stack-en szeretnénk allokálni egy fix méretű tömböt, de nem szeretnénk unsafe kontextusba váltani?
A C# 12 inline array funkciója erre ad lehetőséget:
using System.Runtime.CompilerServices;
namespace InlineArrayPelda;
[InlineArray(10)]
internal struct ArrayInline<T>
{
private T _element;
}
internal class Program
{
private static async Task Main(string[] args)
{
var inline = new ArrayInline<int>();
for (int i=0; i<10; i++)
{
inline[i] = Random.Shared.Next(0, 100);
}
for (int i=0; i<10; i++)
{
Print(inline, i);
}
Console.ReadKey();
}
private static async Task Print(ArrayInline<int> inline, int i)
{
await Task.Delay(inline[i] * 10);
Console.Write($" {inline[i]}");
}
}
A program kimenete valami hasonló lesz:
2 15 21 38 41 49 51 59 72 81
Az inline tömb szintaxisa minimum furcsa. Egy struktúrát kell definiálnunk, aminek egy darab adattagot kell tartalmaznia olyan típusban, amit majd a tömb tartalmazni fog. Az adattag neve és elérési módosítója mindegy. Ez a struktúra attól lesz inline array, hogy az InlineArray attribútummal annotáltuk, aminek a konstruktorában meg kell adni az elemszámot.
Ezt követően majdnem ugyanúgy használhatjuk a tömböt, mint egy hagyományos tömböt. Itt a hangsúly a majdnem szón van, mivel ez a tömb működésében egy C nyelvi tömbre hasonlít. Nincs Length tulajdonsága, nincs collection initializer és nincs IEnumerable<T> implementációnk.
Cserébe viszont, mint a példa programban is látható, egy Task típussal együttműködő, stack-en allokált tömböt kapunk, de egész biztos ez?
A struct alapú típusok alapvetően a stack-en allokálódnak, de ha nincs elég hely, akkor a futtatókörnyezet dönthet úgy, hogy a heap-en allokál. Éppen ezért nem biztos, hogy minden Task típust alkalmazó szituációban gyorsabb kódot fog eredményezni. De akkor mégis mi értelme ennek a funkciónak?
Ezt a .NET belsőleg natív kód interakcióra alkalmazza olyan esetekben, ahol tömb-öt kell átadni a natív kódnak. Ilyen esetekben egy inline array használata gyorsabb interakciót eredményez, mint a hagyományos tömbök. Ennek oka az, hogy a hagyományos tömbök elhelyezkedése .NET esetén a memóriában változhat, akár olvasás közben is.
Ez furcsán hangzik, de ugye a GC külön szálon fut, ami ha kell felszabadít és áthelyez elemeket. Ezt pedig natív kód híváskor figyelembe kell vennie a keretrendszernek extra ellenőrzésekkel. Ezek az ellenőrzések egy sokat hívott natív metódus esetén igencsak össze tudnak adódni. Inline tömbök esetén azonban ezek kihagyhatóak, mivel a tömb folyamatosan kerül allokálásra a memóriában, lehetőség szerint a stack-en.