Effiziente BitStream-Implementierung

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Antworten
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Hallo zusammen,

mich würde mal interessieren, wie man einen BitStream effizient implementieren kann. Mit BitStream meine ich einen Stream (kann ruhig ausschließlich auf einem Byte-Array oder ähnlichem basieren), in den man eine beliebige Anzahl von Bits an eine beliebige Position schreiben kann und beliebige Bits von einer beliebigen Position lesen kann.

Aus eigenen Versuchen weiß ich, dass das Nadelöhr Lese- und Schreiboperation (vorallem aber letzteres) innerhalb des Streams sind (also nicht am Ende -> kein Appending).

Ich will da auch nicht groß Anforderungen definieren und auch die Sprache ist erstmal egal. Mich interessiert mal rein der logische Ansatz. Zum Beispiel ob man unten drunter mit Bytes arbeitet oder mit größeren Datentypen wie 32- oder 64-Bit-Integer. Wie kann man schnell Daten schreiben? Ist Little-Endian oder Big-Endian eventuell einfacher umzusetzen? Welche Zusatzvariablen benötigt man (z.B. Bitposition innerhalb des Bytes/Ints, Gesamtanzahl von Bits, Zeiger auf das aktuelle Byte/Int, usw)? Wie kann man die Anzahl der (teuren) Operationen pro Lese-/Schreiboperation minimieren? Und so weiter.

Habt ihr dazu vielleicht ein paar Ideen?

Mein letzter Ansatz war:

- Byte-Array/Byte-Stream als Container
- Zusatzparameter: Bitpostion im aktuellen Byte, Aktuelles Byte im Container/Streamoffset des Byte-Streams, Gesamtanzahl von Bits
- Appending als Spezialfall von Insert
- Schreiboperation in 1-3 Teile aufgeteilt (Anfangsbits, volle Bytes, Endbits), die abhängig von BitStream-BitOffset und Anzahl zu schreibender Bits sind
- Anfangsbits und Endbits werden dann über Masking, Shifting, usw hinzugefügt, nachfolgende Daten müssen aber teuer umkopiert und geshiftet werden
- volle Bytes die auf Byte-Boundaries passen können ziemlich schnell geschrieben werden

Der Ansatz hat an vielen Stellen Schwachstellen. Daher wollte ich hier mal fragen, wie ihr sowas umsetzen würdet.
Ohne Input kein Output.
Benutzeravatar
Krishty
Establishment
Beiträge: 8240
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Krishty »

std::vector<bool> war bisher immer eine Spezialisierung, die die einzelnen bools als Bits gepackt hat. Ich weiß nicht, ob das im aktuellen C++-Standard immernoch so ist.

Jedenfalls haben wir den auf der Arbeit eingesetzt um Mikroallokationen zu verfolgen. Ende vom Lied war, dass das Gesamtsystem im Einsatz 10 % langsamer war als mit einem std::vector<char>, in den man einfach 0 und 1 schreibt. Und da es, wie gesagt, um das Verfolgen von Mikroallokationen ging, die die Leistung dominierten, gehe ich davon aus, dass solche Bitfelder in der Praxis mindestens 10-Mal langsamer sind als Byte-Arrays. Es lohnt sich also nur wenn man ums Verrecken den Speicherplatz sparen muss.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Naja den BitStream möchte ich vorallem für Komprimierungsaufgaben verwenden, wo es um viele MB bis vielleicht sogar GB Daten geht. Da ist der Faktor 8 schon sehr hoch. Zumal ich aus dem Bitstream bestimmte Bitsequenzen auch als Werte interpretieren will. Für diesen Zweck müsste ich dann die Bytes wieder in Bits umwandeln, was auch wieder Aufwand ist.

Ich hätte den Verwendungszweck mit nennen sollen. Also Hauptziel des BitStreams ist hier vorallem auch den Speicherplatz zu minimieren. Dass dadurch die Performance schlechter ist, als bei reinen ByteStreams ist natürlich klar.
Ohne Input kein Output.
Benutzeravatar
Krishty
Establishment
Beiträge: 8240
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Krishty »

