SpanTool - Ein kleines Library für Span<T> in C# / Vb.Net

    • .NET 5–6
    • C#

      SpanTool - Ein kleines Library für Span<T> in C# / Vb.Net

      Hallo Community

      Hab ein kleines Library gemacht mit dem Namen SpanTool.

      Bevor ich jedoch Anfange möchte ich ein paar Worte über Span<T> bzw. ReadOnlySpan<T> sagen, auch wenn es mehr die C#-Programmierer betrifft. Für Vb.Net-Programmierer kann dieses Feature eine gewinnbringende Herausforderung sein.

      Folgend werde ich vermehrt nur noch von Span<T> sprechen, da beide Werkzeuge quasi gleich sind, einfach mit dem Unterschied, dass ReadOnySpan<T> wie es der Name schon sagt, nur für das Lesen aus dem Speicher genutzt wird.


      Span<T> wie auch ReadOnlySpan<T>

      Span<T> wie auch ReadOnlySpan<T> sind „ref Strukturtypen“ (ref struct) und gibt es seit C# Version 7.2. Sie gewährleisten nebst Type- auch Speichersicherheit, und können zusammenhängende Speicherbereich problemlos darstellen.

      Es ist allgemein bekannt, dass Referenzen meistens auf dem Stack zugewiesen werden. Eine Instanzierung einer Array zeigt dies sehr deutlich. Der linke Teil, die Referenz (Wertetyp) selber wird auf dem Stack zugewiesen. Der rechte Teil (Speicherbereich und somit der Kontext der Referenz) hingegen befindet sich auf dem Heap.

      VB.NET-Quellcode

      1. Dim bytes = New Byte(9) {}

      C#-Quellcode

      1. var bytes = new byte[10];

      Mit Span<T> und ReadOnlySpan<T> wird sichergestellt, dass die komplette Struktur (ref struct) dem Stack zugeordnet wird, auch wenn der Kontext (der eigentliche Speicherbereich) davon nachher auf dem Heap zeigt.

      Somit erkennt man sehr deutlich, dass Span<T> bzw. ReadOnlySpan<T> eigentlich eine Art von „Referenz“ oder „Zeiger“ ist, die nicht als Single-Wertetyp sondern als Werte-StrukturType besteht. Ref Strukturtypen besitzen die Fähigkeit wieder ref Strukturtypen zurückzugeben, und genau das, macht das Ganze so wertvoll.

      Anderseits, da sichergestellt wird, das ref struct auf dem Stack zugewiesen werden, gibt es eine Reihe von Einschränkungen die unbedingt beachtet werden müssen. So können sie z.B. nicht als Felder in Klassen deklariert werden. Weitere wichtige Infos dazu in den folgenden Links.
      learn.microsoft.com/en-us/dotn…ence/builtin-types/struct
      learn.microsoft.com/en-us/dotn…/builtin-types/ref-struct


      Span<T> und ReadOnlySpan<T> sind Power-Werkzeuge, denn sie kommen mit den gängigsten Speicher klar, egal um welche Art von Speicher es sich nun handelt.

      Sie verwalten Speicher aus:
      • - Stack (stackalloc T[size])
      • - Heap (new T[size])
      • - Unmanaged (AllocHGlobal(size))
      Beide Werkzeuge sind gute Alternativen zu Arrays, List<T> etc. Array können sogar implizit zu einem Span<T> bzw. ReadOnlySpan<T> gecastet werden.

      VB.NET-Quellcode

      1. Dim span = = New Byte(9) {}.AsSpan()
      2. Dim span As Span(Of Byte) = New Byte(9) {}
      3. Dim span = array.AsSpan()
      4. Dim span As Span(Of Byte) = array
      5. Dim span = str.AsSpan()
      6. Dim span As ReadOnlySpan(Of Char) = str

      C#-Quellcode

      1. var span = new byte[10].AsSpan();
      2. Span<byte> span = new byte[10];
      3. var span = array.AsSpan();
      4. Span<byte> span = array;
      5. var span = str.AsSpan();
      6. ReadOnlySpan<char> span = str

      Beim Cast zu einer Span<T> wird nie neuer Speicher für den eigentliche Speicherbereich alloziert (alloc), sondern es wird ein neues Span<T> (ref struct) mit einer neuen Referenz für den Speicherbereich auf dem Stack zugeordnet.

      Auch beim Erstellen eines neuen Span<T> (new Span<T>(…)) wird kein neuer Speicher alloziert (alloc), sondern es wird/muss ein vorhandener Speicherbereich zugewiesen werden.

      Hier wird z.B. Speicher vom Stack alloziert und dem Span<T> zugewiesen.

      C#-Quellcode

      1. byte* bytes = stackalloc byte[size];
      2. var span = new Span<byte>(bytes, size);
      3. //oder einfach
      4. Span<byte> bytes = stackalloc byte[size];

      Hier wird Speicher vom Heap alloziert und zugewiesen.

      VB.NET-Quellcode

      1. Dim bytes = New Byte(9) {}
      2. Dim span = New Span(Of Byte)(bytes)
      3. Dim span = New Span(Of Byte)(bytes, 0, bytes.Length)



      Span<T>.Slice()

      Schaut man sich die Überladungen von Slice (Span<T>-Operation) an, so wird man feststellen (wie oben beschrieben), das wiederum ein Span<T> zurückgegeben wird.

      Slice ist das “Äquivalent“ von Skip und Take, das vor allem in LINQ oft genutzt wird. Der einzige Unterschied ist, dass in Slice nie neuer Speicher alloziert wird. Verändere ich in der neuen Span<T> irgendeinen Wert, so ändert sich dieser Wert auch in der ursprünglichen Span<T>.

      Slice-Operationen können also auch als neue “Fenster“ auf den eigentlichen Speicherbereich angesehen werden, und sind sehr kostengünstig (O(1)).

      VB.NET-Quellcode

      1. Dim bytes = New Byte() {255, 254, 253}
      2. Dim span = New Byte() {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}.AsSpan
      3. bytes.CopyTo(span.Slice(1, bytes.Length))

      C#-Quellcode

      1. var bytes = new byte[] { 255, 254, 253 };
      2. Span<byte> span = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
      3. bytes.CopyTo(span.Slice(1, bytes.Length));

      Das Resultat ergibt:
      span = {0, 255, 254, 253, 4, 5, 6, 7, 8, 9}


      VB.NET-Quellcode

      1. Dim span1 = New Byte() {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}.AsSpan
      2. Dim span2 = span1.Slice(1, 3) ‘ { 1, 2, 3 }
      3. span2(1) = 255

      C#-Quellcode

      1. Span<byte> span1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
      2. Span<byte> span2 = span1.Slice(1, 3); // { 1, 2, 3 }
      3. span2[1] = 255;

      Das Resultat ergibt:
      span1 = {0, 1, 255, 3, 4, 5, 6, 7, 8, 9}

      Somit denke ich, dass ich auf einige wichtige Details von Span<T> bzw. ReadOnlySpan<T> hingewiesen habe. Natürlich gibt es noch vieles mehr darüber zu schreiben. Hierzu kann ich nur auf die oben erwähnten Links hinweisen.


      SpanTool

      SpanTool ist eine Library geschrieben in C#.

      SpanTool kann aus Vb.Net für Span<T> wie auch für Array<T> genutzt werden. Durch die implizite Konvertierung wird das Array bei Übergabe an Methoden automatisch in ein Span<T> umgewandelt. Durch die Eigenschaften von Span<T> sind die Werkzeuge bzw. Methoden in SpanTool “höllisch“ schnell.

      SpanTool ist für Vb.Net wie auch für C# geeignet. Für C# gibt es noch zusätzlich die Möglichkeiten über Unmanaged, die ich in SpanTool bei den entsprechenden Methoden auch so benannt habe.

      SpanTool ist in keiner Weise irgendwie vollständig. Vielmehr soll es eine Basis sein. Wer es weiter entwickeln möchte, der darf das ohne irgendwelche bedenken auch machen.

      SpanTool ist lizenzfrei und darf überall frei verwendet, verändert, kopiert, angepasst etc. wo es für die Anwendung gebraucht werden kann.

      Es sind einige Beispiele vorhanden die zeigen wie es angewendet werden kann. Auch hier findet man keine Vollständigkeit. Am besten man schaut bzw. probiert sich immer noch zusätzlich die Überladungen an, die viele weitere Möglichkeiten bieten und quasi selbsterklärend sind.

      Hier ein paar Beispiele aus SpanTool.


      Converting / Cast

      Die Zeiten mit dem BitConverter sind nun definitiv vorbei. SpanTool konvertiert ein beliebig primitiver Datentyp in ein anders beliebig primitiver Datentyp.

      C#-Quellcode

      1. public unsafe static TOUT Convert<TIN, TOUT>(TIN number)
      2. where TOUT : struct where TIN : struct, IComparable
      3. {
      4. if (!typeof(TIN).IsPrimitive) throw new TypeAccessException(nameof(TIN));
      5. if (!typeof(TOUT).IsPrimitive) throw new TypeAccessException(nameof(TOUT));
      6. var sztsrc = SizeOfT<TIN>();
      7. var sztdest = SizeOfT<TOUT>();
      8. var minlength = Math.Min(sztsrc, sztdest);
      9. Span<byte> dstbytes = stackalloc byte[sztdest]; dstbytes.Clear();
      10. var srcbytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref number, 1));
      11. for (var i = 0; i < minlength; i++) dstbytes[i] = srcbytes[i];
      12. if (number.CompareTo(default(TIN)) < 0 && srcbytes.Length < dstbytes.Length)
      13. for (var i = minlength; i < dstbytes.Length; i++) dstbytes[i] = byte.MaxValue;
      14. return MemoryMarshal.AsRef<TOUT>(dstbytes);
      15. }

      Auch Typen-Konvertierungen für Arrays sind als Methoden vorhanden.

      C#-Quellcode

      1. public unsafe static Span<TOUT> Convert<TIN, TOUT>(ReadOnlySpan<TIN> src)
      2. where TIN : struct, IConvertible, IComparable
      3. where TOUT : struct, IConvertible
      4. {
      5. //New Span with memory from Heap
      6. if (!typeof(TIN).IsPrimitive) throw new TypeAccessException(nameof(TIN));
      7. if (!typeof(TOUT).IsPrimitive) throw new TypeAccessException(nameof(TOUT));
      8. return Array.ConvertAll(src.ToArray(), new Converter<TIN, TOUT>(Convert<TIN, TOUT>));
      9. }

      Möchte man die Values einer Array (mit primitiven Datentyp) zusammenhängend konvertieren, so gibt es auch für das eine Methode. Der Vorteil hier ist, dass der gleiche Speicherbereich(!) zurückgegeben wird, einfach als anderer Datentyp. (Nachteil siehe gleich unten)

      C#-Quellcode

      1. public static Span<TOUT> Cast<TIN, TOUT>(Span<TIN> span, int start, int length, bool cut_admit = false)
      2. where TIN : struct where TOUT : struct
      3. {
      4. //return the same memory
      5. if (span.Length - start - length < 0)
      6. throw new ArgumentOutOfRangeException(nameof(length));
      7. var sztin = SizeOfT<TIN>();
      8. var sztout = SizeOfT<TOUT>();
      9. if (!cut_admit)
      10. if ((length * sztin % sztout) != 0)
      11. throw new ArgumentOutOfRangeException(nameof(length));
      12. return MemoryMarshal.Cast<TIN, TOUT>(span.Slice(start, length));
      13. }

      Ein kleiner Nachteil hat jedoch die obere Methode, da der gleiche Speicherbereich verwendet wird, kann unter Umständen die Konvertierung einer zusammenhängenden Zahlenmenge nicht komplett abgeschlossen werden.

      Aber auch dazu habe ich eine Methode erstellt.

      C#-Quellcode

      1. public static void Copy<TSrc, TDest>(
      2. ReadOnlySpan<TSrc> src, in int startsrc, Span<TDest> dst, in int startdst, in int countsrc)
      3. where TSrc : struct where TDest : struct
      4. {
      5. var sztdest = SizeOfT<TDest>();
      6. var bytes = MemoryMarshal.AsBytes(src.Slice(startsrc, countsrc));
      7. var startdstbytes = startdst * sztdest;
      8. var bytesdst = MemoryMarshal.AsBytes(dst);
      9. SpanCopyBytes(bytes, 0, bytesdst, startdstbytes, bytes.Length);
      10. }

      Mit dieser Methode wird eine gewünschte bzw. die komplette Zahlenreihe für die Konvertierung herangezogen. Da es sich hier um ein Kopiervorgang handelt (nicht der gleiche Speicherbereich), muss bei dieser Methode die Ziel-Span (Span<TDest>) mitgegeben werden.

      Es gibt aber noch andere Werkzeuge, siehe dazu in der Library SpanTool.


      ToString

      ToString() gibt auf eine einfache Art und Weise wiederum eine neue Zeichenfolge zurück.

      C#-Quellcode

      1. public static string ToString<T>(Span<T> span, int start, int length, string separator = "") where T : struct
      2. {
      3. if (span.Length == 0) return string.Empty;
      4. if (span.Length - start - length < 0) return string.Empty;
      5. var result = new StringBuilder(length);
      6. var spanresult = span.Slice(start, length);
      7. for (var i = 0; i < length; i++)
      8. result.Append(separator + spanresult[i].ToString());
      9. return result.ToString();
      10. }



      IndexOf

      Eine weitere sehr nützliche Funktion ist IndexOf

      C#-Quellcode

      1. public static int IndexOf<T>(Span<T> span, T value) where T : IEquatable<T>
      2. {
      3. return MemoryExtensions.IndexOf(span, value);
      4. }

      Oder wenn alle Indexes gesucht werden sollen

      C#-Quellcode

      1. public static int[] IndexOfAll<T>(Span<T> span, T value) where T : IEquatable<T>
      2. {
      3. var index = -1;
      4. var result = new List<int>();
      5. while (true)
      6. {
      7. index = IndexOf(span, value, index + 1);
      8. if (index >= 0) result.Add(index);
      9. if (index < 0) break;
      10. if (index >= span.Length - 1) break;
      11. }
      12. return result.ToArray();
      13. }

      Natürlich gibt es auch hier weitere Methode, wie z.B. die Suche von Bereichen. Man muss sich nur die Überladungen anschauen. Siehe auch dazu den SpanTool - Code.


      ForEach

      SpanTool biete auch die Möglichkeit mit ForEach (mit oder Ohne Index) zu arbeiten.

      C#-Quellcode

      1. public unsafe static void ForEach<T>(Span<T> span, Action<T> action)
      2. {
      3. foreach (var spanitem in span)
      4. action(spanitem);
      5. }

      Weitere Möglichkeiten siehe SpanTool - Code.


      Contain

      Eine sehr nützliche Methode, wenn es um das sicherstellen geht, ob ein gewünschter Wert vorhanden ist.

      C#-Quellcode

      1. public static bool Contains<T>(Span<T> span, Span<T> value) where T : IEquatable<T>
      2. {
      3. var idxs = IndexOfAll(span, value[0]);
      4. if (idxs.Length < 1) return false;
      5. if (value.Length == 1 && idxs.Length > 0) return true;
      6. var length = value.Length;
      7. for (var i = 0; i < idxs.Length; i++)
      8. {
      9. if (idxs[i] > span.Length - value.Length) return false;
      10. if (span.Slice(idxs[i], length).SequenceEqual(value))
      11. return true;
      12. }
      13. return false;
      14. }

      Weitere Möglichkeiten siehe SpanTool - Code.


      Reverse

      Auch die Reverse kann immer wieder mal gebraucht werden. Vorteil es wird der gleiche Speicherbereich genutzt.

      C#-Quellcode

      1. public static void Reverse<T>(Span<T> span)
      2. {
      3. if (span.Length < 2) return;
      4. MemoryExtensions.Reverse(span);
      5. }

      Weitere Möglichkeiten siehe SpanTool - Code.


      Strings

      Auch einige String-Werkzeuge sind vorhanden, wie z.B.:

      C#-Quellcode

      1. public static string Insert(ReadOnlySpan <char>str, string values, int start)
      2. {
      3. if (str.Length - start - values.Length < 0)
      4. throw new ArgumentOutOfRangeException(nameof(values));
      5. var result = str.ToArray();
      6. Insert(result, values, start);
      7. return ToString<char>(result);
      8. }

      Auch hier gibt es weitere Methoden, die für den einen oder anderen interessant sein könnte.


      Und und und …


      Freundliche Grüsse

      exc-jdbi
      Dateien
      • SpanTool.zip

        (37,82 kB, 60 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 2 mal editiert, zuletzt von „exc-jdbi“ ()