Andreas Rozek Lesehinweise letzte Änderungen Gästebuch-Eintrag Mitteilungen an den Autor  English Version  zur Leitseite zum vorherigen Thema zum nächsten Thema  zur ersten Seite zur vorherigen Seite zur nächsten Seite

Reed-Solomon Erasure Correction
Überlegungen zu Parameterwahl und Effizienz

Das vorliegende Dokument ist aus einem nach HTML konvertierten und anschließend (erheblich) bearbeiteten Mathematica-Notebook entstanden und beschreibt die Überlegungen im Hinblick auf eine vollständig in Java implementierte "Forward Error Correction" auf der Basis einer "Reed-Solomon Erasure Correction".

Die nachstehend vorgestellten Überlegungen wurden vom Autor als Mitarbeiter der Abteilung "Projekte, Kommunikationssysteme und BelWü-Entwicklung" am Rechenzentrum der Universität Stuttgart im Rahmen des EU Forschungsprojektes MECCANO angestellt und in diesem Kontext von der EU finanziert.

Bitte beachten Sie auch meine Betrachtungshinweise und die Liste der letzten Änderungen!

Themenübersicht

Die folgende Themenübersicht führt Sie unmittelbar zu der von Ihnen gewünschten Information - klicken Sie einfach auf das Thema Ihrer Wahl:

Die Themen im Einzelnen

Vorwort

Zuverlässige Multicast-Übertragungen lassen sich im Prinzip wie folgt realisieren:

  1. der Empfänger fordert beim Sender die erneute Übertragung (Retransmission) aller verlorenen Pakete an oder

  2. der Empfänger rekonstruiert verlorene Pakete mithilfe von redundanter Information, die vom Sender erzeugt und ebenfalls verschickt wurde.

Das erstgenannte Verfahren ist äußerst robust, kostet aber Zeit und kann (bei einer hohen Anzahl von Empfängern, die evtl. sogar ghz. um "Retransmissions" bitten) zu einer Überlastung des Senders führen (Sender Implosion).

Redundanz kann ihrerseits wieder auf zweierlei Art erzeugt werden:

  1. durch mehrfaches Verschicken derselben Daten (vgl. NTE) oder

  2. durch Verschicken von Prüfinformationen, die zur Rekonstruktion verlorener Pakete genutzt werden können (vgl. RAT). Die Verfahren zur Gewinnung dieser Prüfinformation können dabei von der Art der zu verschickenden Daten unabhängig sein oder aber bestimmte Eigenheiten dieser Daten ausnutzen (wie z.B. im Falle von Audio-Datenströmen).

Die vorliegende kleine Studie untersucht (anhand von konkreten, aus MBone-Messungen gewonnenen Daten) die Auswirkungen von "Reed-Solomon Erasure Correction" (kurz: RSEC) Codes auf die Anzahl der erforderlichen Retransmission-Anforderungen, bestimmt die Parameter von möglichst einfachen, aber dennoch wirkungsvollen RSEC-Kodierungen und berechnet die Eckdaten einer auf "table-lookup" basierenden RSEC-Implementierung.

Die Frage nach dem Sinn

Anhand von Meßergebnissen soll jedoch zunächst einmal festgestellt werden, ob die Anzahl der Retransmission-Anforderungen beim derzeitigen Paketverlust-"Verhalten" durch den Einsatz von RSE-Codes überhaupt gesenkt werden kann.

Eine erste Interpretation der Meßergebnisse

Das Java-Programm "RTP_02" des Autors schreibt abwechselnd die Anzahl aufeinanderfolgender empfangener bzw. verlorener Pakete in die Datei "RTP_02.data":

Zur besseren Handhabung wird LossInfo nun in je eine Liste aller empfangenen und aller verlorenen Paketfolgen zerlegt (Achtung: die ReceptionList enthält ein Element mehr als die LossList):

Daraus läßt sich leicht der prozentuale Anteil der verlorenen Pakete ermitteln:

Ein "Burst" sei eine Kette aufeinanderfolgender verlorener Pakete. Jeder Burst hat eine bestimmte Länge, für die spätere Auswertung wird die max. Burst-Länge in der LossList benötigt:

Interessant ist nun, wie häufig Bursts einer bestimmten Länge auftreten:

     

Kurze Bursts sind offensichtlich sehr viel häufiger als lange. Dies läßt vermuten, daß ein Verfahren, welches kurze Bursts "übersteht", bereits einen großen Teil aller in der Praxis auftretenden Verlustfälle behandeln kann - in allen übrigen Fällen müssen die verlorenen Pakete beim Sender erneut angefordert werden.

Kurze Bursts sind außerdem einfacher zu korrigieren als lange. Es stellt sich deshalb die Frage, wie hoch der Anteil der Bursts bis zu einer bestimmten Länge an der Gesamtzahl aller Bursts ist:

     