Dann brauchst du aber nicht so etwas wie Lese- und Schreiboperationen mitten im Stream, oder? Die Kompressionen laufen ja normalerweise linear durch die Bits bzw. geben sie nacheinander aus.

Da reicht dann ein Array von 32 Bytes in die du 0 und 1 schreibst, und sobald du das 32te erreichst, packst du alle in eine Zahl und schreibst die in deine Datei.
Zuletzt geändert von Krishty am 24.04.2014, 13:32, insgesamt 1-mal geändert.
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Schrompf
Moderator
Beiträge: 4855
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Schrompf »

Ich habe einen std::vector<> als Speicher druntergelegt und schreibe jeweils in einen uint64_t darin. Bei mir gab es nie was anderes als append(), daher hat für mich dann noch ein schlichter BitIndex innerhalb des aktuellen QuadWords ausgereicht. Lese- und Schreib-Operationen sind maximal zwei Operationen bei mir - eine Spezialisierung für volle Bytes (oder QuadWords bei mir) könnte aber nützlich werden, wenn man deutlich mehr Daten als ein Grundelement auf einmal schreiben will. Was bei mir nie der Fall war.

Die uint64_t schreibe ich dann einfach als LittleEndian-Zahlen in den Speicher, auf Platte oder ins Netzwerk. Alle Plattformen, die ich bisher im Blick habe, sind LE. Und wenn mal eine BE-Plattform dazu kommt, tut es eine einzelne Intrinsic jeweils beim Lesen und Schreiben, was auf aktuellen CPUs praktisch kostenlos sein dürfte.

Ich habe das ganze System allerdings noch nicht profiled. Ich wollte irgendwann noch eine Option ausprobieren, die den Ergebnis-uint64 einfach immer sofort schreibt oder liest, um das eine if() noch loszuwerden, aber ich kann mir gut vorstellen, dass das eher langsamer denn schneller wird. Meine Schreiboperationen liegen praktisch immer im niedrigen einstelligen Bitbereich, da dürfte die Sprungvorhersage ausreichend gute Arbeit leisten, damit das if() quasi ohne Stocken überrannt wird. Ich denke, da sind Cache-Effizienzen bei den Quelldaten 10x wichtiger.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Krishty hat geschrieben:Dann brauchst du aber nicht so etwas wie Lese- und Schreiboperationen mitten im Stream, oder? Die Kompressionen laufen ja normalerweise linear durch die Bits bzw. geben sie nacheinander aus.

Da reicht dann ein Array von 32 Bytes in die du 0 und 1 schreibst, und sobald du das 32te erreichst, packst du alle in eine Zahl und schreibst die in deine Datei.
Naja ich will aber z.B. auch Zahlenwerte schreiben und um daraus dann erst die Bytes zu erstellen müsste ich wieder über jedes Bit iterieren. Also es geht mir hier nicht nur um sowas wie Huffman. Denkbar ist z.B. auch, dass ich Werte habe, von denen ich weiß dass sie immer im Bereich von 0-31 liegen und ich daher alles als 5-Bit-Werte rausschreibe oder aber bestimmte RLE-Daten, die mit weniger als 8-Bit arbeiten. Ich habe in der Regel auch nicht Einzelbits, sondern Werte von 1 bis 64 Bit Länge. Und ehrlich gesagt würde ich gern auch "normale Daten" gleich mit reinschreiben. Wenn ich z.B. ein char-Array anhängen muss und danach wieder kodierte Daten kommen, dann möchte ich nicht unbedingt Extrastreams erstellen.

