Timeseries Data Processing – Überlegungen zu Datentypen – Teil 1 – Array

Vor gar nicht allzu langer Zeit habe ich mit dem Vergleich einiger C# Lösungen zur Echtzeitanzeige von Daten innerhalb eines festen Zeitfensters befasst. Dabei wurde davon ausgegangen, dass dem Zeitfenster neue Daten mit einer konstanten Frequenz hinzugefügt wurden und auch die Gesamtanzahl der darzustellenden Daten gleich bleibt.

Um die Grenzen der zu testenden Systeme beurteilen zu können, ging es dabei also letztendlich um zwei wesentliche Fragen.

  1. Wie wirkt sich die Gesamtanzahl der darzustellenden Datensätze pro Sekunde aus.
  2. Welchen Einfluss hat die Änderungsrate. Also die Anzahl der neuen Datensätze pro Sekunde.

Die Daten (in meinem Fall vom Typ double) für die Simulation wollte ich, mit der Hilfe von Zufallszahlen und einer for-Schleife, selbst erzeugen und dann zur Anzeige an die entsprechende Lösung übergeben. Dabei stellte sich natürlich die Frage ob, und wie, sich der zur Speicherung gewählte Datentypen auswirkt.

Der Datentyp musste dabei folgendes ermöglichen.

  1. Löschen des ältesten Datensatzes.
  2. Hinzufügen eines neuen Datensatzes.

Spontan kam mir erst einmal ein normales Array in den Sinn. Arrays sind ja bekannt dafür, dass sie relativ schnell sind, da beim Initialisieren bereits der benötigte Speicher im Heap reserviert wird. Sobald das Array komplett mit Daten befüllt wurde, kommt Bewegung in das Spiel. Der älteste Eintrag muss nun gelöscht werden, um Platzt für den neuesten zu machen.

Beim Array hat der erste Datensatz den Index 0. Dies ist dann automatisch auch der älteste Datensatz. Der letzte, also aktuellste Datensatz, befindet sich am Ende. Das Array hat aber eine feste Größe. Daher ist es notwendig, alle Datensätze um eins nach oben zu verschieben. Damit fliegt das oberste Element (rot) raus und unten ist wieder Platz für das neue Element (grün).

Zum Testen der Performance habe ich eine einfache Konsolenanwendung in C# erstellt. Mich interessiert, wie viele neu Datensätze ich dem Array mit dieser Methode in einem ungefähren Zeitfenster von 50 Millisekunden hinzufügen kann. Dazu benutze ich System.Threading.Timer. Ungefähr 50 ms deshalb, weil die normalen Timer für diesen Zeitbereich leider nicht präzise genug sind (es kann schon mal zu Abweichungen von 15 ms und mehr kommen). Aber das ist ein anderes Thema und zur Demonstration der Performanceunterschiede ist die Genauigkeit ausreichend.

Begonnen habe ich mit folgenden, sehr konservativen Vorgaben:

  1. Gesamtanzahl der Datensätze: 10000
  2. Neue Datensätze pro Zeitfenster (50ms): 1000 (entspricht 20kHz)

Die folgende Animation simuliert das Zeitfenster von 50ms.
Neu Daten sind grün, und die gelöschten Daten rot dargestellt.
Die Gesamtanzahl ergibt sich aus dem blauen und grünen Bereich.

Jede ausgegebene Zeile des Tests steht für einen Timeraufruf bzw. Zyklus von ca. 50 ms und liefert folgende Informationen.

  1. Die Uhrzeit
  2. Die Thread-Id
  3. Die Gesamtanzahl der Elemente.
  4. Die benötigte Zeit für das hinzufügen der neuen Elemente pro Zeitfenster

Zusammen mit dem dazugehörigen Screenshot der Diagnostic-Tools lässt sich daraus folgendes erkennen.

  1. Wie erwartet bleibt der verwendete Speicher nach der Initialisierung konstant.
  2. Solange nur neue Daten hinzugefügt werden, ist die Performance sehr gut.
  3. Sobald die Daten im Array verschoben werden müssen (nach ca. 2 Sekunden), kann von Performance keine Rede mehr sein.
  4. Da, auch beim Verschieben, immer die gleichen Speicheradressen für die Daten im Array verwendet werden, hat der Garbage Collector nichts zu tun. Das ist gut.
  5. Das 50 ms Zeitfenster ist für das Verschieben der Daten im Array zu klein.

Mir war natürlich klar, das ich mit einem normalen Array hier nicht besonders weit kommen würde und deshalb habe ich, obwohl es eventuell noch Verbesserungsmöglichkeiten z.B. via Array.Copy gegeben hätte, für diese Methode keine weiteren Tests durchgeführt. Das Ergebnis aber war, in negativer Hinsicht, auch so beeindruckend.

Was ich brauchen würde, war das Konzept einer Warteschlange oder eines Ringpuffers. Zum Glück gibt es solche oder ähnliche Datentypen/Klassen auch in C# und in den nächsten Teilen werde ich zeigen mit welchen Typen ich, nach und nach, die Performance steigern konnte.

Scroll to top