Wenn man einen prozentualen Anteil Percentage aller Fälle behandeln will, sollten demnach Bursts bis zu folgender Länge berücksichtigt werden:

Die RSE-Kodierung als "Black Box"

Für die vorliegende Studie ist es vollkommen ausreichend, die Reed-Solomon-Kodierung als "black box" mit folgenden Eigenschaften zu betrachten:

  • ein RSE-Kodierer erzeugt Blöcke von jeweils BlockSize Symbolen der Länge SymbolSize, von denen DataCount Symbole echte Applikationsdaten enthalten und die restlichen ParityCount Symbole für Prüfinformation reserviert sind;

  • ist bekannt, welches Datensymbol verlorengegangen ist, so reicht ein beliebiges Prüfsymbol (aus dem gegebenen Übertragungsblock) aus, um dieses Datensymbol zu rekonstruieren. ParityCount Prüfsymbole genügen also, um genau soviele verlorene Datensymbole zu rekonstruieren;

Desweiteren gelten die folgenden Beziehungen:

  • 2 <= SymbolSize <= 16 [Bit]
    d.h., es können nur Symbole mit einer Größe von 2...16 Bit verarbeitet werden. Größere Pakete müssen vor der Kodierung in Folgen kleinerer Symbole zerlegt und die RSE- Kodierung unabhängig vom Rest des Paketes für jedes Symbol aus dieser Folge separat durchgeführt werden. Achtung: damit verlorene Pakete tatsächlich wiederhergestellt werden können, müssen die einzelnen Symbole auch wirklich aus unterschiedlichen Paketen stammen. Es macht keinen Sinn, Blöcke mit Symbolen aus demselben Paket zu bilden!

  • BlockSize = 2^SymbolSize-1
    d.h., die Wahl einer SymbolSize legt zugleich auch die zugehörige BlockSize fest. Durch die Verwendung eines "verkürzten RSE-Kodes" kann man jedoch auch Blöcke mit weniger Symbolen bilden: in diesem Fall werden die "fehlenden" Symbole für Kodierung und Dekodierung als Datensymbole z.B. mit dem Wert 0 angenommen, aber nicht mit übertragen.

  • ParityCount <= DataCount
    aus mathematischer Sicht ist diese Annahme zwar nicht erforderlich, doch sollen an dieser Stelle nicht mehr Prüfsymbole erzeugt werden, als Datensymbole zu rekonstruieren sind.

Eine erneute Betrachtung der Meßwerte

Im Hinblick auf eine spätere RSE-Kodierung ist es wichtig zu wissen, wieviele Pakete innerhalb eines bestimmten Blockes verloren gegangen sind. Aus diesem Grund wird die in LossInfo beschriebene Paketfolge (gedanklich) in Sequenzen von jeweils BlockSize Paketen zerlegt und die Anzahl der in jedem Block verloren gegangenen Pakete berechnet. Um das Ergebnis etwas allgemeiner zu halten, wird die Berechnung auch für den Fall durchgeführt, daß der erste Block erst mit dem zweiten, dritten, ..., oder (BlockSize-1)-ten gemessenen Paket beginnt. Alle Resultate werden aufsummiert und anschließend wieder durch BlockSize geteilt. Auf diese Weise läßt sich der Einfluß eines "unglücklichen" Meßbeginns minimieren.

Achtung: das nachstehend implementierte Verfahren funktioniert anders als eben beschrieben (und ist insbesondere effizienter), liefert aber dasselbe Ergebnis.

Der Einfachheit halber sollen an dieser Stelle nur Block- und Burstlängen von bis zu 16 Symbolen betrachtet werden:



Da ein RSE-Code mit ParityCount Prüfsymbolen alle Fälle von bis zu ParityCount verlorenen Paketen behandeln kann, stellt sich erneut die Frage, wie hoch der Anteil an "korrigierbaren" Paketverlusten bei gegebener BlockSize und ParityCount ist:

     

Die oben genannte "Bedingung" ParityCount <= DataCount wurde in Efficiency bislang noch nicht berücksichtigt. Dies wird an dieser Stelle nachgeholt:

     

Auch an dieser Darstellung ist gut zu erkennen, daß bereits eine geringe Anzahl von Prüfsymbolen (pro Block) einen großen Teil von "Retransmissions" verhindern kann - selbst wenn die Blockgröße erhöht (und damit der relative Overhead verringert) wird.

Der Einsatz von RSE-Kodierungen lohnt also auf jeden Fall.

Auf der Suche nach einem Verfahren

Das erklärte Ziel ist eine Implementierung von RSE-Kodierung und -Dekodierung in Java - wobei beide Vorgänge möglichst wenig Zeit in Anspruch nehmen dürfen. Aus diesem Grunde wird eine Realisierung mithilfe von Table-Lookups angestrebt:

Kodierung

aus den zu kodierenden Daten wird eine Adresse generiert, an welcher in einer Kodiertabelle die zu diesen Daten gehörenden Prüfinformationen zu finden sind;

Dekodierung

  • Trivialfall
    sind alle Datensymbole vorhanden, so wird die Dekodierung übersprungen

  • Korrekturfall
    sind LossCount Datensymbole verlorengegangen, jedoch mindestens ebensoviele Prüfsymbole vorhanden, so wird zunächst anhand der Positionen der verlorenen Datensymbole sowie der zu verwendenden Prüfsymbole ermittelt, um welchen "Verlustfall" es sich handelt. Aus dieser Zahl, den vorhandenen Daten und genau LossCount Prüfsymbolen wird nun wieder die Adresse errechnet, an der in einer Dekodiertabelle die LossCount fehlenden Datensymbole zu finden sind.

  • Verlustfall
    sind LossCount Datensymbole verlorengegangen und nicht mindestens ebensoviele Prüfsymbole vorhanden, müssen die fehlenden Datensymbole beim Sender neu angefordert werden.

Restriktionen durch die Art der Implementierung

Aus der Forderung, Kodierung und Dekodierung in Java als "Table-Lookup" zu realisieren (und zwar möglichst effizient), ergeben sich eine Reihe von Randbedigungen:

  • will man auf kompliziertere Strukturen verzichten, so bleiben nur Werte vom Java-Typ byte, int, oder long für die Elemente der Kodier- und Dekodiertabellen. Sollen darin (bei gegebener SymbolSize) die Prüfsymbole bzw. rekonstruierten Datensymbole untergebracht werden, so gilt die folgende Beziehung:
      ParityBits := SymbolSize * ParityCount <= 64

  • die Adresse für die Kodiertabelle soll durch Aneinanderreihung der zu kodierenden Datensymbole gewonnen werden, der Inhalt des ersten Symboles werde dabei an der höchstwertigen Stelle plaziert. Bei gegebener SymbolSize berechnet sich die erforderliche Anzahl von Tabellenplätzen (EncoderLength) wie folgt:
      DataBits := SymbolSize * DataCount
      EncoderLength := 2^DataBits

  • zur Dekodierung sollen ebenfalls DataCount Symbole (Daten- oder Prüfsymbole) herangezogen werden ("überzählige" Prüfsymbole werden im Hinblick auf eine möglichst kleine Tabelle ignoriert). Gleichzeitig muß aber auch die aktuelle Fehlersituation (abh. von der Position der fehlenden Datensymbole sowie der zu verwendenden Prüfsymbole) berücksichtigt werden. Eine kombinatorische Betrachtung (siehe Anhang) führt dabei zu folgender Formel für die Anzahl der möglichen Fehlerfälle:
      CaseCount := Sum[
        Product[(DataCount-(i-1))/i, {i,1,Losses}] *
        Product[(ParityCountCount-(j-1))/j, {j,1,Losses}],
      {Losses, 1, ParityCount}]
    unter der Annahme ParityCount <= DataCount. Die Anzahl der Elemente in der Dekodiertabelle (DecoderLength) beträgt somit
      DecoderLength := (2^DataBits) * CaseCount

Will man den Platzbedarf (MemoryLimit) für beide Tabellen zusammen auf einen bestimmten Wert beschränken (z.B. 1MByte) so ergeben sich folgende Kombinationsmöglichkeiten für SymbolSize, BlockSize und DataCount/ParityCount:

Mithilfe der oben berechneten Efficiency läßt sich jeder Parameterkombination aus Choices eine "Güte" zuordnen:

Die berechneten Speichergrößen und "Güten" legen den Entschluß nahe, für eine gegebene SymbolSize mehrere Tabellen für unterschiedliche Kombinationen von BlockSize und ParityCount anzulegen, um sowohl Situationen mit hohen Verlustraten als auch solche mit geringen Verlusten optimal behandeln zu können.

Aufsuchen passender Einträge in Kodier- und Dekodiertabelle

Im Hinblick auf die Implementierung wird es Zeit, sich darüber Gedanken zu machen, wie Kodier- und Dekodiertabelle organisiert sein müssen.

Der Aufbau der Kodiertabelle ist sehr einfach:

  • die Adresse eines Tabelleneintrages ergibt sich durch Aneinanderreihung der Inhalte der einzelnen Datensymbole, wobei das erste Symbol an die höchstwertige Stelle der so gewonnenen Zahl geschrieben werden soll (s.o.);

  • der aus der Tabelle entnommene Wert besteht nun wiederum aus einer Aneinanderreihung von Symbolen (nämlich den Prüfsymbolen), wobei diesmal jedoch das erste Symbol an der wertniedrigsten Stelle stehen soll.