Und ja Appending ist die Regel, aber ich hatte auch schon Fälle wo das Inserting wichtig war. Beispielsweise habe ich Daten kodiert und die Länge der kodierten Daten dann vorn in den Stream geschrieben, da beim Lesen der Daten vorher die Länge bekannt sein musste. Hier könnte man eventuell ein DWORD reinstopfen und sicherstellen, dass hier feste Byte-Grenzen vorliegen oder die Länge außerhalb des Bitstreams verarbeiten, aber ich würde schon gern wissen wie man das Einfügen innerhalb des BitStreams relativ effizient gestalten könnte. Es gibt schon Fälle wo man erst Daten berechnen muss und diese dann weiter vorn im Stream benötigt (Längen/Größen, Checksum, etc). Headerdaten müssen nicht unbedingt immer am Anfang stehen und selbst wenn sie an festen Grenzen stehen, müsste ich sie im BitStream eventuell dort einfügen während schon andere Daten im Bitstream folgen. Vielleicht würde aber das Einfügen von primitiven Datentypen an festen Byte-Grenzen genügen, so dass man sich aufwendiges Geshifte spart. Das ist eigentlich schonmal eine gute Erkenntnis. Eventuell könnte man auch vorher einfach schon genügend Platz anhängen und später nur ersetzen. Ich hatte aber z.B. die Länge der kodierten Daten selbst als EncodedInt abgelegt, der für sich dann auch eine Länge von 1-4 Bytes hatte und ich somit vorher nicht wusste wie lang er wird.

Schrompf hat geschrieben:Ich habe einen std::vector<> als Speicher druntergelegt und schreibe jeweils in einen uint64_t darin. Bei mir gab es nie was anderes als append(), daher hat für mich dann noch ein schlichter BitIndex innerhalb des aktuellen QuadWords ausgereicht. Lese- und Schreib-Operationen sind maximal zwei Operationen bei mir - eine Spezialisierung für volle Bytes (oder QuadWords bei mir) könnte aber nützlich werden, wenn man deutlich mehr Daten als ein Grundelement auf einmal schreiben will. Was bei mir nie der Fall war.

Die uint64_t schreibe ich dann einfach als LittleEndian-Zahlen in den Speicher, auf Platte oder ins Netzwerk. Alle Plattformen, die ich bisher im Blick habe, sind LE. Und wenn mal eine BE-Plattform dazu kommt, tut es eine einzelne Intrinsic jeweils beim Lesen und Schreiben, was auf aktuellen CPUs praktisch kostenlos sein dürfte.

Ich habe das ganze System allerdings noch nicht profiled. Ich wollte irgendwann noch eine Option ausprobieren, die den Ergebnis-uint64 einfach immer sofort schreibt oder liest, um das eine if() noch loszuwerden, aber ich kann mir gut vorstellen, dass das eher langsamer denn schneller wird. Meine Schreiboperationen liegen praktisch immer im niedrigen einstelligen Bitbereich, da dürfte die Sprungvorhersage ausreichend gute Arbeit leisten, damit das if() quasi ohne Stocken überrannt wird. Ich denke, da sind Cache-Effizienzen bei den Quelldaten 10x wichtiger.
Ok wenn du QuadWords nimmst wird wahrscheinlich nie mehr als 1 Element geschrieben. Bei mir war es bislang so, dass man maximal 64 Bit schreiben/lesen konnte, aber als Container hatte ich Bytes. Somit war es da schon möglich, dass man nur 4 volle Bytes schreibt.

Naja das mit LE/BE hat ja in dem Fall noch andere Auswirkungen. Wenn der eigentliche Wert über 2 QuadWords läuft musst du ja wissen welche Bits jetzt in welcher Reihenfolge dazugehören. Würde ich den Wert 255 als 8-Bit bei Bitposition 62 schreiben, wären 2 Bit noch im ersten QuadWord und 6 Bit im zweiten. Egal wie ich die lese oder schreibe, ich müsste am Ende wissen ob ich die Bits von oben nach unten oder umgekehrt in die Streamteile/Einzelbytes packe. Natürlich alles kein Problem. Ich dachte nur, man kann eventuell besser optimieren, wenn man LE oder BE nimmt.
Ohne Input kein Output.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4855
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Schrompf »

Nein, es hat keine Auswirkungen. Solange Du feste Elemente schreibst und liest, kann es Dir völlig wurscht sein, wie die im Speicher liegen. Relevant wird das erst, wenn Du zusätzlich zum bitweisen Bearbeiten auch noch direkt reingreifen willst. Und ganz ehrlich: ich rieche da Overengineering.

