Zweckmäßigkeit der Verwendung von Bitfeldern in Strukturen

7

Ich muss eine große Datenmenge (für ein Arduino) in einem Programm verfolgen, während ich mich um eine ganze Reihe anderer Geschäfte kümmere.

Ich habe mit einer Struktur wie dieser begonnen:

struct MyStruct
{
   // note: these names might as well be foo bar baz
   uint8_t color; 
   boolean state; 
   uint8_t area;
   uint8_t number;
   uint8_t len;
};

Ich habe eine Reihe von 800 davon. Bei jeweils 5 Bytes sind das 4 KB oder die Hälfte des RAM auf dem Arduino Mega.

Ich habe zu einer Struktur wie dieser gewechselt:

struct MyStruct
{
   uint8_t color : 3; // max value 7
   // uint8_t state : 1; *** moved, thanks Ignacio ***
   uint8_t area : 5; // m.v. 31
   uint8_t number;
   uint8_t len : 5; // m.v. 31
   uint8_t state : 1; // m.v. 1
};

Ich verstehe, dass dies die Größe jeder Instanz auf 3 Bytes reduziert, was insgesamt 2,4 KB in meinem Array entspricht.

Ich habe gelesen, dass in einigen Implementierungen / Chipsätzen die Verwendung von Bitfeldern in Strukturen zu einer weniger effizienten Ausführung führen kann. Natürlich ist dies eine effizientere Nutzung des Speichers, aber meine Frage ist:

Wie geht der Arduino mit Bitfeldern um? Wird es besser, schlechter oder in Bezug auf Geschwindigkeit oder Geschwindigkeit vernachlässigbar anders sein, wenn es durch ein Array von Bitfeldstrukturen iteriert? Gibt es eine gute Möglichkeit, dies zu testen?

Anthropomo
quelle
3
Diese Reihenfolge verwendet 4 Bytes. Holen Sie sich das 1-Bit-Feld zwischen den anderen 8.
Ignacio Vazquez-Abrams
1
@Ignacio Erstaunlich ... Sie haben gerade weitere 9% meiner gesamten Speichernutzung gespart.
Anthropomo
Der Grund, warum Bitfelder in Strukturen zu einer weniger effizienten Ausführung führen können, liegt in Einschränkungen der Wortausrichtung. Zum Beispiel, wenn sie die natürliche 32/64-Ausrichtung überschreiten und mehrere Zugriffe verursachen. Dies sollte kein Problem für die ATmega
Milliways
1
Die Frage, die Sie sich stellen müssen, ist, ob Sie sich am meisten Sorgen machen, dass Ihnen der Arbeitsspeicher, der Programmspeicher oder die CPU-Zyklen ausgehen. In dieser Situation können Sie auf Kosten des anderen einiges für einen optimieren und einige Auswirkungen auf den dritten haben. Ab einem bestimmten Punkt sollten Sie auch fragen, ob ein ATmega-basiertes Arduino wirklich die richtige Plattform ist.
Chris Stratton
Ich konnte das Programm umstrukturieren, um den Array-Index zu verwenden, in dem ich die 8-Bit-Nummer verwendet habe, also bis zu 2 Bytes pro Struktur, weitere 9% rasiert. Es sieht so aus, als könnte ich die Zyklen schonen. Ich iteriere alle 50 Millisekunden durch die 800 Strukturen und verarbeite gleichzeitig intermittierende http-Anfragen. Keine Probleme. Ich habe mit diesem die ATmega-Grenzen überschritten, aber ich scheine weit darunter zu kommen.
Anthropomo

Antworten:

8

Was Sie sehen, ist der klassische Zeit-Speicher-Kompromiss. Die Bitfelder sind im Speicher kleiner, benötigen jedoch mehr Zeit für die Bearbeitung. Sie können zählen, dass die Bitfelder unabhängig vom Prozessor langsamer sind.

Sie verwenden das Wort effizient , aber dieses Wort hat keine bestimmte Bedeutung ohne eine Metrik für das, was gut oder schlecht ist. Wenn Sie nur 8 KB RAM haben und die Verwendung des Speichers schlecht ist, kann die Zeit günstig sein. Wenn Sie Echtzeitbeschränkungen haben, ist die Verwendung von Zeit schlecht und der Speicher kann billig sein. Im Allgemeinen können Sie sich nur aus diesem Kompromiss herauskaufen. Mit anderen Worten, wenn Sie Zeit und Gedächtnis schlecht finden, geben Sie Geld aus und verwenden Sie einen größeren Chip. Es gibt keine einheitliche Antwort auf das, was gut oder schlecht ist. Dies ist einer der Gründe, warum es für Mikrocontroller so viele Möglichkeiten gibt, dass Menschen den Chip an die Anwendung und die Anwendung an den Chip anpassen.

Das Auffüllen der Bits eines Bitfelds ist langsamer als das Füllen vollständiger Bytes. Nehmen wir zum Beispiel

x = 5;
...
asimplestruct.len = x;
// vs
abitfield.len = x;

Der erste einfache Fall wird nur:

  • Lade den Wert von x in ein Register
  • Speichern Sie es im Byte für len