Der Zugriff auf die Dekodiertabelle ist etwas komplizierter:

  • der niederwertige Teil der Tabellenadresse ergibt sich durch Aneinanderreihung der ersten DataCount vorhandenen Daten- oder Prüfsymbole in ihrer ursprünglichen Reihenfolge (verlorene Symbole werden also nicht berücksichtigt!), wobei das erste angetroffene Symbol an die höchstwertige Stelle dieser Teiladresse geschrieben wird;

  • der höherwertige Teil der Tabellenadresse besteht aus der Nummer des zu behandelnden Fehlerfalles (welcher seinerseits angibt, wieviele und welche Datensymbole verloren gegangen sind und welche Prüfsymbole stattdessen verwendet werden sollen).

Durch Aufschreiben der verschiedenen Kombinationsmöglichkeiten von Daten- und Prüfsymbolen bei gegebener BlockSize, DataCount, ParityCount sowie Anzahl verlorener Datensymbole (Losses) kommt man leicht zu folgender kompliziert anmutender Formel zur Berechnung des Fehlerfalles (LossCase):

  LossBase = Sum[
    Product[(DataCount-j)*(ParityCount-j), {j, 0, i-1}],
  {i, 1, Losses-1}]

  Factor = Product[ParityCount-k, {k, 0, Losses-1}]

  LossCase = LossBase + Sum [
    Data[[m]] * Product[DataCount-n, {n,1,Losses-m}]*Factor +
    Parity[[m]]* Product[ParityCount-n, {n,1,Losses-m}], {m,1,Losses-1}] +
    Data[[Losses]]*Factor + Parity[[Losses]
  ]

Darin sind Data[[m]] die Positionen der verlorenen Datensymbole und Parity[[m]] die Positionen der stattdessen zu verwendenden Prüfsymbole.

Man beachte auch, daß z.B. LossBase für den Fall Losses = 1 zu Null wird.

Anhang

Betrachtung zur Anzahl der Fehlerfälle

Durch Aufzeichnen mache man sich klar, daß es die folgende Anzahl von Möglichkeiten für den Verlust von 1, 2, ... Paketen gibt, wenn man zusätzlich o.B.d.A. vereinbart, daß das erste verlorene Paket vor dem zweiten verlorenen Paket usw. liegen soll (n stehe für DataCount):

Die Aufstellung legt nahe, daß hier eine Produktregel vorliegt - zur genaueren Analyse betrachte man die Quotienten der o.a. Glieder:

Setzt man für das erste Glied den Term (-0+n) an und ersetzt außerdem n durch DataCount, so erhält man folgende Formel für die Anzahl der Kombinationsmöglichkeiten verlorener Symbole bei insgesamt Losses Verlusten (von Datensymbolen):

Pro verlorenem Datensymbol wird ein erfolgreich empfangenes Prüfsymbol benötigt. Die Anzahl der Kombinationsmöglichkeiten für die zu verwendenden Prüfsymbole ergibt sich auf ganz ähnliche Weise:

Da bei insgesamt ParityCount Prüfsymbolen bis zu ParityCount Datensymbole rekonstruiert werden können, müssen für das endgültige Ergebnis alle Terme für Losses = 1...ParityCount aufsummiert werden:

Begleitmaterial

Folgende Dateien stehen zusätzlich zum Herunterladen zur Verfügung:

  • createTable.c (ASCII-Datei, 45039 Bytes)
    ein einfaches ANSI-C Programm zur Berechnung der Kodier- und Dekodiertabellen für die Java-Implementierung des Reed-Solomon Erasure Correction
  • RSETest.java (ASCII-Datei, 27492 Bytes)
    ein einfaches Beispiel für die Reed-Solomon Erasure Correction in Java

Kodier/Dekodiertabellen

Die folgenden Dateien enthalten je eine fertige Kodier/Dekodiertabelle für die Java-Implementierung. Die erste Ziffer im Dateinamen gibt dabei die SymbolSize, die zweite die BlockSize und die letzte Ziffer den ParityCount an:

Literaturhinweise

[1] Phil Karn
Reed-Solomon coding/decoding package v1.0
die vom Autor eingesetzten Programme zur Berechnung der Kodier/Dekodiertabellen für die Java-Implementierung des Reed-Solomon-Verfahrens beruhen auf der Arbeit von Phil Karn
(siehe http://www.piclist.com/techref/method/error/rs-gp-pk-uoh-199609/index.htm)

Haftungsausschluß

Bitte beachten Sie auch den Haftungsausschluß des Autors!

http://www.Andreas-Rozek.de/Mathematica/Java-RSEC.html    (letzter Stand: 01.05.2002)