Wenn Du wirklich an Tempo interessiert bist, würdest Du mal einen Testfall ausprogrammieren und dann nachmessen. Ich vermute da z.B., das Bitschiebereien nicht "aufwändig" sind, sondern zu den schnellsten Operationen gehören, die so ein Prozessor drauf hat. Primär, weil hier alle nötigen Operanden dafür noch aus dem First Level Cache kommen.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Was meinst du mit bitweisem Bearbeiten? Nur Einzelbits schreiben / lesen? Das ist eigentlich nicht mein Ziel. Also ich würde schon gern eine bestimmte Anzahl an Bits in einem Rutsch lesen oder schreiben. Das könnte man auch über mehrfaches Lesen/Schreiben von Einzelbits realisieren, aber ob das dann sehr effizient ist weiß ich nicht.

Naja um das Shiften mach ich mir nicht so viele Sorgen, sondern eher um die Logik drumrum. Gerade wenn man über Byte-/QuadWord-Grenzen hinaus liest muss man schon eine gewisse Logik drumrumbauen. Ok wenn man bei reinem Hinzufügen bleibt und das Einfügen mittendrin weglässt, wird man schon einiges optimieren können und die Logik bleibt recht simpel.

Für mich ist ein Stream halt auch immer etwas, in dem ich Funktionen wie seek verwenden kann. Aber wahrscheinlich reicht in 99% aller Fälle wirklich ein reines append aus. Beim Lesen bin ich mir aber nicht sicher ob man einen BitStream immer von vorn nach hinten lesen will. Wenn ich an eine große zip-ähnliche Datei denke wären so Features wie partielles Entpacken schon etwas erstrebenswertes und das würde dann mit seeking definitiv schneller gehen, als alles zu entpacken und dann wieder was wegzulöschen. Beim Lesen ist das aber wahrscheinlich nicht so ein großes Problem, da man die Daten nicht ändert.
Ohne Input kein Output.
Benutzeravatar
Krishty
Establishment
Beiträge: 8240
Registriert: 26.02.2009, 11:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Krishty »

In Zip-ähnlichen Archiven nutzt man normalerweise eine Solid Block Size; man sagt also z.B., dass ein komprimierter Block immer nur 2 MiB lang sein kann und an einer Byte-Grenze beginnt. Möchte man eine bestimmte Datei aus dem Archiv, springt man zum ersten 2-MiB-Block, in dem ihre Daten liegen und beginnt zu entpacken. „Solide“ Archive bestehen nur aus einem einzelnen Block und komprimieren etwas besser, aber dafür muss man immer das komplette Archiv entpacken, wenn man an eine einzelne Datei will. So umgehen die meisten Archivierungsprogramme Seeking in Bitströmen.

(Außerdem lassen sich so Wiederherstellungsinformationen, Prüfsummen, o.Ä. in die Daten einbetten.)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne
Benutzeravatar
Schrompf
Moderator
Beiträge: 4855
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Schrompf »

BeRsErKeR hat geschrieben:Was meinst du mit bitweisem Bearbeiten? Nur Einzelbits schreiben / lesen.
Nein. Ich meinte: es kann Dir völlig wurscht sein, ob das Ding LE oder BE ist, solange Du die Daten, die Du mit einem Bitstream geschrieben hast, auch nur mit einem Bitstream liest. Probleme gibt es erst, wenn Du in den per Bitstream enstandenen Datenhaufen auch mit normalen Byte-Zugriffen reingreifen willst. Dann und nur dann musst Du festlegen, wie der Kram im Speicher abgelegt wird.

Von Bit-Einzelverarbeitung habe ich nichts gesagt. Das wäre in der Tat die maximal ineffiziente Möglichkeit.

So sieht der Schreiber bei mir aus:

Code: Alles auswählen

/// Der TBitSchreiber wird mit einem TSpeicher erzeugt, in den er schreiben soll. Dann kann man damit einzelne Bits 
/// oder Bitgruppen schreiben. Bei Zerstörung oder bei Beenden() wird das momentan angefangene Byte mit Nullbits aufgefüllt
/// und geschrieben.
/// Da der BitSchreiber intern einige Bytes puffern muss, ist es eine sehr schlechte Idee, mehrere TBitSchreiber oder 
/// TSegmentSchreiber auf einem TSpeicher gleichzeitig zu betreiben. Daher ist der TBitSchreiber auch nicht kopierbar, nur movable.
class TBitSchreiber
{
public:
  /// Konstruktor mit dem Speicher, in den geschrieben werden soll
  TBitSchreiber( TSpeicher& pSpeicher)
  {
    mSpeicher = &pSpeicher;
    mPuffer = 0; mPosition = 0;
  }