Der zweite macht so etwas wie:

  • Lädt den aktuellen Wert von abitfield
  • lädt eine Maske
  • löscht die Bits für len
  • lädt den aktuellen Wert von x
  • lädt eine Maske
  • löscht nicht verwendete Bits von x
  • verschiebt die Bits von x
  • oder ist x mit dem abitfield
  • speichert den aktuellen Wert zurück im Speicher

Wenn alle Ihre Vorgänge Daten in das Bitfeld packen oder aus dem Bitfeld entpacken, sollten Sie mit einer langsameren Ausführung rechnen. Bitfelder sind eine Art Komprimierung - sie kosten Ticks.

Das Bewegen in Bitfeldern ist jedoch schneller, da weniger Bytes geladen und gespeichert werden müssen. Wenn Sie dieses Array sortieren, kann die geringere Anzahl von Bytes von Vorteil sein. Wenn Sie sie über eine serielle Schnittstelle übertragen, kann die komprimierte Größe des Bitfelds ein Gewinner sein.

Also in Bezug auf Ihre Frage:

Gibt es eine gute Möglichkeit, dies zu testen?

Der einzige gute Weg, dies zu testen, besteht darin, Testfälle für beide Ansätze mit einem Muster zu schreiben, das genau zu Ihrer Anwendung passt. Es ist wirklich wichtig, welche Mischung von Operationen Sie ausführen, um zu entscheiden, ob der Unterschied vernachlässigbar oder signifikant ist.

Verwenden Sie bei dieser Art von Optimierungsexperimenten auf jeden Fall die Quellcodeverwaltung für Ihr Projekt. Sie können mit nur wenigen Klicks ein lokales GIT- oder Mercurial-Repository erstellen. Wenn Sie eine Kette von Kontrollpunkten beibehalten, können Sie Ihren Code zerreißen und die Auswirkungen verschiedener Implementierungen untersuchen. Wenn Sie falsch abbiegen, können Sie im Repository einfach zum letzten guten Punkt zurückkehren und einen anderen Pfad ausprobieren.

(Randnotiz: Dieses Mal besteht der Speicherkompromiss auch in die entgegengesetzte Richtung. Wenn Sie die Compileroptionen für Desktop-Klassenprozessoren durchsehen, finden Sie etwas, das als Strukturpacken bezeichnet wird . Mit dieser Option können Sie leere Bytes zwischen Einzelbytefeldern hinzufügen, so dass diese Bleiben Sie an Wort- oder Doppelwortgrenzen ausgerichtet. Dies mag verrückt erscheinen, um RAM absichtlich wegzuwerfen, aber auf Prozessoren mit 16 oder 32 Bit breiten Registern und Bussen können sogenannte wort- oder dword-ausgerichtete Speicheroperationen schneller sein als byteweise Operationen.)

jdr5ca
quelle
3

Eine Diskussion der Bitfelder wäre nicht vollständig, ohne Folgendes zu erwähnen:

Strukturen mit Bitfeldern können als tragbarer Weg verwendet werden, um zu versuchen, den für eine Struktur erforderlichen Speicherplatz zu reduzieren (mit den wahrscheinlichen Kosten für die Erhöhung des Befehlsraums und der Zeit, die für den Zugriff auf die Felder erforderlich sind), oder als nicht tragbarer Weg um ein auf Bitebene bekanntes Speicherlayout zu beschreiben.

[Kernighan & Ritchie, Programmiersprache C, 2. Auflage , Anhang A-8.3]

Andere Poster, einschließlich Sie selbst, haben sich mit dem Kompromiss zwischen Raum und Zeit befasst. Was jedoch manchmal übersehen wird (möglicherweise nicht relevant für Ihre Anwendung -?), Ist die vollständige Implementierungsabhängigkeit des Speicherlayouts . Dies ist wichtig, wenn Anwendungen diese Daten mit einem anderen Prozess teilen: Lesen oder Schreiben von Hardware, deren Registerbitzuweisungen notwendigerweise festgelegt sind; Übermittlung der Daten an ein anderes System (über Netzwerk oder Speichergerät spielt keine Rolle); oder zu einer anderen Anwendung auf demselben System, die möglicherweise mit einem anderen Compiler oder einer anderen Version des Compilers kompiliert wurde.

JRobert
quelle
1

Ein kleiner Tipp - ich sehe, dass die Struktur keine 1B für 8-Bit-CPU ist. Am Ende Ihrer Struktur können Sie hinzufügen Polsterung 2 - Bit Länge oder umdenken Erhöhung der Größe des Staates .

struct MyStruct
{
   uint8_t color : 3; // max value 7
   // uint8_t state : 1; *** moved, thanks Ignacio ***
   uint8_t area : 5; // m.v. 31
   uint8_t number;
   uint8_t len : 5; // m.v. 31
   uint8_t state : 1; // m.v. 1
   uint8_t padding : 2;
};
Soerium
quelle
Durch Hinzufügen des Auffüllens wird der dynamische Speicher zur Kompilierungszeit nicht geändert. Verzeihen Sie meine Unwissenheit hier, aber es scheint, als würde sich der Compiler um das Auffüllen kümmern. Oder passiert zur Laufzeit etwas, um dies zu kompensieren, wenn ich die Polsterung nicht einbeziehe?
Anthropomo
Das stimmt. In diesem Fall ist eher eine gute Praxis.
Soerium