  /// Destruktor, beendet noch das aktuelle Byte mit Nullen und schreibt es.
  ~TBitSchreiber()
  {
    if( mSpeicher )
      Beenden();
  }

  /// Beendet das Schreiben vorzeitig, schreibt alle übrigen Daten im Puffer
  void Beenden()
  {
    tassert( mSpeicher );
    // beschriebene Bytes des aktuellen Puffers noch schreiben
    size_t anzBytes = (mPosition + 7) / 8;
    for( size_t a = 0; a < anzBytes; ++a )
      mSpeicher->SchreibeByte( (mPuffer >> (8*a)) & 0xff);
  }

private:
  TBitSchreiber( const TBitSchreiber& bs);
  TBitSchreiber& operator = (const TBitSchreiber& bs);

public:
  /// Move-Konstruktor
  TBitSchreiber( TBitSchreiber&& bs) { *this = std::move( bs); }
  /// Move-Operator
  void operator = (TBitSchreiber&& bs) 
  { 
    mSpeicher = bs.mSpeicher; mPuffer = bs.mPuffer; mPosition = bs.mPosition;
    bs.mSpeicher = nullptr;
  }

public:
  /// Schreibt die untersten x Bits der gegebenen Zahl in den Bitstrom, maximal 64. Die höheren Bits der Zahl werden ignoriert.
  void SchreibeBits( size_t pAnzBits, uint64_t pWert)
  {
    tassert( mSpeicher );
    mPuffer |= (pWert & BitHelfer::MachEins<uint64_t>( pAnzBits)) << mPosition;
    mPosition += pAnzBits;

    // falls die Bits nicht komplett in den aktuellen Puffer gepasst haben, schreiben wir den Puffer weg und fangen einen neuen an
    if( mPosition >= 64 )
    {
      mSpeicher->SchreibeQuadWort( mPuffer);
      mPuffer = (pWert >> (pAnzBits + 64 - mPosition)) & BitHelfer::MachEins<uint64_t>( mPosition - 64);
      mPosition -= 64;
    }
  }

protected:
  /// der Speicher, auf dem wir arbeiten. Kann null sein, wenn wir weiterkopiert wurden
  TSpeicher* mSpeicher;

  /// das aktuell in Konstruktion befindliche Byte und Puffer obendrauf
  uint64_t mPuffer;

  /// Bitposition, ab der geschrieben wird
  size_t mPosition;
};
Die Komfortfunktionen für diverse Datentypen habe ich entfernt. Kannst Du natürlich auch alles noch auf Stream-Operatoren umbauen, aber für so eine Fire&Forget-Serialisierung war das Ding eigentlich nicht gedacht. Man könnte jetzt noch Tempo rausholen, indem man den Stream nicht auf einem externen Speicherblock erstellt, sondern ein instanz-internes Feld beschreibt, aber da mein TSpeicher auch schon eine Small Block Optimization hat, bei der er dann auch nur auf dem Stack drei Byte weiter liegt, bringt das wenig. Wie gesagt: die Cache-Optimierung bei den Quelldaten dürfte 10x wichtiger sein als irgendwelche Detailoptimierungen an den 8 wirksamen Zeilen hier.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
DerAlbi
Establishment
Beiträge: 269
Registriert: 20.05.2011, 05:37

Re: Effiziente BitStream-Implementierung

Beitrag von DerAlbi »

Also das Lesen eines Bitstreams ist auch Gegenstand z.B. in der Mp3-Dekodierung. Ich habe gerade mal in den Helix Mp3 Fixkommma-Decoder geschaut:

dort wird ein Cache verwenet, der so breit ist, wie die Maschine verarbeiten kann. Für die Zielarchitektur war das 32bit.
Der Cache wird Byteweise nachgefüllt, wobei der Sonderfall von n=4 Bytes getrennt behandelt wird. (ne nach dem ob unalgined Zugriff erlaubt ist, oder nicht)

Eine Funktion GetBits(...) kann dann maximal 31 bit auf einmal abrufen. (Reicht dem Huffman-Decoder wohl auch)
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Krishty hat geschrieben:In Zip-ähnlichen Archiven nutzt man normalerweise eine Solid Block Size; man sagt also z.B., dass ein komprimierter Block immer nur 2 MiB lang sein kann und an einer Byte-Grenze beginnt. Möchte man eine bestimmte Datei aus dem Archiv, springt man zum ersten 2-MiB-Block, in dem ihre Daten liegen und beginnt zu entpacken. „Solide“ Archive bestehen nur aus einem einzelnen Block und komprimieren etwas besser, aber dafür muss man immer das komplette Archiv entpacken, wenn man an eine einzelne Datei will. So umgehen die meisten Archivierungsprogramme Seeking in Bitströmen.

(Außerdem lassen sich so Wiederherstellungsinformationen, Prüfsummen, o.Ä. in die Daten einbetten.)
Ja das wird wohl so ziemlich immer ok sein. Selbst Blöcke mit dynamischer Größe sind ja kein Thema wenn man die Blockgröße als Primitivtyp (z.B. DWORD) am Blockanfang hinterlegt. Ist ja dann eine volle Byte-Grenze. Die Vermeidung von Änderungen innerhalb des Bitstreams ist in der Tat eine große Vereinfachung. Dann nehm ich lieber solche harmlosen Randbedingungen in Kauf.

Schrompf hat geschrieben:
BeRsErKeR hat geschrieben:Was meinst du mit bitweisem Bearbeiten? Nur Einzelbits schreiben / lesen.
Nein. Ich meinte: es kann Dir völlig wurscht sein, ob das Ding LE oder BE ist, solange Du die Daten, die Du mit einem Bitstream geschrieben hast, auch nur mit einem Bitstream liest. Probleme gibt es erst, wenn Du in den per Bitstream enstandenen Datenhaufen auch mit normalen Byte-Zugriffen reingreifen willst. Dann und nur dann musst Du festlegen, wie der Kram im Speicher abgelegt wird.

Von Bit-Einzelverarbeitung habe ich nichts gesagt. Das wäre in der Tat die maximal ineffiziente Möglichkeit.
Ok da hab ich dich einfach falsch verstanden. Stimmt natürlich. Allerdings werde ich wohl auch direkt Byte-Zugriffe verwenden. Allerdings nur an vollen Byte-Grenzen. Sollte nicht so aufwendig sein.

Schrompf hat geschrieben: So sieht der Schreiber bei mir aus:

...

Die Komfortfunktionen für diverse Datentypen habe ich entfernt. Kannst Du natürlich auch alles noch auf Stream-Operatoren umbauen, aber für so eine Fire&Forget-Serialisierung war das Ding eigentlich nicht gedacht. Man könnte jetzt noch Tempo rausholen, indem man den Stream nicht auf einem externen Speicherblock erstellt, sondern ein instanz-internes Feld beschreibt, aber da mein TSpeicher auch schon eine Small Block Optimization hat, bei der er dann auch nur auf dem Stack drei Byte weiter liegt, bringt das wenig. Wie gesagt: die Cache-Optimierung bei den Quelldaten dürfte 10x wichtiger sein als irgendwelche Detailoptimierungen an den 8 wirksamen Zeilen hier.
Ah super. Das sieht doch schon recht gut aus. Sehr simpel und für die meisten Zwecke ausreichend. Vielen Dank. Nach sowas hab ich gesucht.
Ohne Input kein Output.
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Ich habe nochmal einen Blick auf deinen code geworfen Schrompf. Wenn ich das jetzt nicht falsch interpretiere sorgt deine Beenden-Funktion ja doch dafür, dass LE/BE entscheidend ist, jedenfalls wenn ich davon ausgehe, dass ich Beenden mehrfach aufrufen kann und danach eventuell auch weiterschreibe. Weil du schreibst ja dort die EInzelbytes in fester Reihenfolge raus. Wenn ich danach dann nochmal ein Write aufrufe liegen die Bytes beim Lesen vielleicht nicht mehr in der korrekten Reihenfolge vor wenn ich die falsche Endianess verwende. Oder bin ich gerade nur gaga?
Ohne Input kein Output.
Benutzeravatar
Schrompf
Moderator
Beiträge: 4855
Registriert: 25.02.2009, 23:44
Benutzertext: Lernt nur selten dazu
Echter Name: Thomas Ziegenhagen
Wohnort: Dresden
Kontaktdaten:

Re: Effiziente BitStream-Implementierung

Beitrag von Schrompf »

Nein, das siehst Du richtig. Zum Einen ist Beenden() zum Beenden gedacht, nicht um danach weiterzuschreiben. Das kannst Du also gern mit einer Assertion absichern. Und zum Anderen habe ich nicht gesagt, dass dem Reader die Endianess egal sein kann. Ich habe gesagt, dass es Dir egal sein kann. Der Code ist auf Little Endian geschrieben und muss dort natürlich funktionieren. Wenn ich ihn auf BE portiere, würde ich beim Lesen und Schreiben des QuadWortes jeweils eine Endian-Korrektur einbauen und alles läuft wieder wie vorher.

Evtl. meinst Du aber auch, dass dieser Bitstream auch Puffer im Vielfachen eines uint64_t braucht. Das stimmt. Meine Pufferklasse stellt das sicher, aber es ist natürlich nicht selbstverständlich für beliebige Puffer.

[edit] Mir fällt auf, dass das ausweichend bzw. ungenau ist. Die Beenden()-Funktion ist so geschrieben, damit sie auf LE funktioniert und ich trotzdem am Ende nicht ein ganzes QuadWort rausschreiben muss. Die Aussage "Endianess kann egal sein" gilt dann nicht mehr, das stimmt. Ich benutze diese Klasse auch zum Zusammenbauen von Netzwerk-Nachrichten, da sind mir die paar Byte am Ende wichtig. Im Gegensatz dazu sind BE-Umgebungen heutzutage aber verdammt selten geworden. Daher halte ich Endian-Korrekturen hier für unnötige Hirnaktivität.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
Benutzeravatar
BeRsErKeR
Establishment
Beiträge: 689
Registriert: 27.04.2002, 22:01

Re: Effiziente BitStream-Implementierung

Beitrag von BeRsErKeR »

Normalerweise mach ich auch alles mit LE. Hier dachte ich nur, BE würde mir mehr bringen, da ich auch auf Byte-, Word- und Dword-Ebene reingreifen wollte. Ich habe das aber nun auch auf deine Weise mit LE gelöst. Bevor ich primitive Datentypen schreibe oder lese gehe ich zunächst auf die nächste 8-Bit-Grenze, falls nicht schon geschehen, und schreibe/lese dann als ob ich 8, 16, 32 oder 64 Bit schreibe. Das funktioniert super. Ich kann beim Lesen an eine beliebige Stelle gehen und lese den korrekten Wert und kann auch die primitiven Datentypen dazwischen schreiben ohne dass das Probleme macht. Das Seeking basiert zwar der Einfachheit halber auf Byte-Index im darunterliegenden Stream und einem BitOffset von 0-7, aber beim Lesen lese ich darauf basierend einfach ein QWord von der passenden 64-Bit-Grenze (und eventuell noch das nächste). Der Code ist ziemlich simpel geblieben und ich kann damit so ziemlich alles machen, was ich wollte: Seeking beim Lesen, primitive Datentypen lesen und schreiben, usw.

Die Beenden-Funktion habe ich bei mir durch eine Flush-Funktion ersetzt, die man auch zwischendrin aufrufen kann. Allerdings setzt meine voraus, dass immer die vollen 64 Bit rausgeschrieben werden. Da meine Bitsequenzen sehr lang sind, machen die paar Bytes in meinem Fall nichts aus.

Vielen Dank nochmal für deine Hilfe Schrompf.
Ohne Input kein Output.
Antworten