Ist es eine gute Praxis, kleinere Datentypen für Variablen zu verwenden, um Speicherplatz zu sparen?

32

Als ich die C ++ - Sprache zum ersten Mal lernte, stellte ich fest, dass neben int, float usw. kleinere oder größere Versionen dieser Datentypen in der Sprache vorhanden waren. Zum Beispiel könnte ich eine Variable x aufrufen

int x;
or 
short int x;

Der Hauptunterschied besteht darin, dass short int 2 Bytes Speicher benötigt, während int 4 Bytes benötigt und short int einen geringeren Wert hat.

int x;
short int x;
unsigned short int x;

das ist noch restriktiver.

Meine Frage ist hier, ob es eine gute Praxis ist, separate Datentypen zu verwenden, je nachdem, welche Werte Ihre Variable im Programm annimmt. Ist es eine gute Idee, Variablen immer nach diesen Datentypen zu deklarieren?

Bugster
quelle
3
Kennen Sie das Flyweight-Designmuster ? "Ein Objekt, das den Speicherverbrauch minimiert, indem es so viele Daten wie möglich mit anderen ähnlichen Objekten teilt. Es ist eine Möglichkeit, Objekte in großer Anzahl zu verwenden, wenn eine einfache wiederholte Darstellung eine inakzeptable Menge an Speicher benötigt ..."
gnat
5
Bei den Standardeinstellungen für den Pack- / Alignment-Compiler werden die Variablen ohnehin an 4-Byte-Grenzen ausgerichtet, sodass möglicherweise überhaupt kein Unterschied besteht.
Nikie
36
Klassischer Fall vorzeitiger Optimierung.
Scarfridge
1
@nikie - Möglicherweise sind sie an einer 4-Byte-Grenze auf einem x86-Prozessor ausgerichtet, dies gilt jedoch nicht im Allgemeinen. MSP430 setzt char auf eine beliebige Byteadresse und alles andere auf eine gerade Byteadresse. Ich denke, dass AVR-32 und ARM Cortex-M gleich sind.
uɐɪ
3
Der zweite Teil Ihrer Frage impliziert, dass durch das Hinzufügen unsignedeiner Ganzzahl weniger Platz belegt wird, was natürlich falsch ist. Die Anzahl der diskreten darstellbaren Werte ist identisch (Geben oder Nehmen 1, je nachdem, wie das Vorzeichen dargestellt wird), es wird jedoch ausschließlich ins Positive verschoben.
Underscore_d

Antworten:

41

In den meisten Fällen sind die Platzkosten vernachlässigbar und Sie sollten sich keine Gedanken darüber machen. Sie sollten sich jedoch Gedanken über die zusätzlichen Informationen machen, die Sie durch die Angabe eines Typs geben. Zum Beispiel, wenn Sie:

unsigned int salary;

Sie geben einem anderen Entwickler nützliche Informationen: Das Gehalt darf nicht negativ sein.

Der Unterschied zwischen short, int und long führt in Ihrer Anwendung selten zu Speicherplatzproblemen. Es ist wahrscheinlicher, dass Sie versehentlich die falsche Annahme treffen, dass eine Zahl immer in einen Datentyp passt. Es ist wahrscheinlich sicherer, immer int zu verwenden, es sei denn, Sie sind zu 100% sicher, dass Ihre Zahlen immer sehr klein sind. Selbst dann ist es unwahrscheinlich, dass Sie merklich Platz sparen.

Oleksi
quelle
5
Es ist wahr, dass es heutzutage selten Probleme geben wird, aber wenn Sie eine Bibliothek oder eine Klasse entwerfen, die ein anderer Entwickler verwenden wird, ist das eine andere Sache. Vielleicht benötigen sie Speicherplatz für eine Million dieser Objekte. In diesem Fall ist der Unterschied groß - 4 MB im Vergleich zu 2 MB nur für dieses eine Feld.
dodgy_coder
30
Die Verwendung unsignedin diesem Fall ist eine schlechte Idee: Nicht nur das Gehalt kann nicht negativ sein, sondern auch die Differenz zwischen zwei Gehältern kann nicht negativ sein. (Im Allgemeinen ist die Verwendung von unsigned für alles andere als Bit-Twiddling und das Definieren des Verhaltens beim Überlauf eine schlechte Idee.)
zvrba
15
@zvrba: Der Unterschied zwischen zwei Gehältern ist selbst kein Gehalt und es ist daher legitim, einen anderen Typ zu verwenden, der signiert ist.
JeremyP
12
@JeremyP Ja, aber wenn Sie C verwenden (und dies scheint auch in C ++ der Fall zu sein), führt die vorzeichenlose Ganzzahlsubtraktion zu einem vorzeichenlosen int , das nicht negativ sein kann. Es könnte sich in den richtigen Wert verwandeln, wenn Sie es in ein vorzeichenbehaftetes int umwandeln, aber das Ergebnis der Berechnung ist ein vorzeichenloses int. Siehe auch diese Antwort für mehr vorzeichenbehaftete / vorzeichenlose Berechnungsverrücktheiten - weshalb Sie niemals vorzeichenlose Variablen verwenden sollten, es sei denn, Sie drehen wirklich ein bisschen herum.
Tacroy
5
@zvrba: Die Differenz ist eine Geldmenge, aber kein Gehalt. Nun könnte man argumentieren, dass ein Gehalt auch eine Geldmenge ist (beschränkt auf positive Zahlen und 0, indem die Eingabe validiert wird, was die meisten Leute tun würden), aber der Unterschied zwischen zwei Gehältern ist selbst kein Gehalt.
JeremyP
29

Das OP sagte nichts über die Art des Systems aus, für das sie Programme schreiben, aber ich nehme an, dass das OP an einen typischen PC mit GB Speicher gedacht hat, da C ++ erwähnt wird. Wie einer der Kommentare besagt, kann die Größe der Variablen selbst bei dieser Art von Speicher einen Unterschied ausmachen, wenn Sie mehrere Millionen Elemente eines Typs haben - beispielsweise ein Array.

Wenn Sie in die Welt der eingebetteten Systeme einsteigen - was nicht wirklich im Rahmen der Frage liegt, da das OP es nicht auf PCs beschränkt - dann ist die Größe der Datentypen sehr wichtig. Ich habe gerade ein kurzes Projekt auf einem 8-Bit-Mikrocontroller abgeschlossen, der nur 8 KByte Programmspeicher und 368 Byte RAM hat. Dort zählt natürlich jedes Byte. Man verwendet niemals eine Variable, die größer ist als sie benötigt (sowohl vom Standpunkt des Raums als auch der Codegröße - 8-Bit-Prozessoren verwenden viele Anweisungen, um 16- und 32-Bit-Daten zu bearbeiten). Warum eine CPU mit so begrenzten Ressourcen verwenden? In großen Mengen können sie nur ein Viertel kosten.

Derzeit arbeite ich an einem weiteren Embedded-Projekt mit einem 32-Bit-MIPS-basierten Mikrocontroller, der 512 KByte Flash und 128 KByte RAM enthält (und etwa 6 USD kostet). Wie bei einem PC beträgt die "natürliche" Datengröße 32 Bit. Jetzt wird es effizienter, Ints für die meisten Variablen anstelle von Zeichen oder Kurzzeichen zu verwenden. Aber auch hier muss jeder Typ von Array oder Struktur berücksichtigt werden, ob kleinere Datentypen gerechtfertigt sind. Im Gegensatz zu Compilern für größere Systeme ist es wahrscheinlicher, dass Variablen in einer Struktur in ein eingebettetes System gepackt werden. Ich achte darauf, immer zuerst alle 32-Bit-Variablen, dann 16-Bit und dann 8-Bit zu setzen, um "Löcher" zu vermeiden.

Tcrosley
quelle
10
+1 für die Tatsache, dass für eingebettete Systeme unterschiedliche Regeln gelten. Die Tatsache, dass C ++ erwähnt wird, bedeutet nicht, dass das Ziel ein PC ist. Eines meiner letzten Projekte wurde in C ++ auf einem Prozessor mit 32 KB RAM und 256 KB Flash geschrieben.
uɐɪ
13

Die Antwort hängt von Ihrem System ab. Im Allgemeinen sind hier die Vor- und Nachteile der Verwendung kleinerer Typen:

Vorteile

  • Kleinere Typen belegen auf den meisten Systemen weniger Speicher.
  • Kleinere Typen ermöglichen auf einigen Systemen schnellere Berechnungen. Dies gilt insbesondere für Float vs Double auf vielen Systemen. Und kleinere int-Typen liefern auf 8- oder 16-Bit-CPUs auch erheblich schnelleren Code.

Nachteile

  • Viele CPUs haben Ausrichtungsanforderungen. Einige greifen schneller auf ausgerichtete Daten zu als nicht ausgerichtete. Einige müssen die Daten ausgerichtet haben, um überhaupt darauf zugreifen zu können. Die größeren Integer-Typen entsprechen einer ausgerichteten Einheit, sodass sie höchstwahrscheinlich nicht fehlausgerichtet sind. Dies bedeutet, dass der Compiler möglicherweise gezwungen ist, kleinere Ganzzahlen in größere zu setzen. Und wenn die kleineren Typen Teil einer größeren Struktur sind, werden möglicherweise verschiedene Füllbytes vom Compiler an einer beliebigen Stelle in die Struktur eingefügt, um die Ausrichtung zu korrigieren.
  • Gefährliche implizite Konvertierungen. C und C ++ haben mehrere undurchsichtige, gefährliche Regeln für die Heraufstufung von Variablen zu größeren, implizit ohne Typumwandlung. Es gibt zwei miteinander verflochtene implizite Konvertierungsregeln, die "ganzzahlige Heraufstufungsregeln" und die "üblichen arithmetischen Konvertierungen". Lesen Sie hier mehr darüber . Diese Regeln sind eine der häufigsten Ursachen für Fehler in C und C ++. Sie können eine ganze Reihe von Problemen vermeiden, indem Sie im gesamten Programm den gleichen Integer-Typ verwenden.

Mein Rat ist, dies zu mögen:

system                             int types

small/low level embedded system    stdint.h with smaller types
32-bit embedded system             stdint.h, stick to int32_t and uint32_t.
32-bit desktop system              Only use (unsigned) int and long long.
64-bit system                      Only use (unsigned) int and long long.

Alternativ können Sie das int_leastn_toder int_fastn_taus stdint.h verwenden, wobei n die Zahl 8, 16, 32 oder 64 ist. Der int_leastn_tTyp bedeutet "Ich möchte, dass dies mindestens n Bytes sind, aber es ist mir egal, ob der Compiler es als zuweist ein größerer Typ für die Ausrichtung ".

int_fastn_t bedeutet "Ich möchte, dass dies n Byte lang ist, aber wenn mein Code dadurch schneller ausgeführt wird, sollte der Compiler einen größeren als den angegebenen Typ verwenden".

Im Allgemeinen sind die verschiedenen stdint.h-Typen viel besser als normale intusw., da sie portabel sind. Es intsollte nicht nur eine bestimmte Breite angegeben werden, um es tragbar zu machen. In Wirklichkeit ist es jedoch schwierig zu portieren, da Sie nie wissen, wie groß es auf einem bestimmten System sein wird.


quelle
Punkt für Punkt in Bezug auf die Ausrichtung. In meinem aktuellen Projekt hat die unbeabsichtigte Verwendung von uint8_t auf einem 16-Bit-MSP430 die MCU auf mysteriöse Weise zum Absturz gebracht (höchstwahrscheinlich kam es irgendwo zu einem falsch ausgerichteten Zugriff, möglicherweise durch GCCs Fehler, möglicherweise nicht). Die bloße Ersetzung von uint8_t durch "unsigned" beseitigte die Abstürze. Die Verwendung von 8-Bit-Typen auf> 8-Bit-Bögen, wenn sie nicht schwerwiegend sind, ist zumindest ineffizient: Der Compiler generiert zusätzliche Anweisungen 'und reg, 0xff'. Verwenden Sie aus Gründen der Portabilität 'int / unsigned' und befreien Sie den Compiler von zusätzlichen Einschränkungen.
Alexei
11

Abhängig von der Funktionsweise des jeweiligen Betriebssystems erwarten Sie im Allgemeinen, dass der Speicher nicht optimiert zugewiesen wird, sodass beim Aufrufen eines Bytes oder eines Wortes oder eines anderen kleinen Datentyps der Wert ein gesamtes Register belegt, das nur sehr stark belegt ist besitzen. Wie Ihr Compiler oder Interpreter dies interpretiert, ist jedoch etwas anderes. Wenn Sie beispielsweise ein Programm in C # kompilieren, kann der Wert ein Register für sich selbst physisch belegen, der Wert wird jedoch einer Grenzüberprüfung unterzogen, um sicherzustellen, dass Sie dies nicht tun Versuchen Sie, einen Wert zu speichern, der die Grenzen des beabsichtigten Datentyps überschreitet.

In Bezug auf die Leistung und wenn Sie solche Dinge sehr umständlich angehen, ist es wahrscheinlich schneller, einfach den Datentyp zu verwenden, der der Zielregistergröße am ehesten entspricht, aber dann verpassen Sie all den netten syntaktischen Zucker, der das Arbeiten mit Variablen so einfach macht .

Wie hilft dir das? Nun, es liegt wirklich an Ihnen, zu entscheiden, für welche Art von Situation Sie programmieren. Für fast jedes Programm, das ich jemals geschrieben habe, reicht es aus, einfach Ihrem Compiler zu vertrauen, um die Dinge zu optimieren und den Datentyp zu verwenden, der für Sie am nützlichsten ist. Wenn Sie eine hohe Genauigkeit benötigen, verwenden Sie die größeren Gleitkommadatentypen. Wenn Sie nur mit positiven Werten arbeiten, können Sie wahrscheinlich eine Ganzzahl ohne Vorzeichen verwenden. Meistens ist es jedoch ausreichend, den Datentyp int zu verwenden.

Wenn Sie jedoch sehr strenge Datenanforderungen haben, wie z. B. das Schreiben eines Kommunikationsprotokolls oder eine Art Verschlüsselungsalgorithmus, kann die Verwendung von Datentypen mit Bereichsprüfung sehr nützlich sein, insbesondere wenn Sie versuchen, Probleme im Zusammenhang mit Datenüberschreitungen / -unterläufen zu vermeiden oder ungültige Datenwerte.

Der einzige andere Grund, warum ich mir spontan vorstellen kann, bestimmte Datentypen zu verwenden, besteht darin, dass Sie versuchen, die Absicht in Ihrem Code zu kommunizieren. Wenn Sie beispielsweise eine Abkürzung verwenden, teilen Sie anderen Entwicklern mit, dass Sie positive und negative Zahlen in einem sehr kleinen Wertebereich zulassen.

S.Robins
quelle
6

Wie Scarfridge kommentierte, ist dies ein

Klassischer Fall vorzeitiger Optimierung .

Der Versuch , zu optimieren für die Speichernutzung könnte in anderen Bereichen der Leistung auswirken, und die goldenen Regeln der Optimierung sind:

Die erste Regel zur Programmoptimierung: Tun Sie es nicht .

Die zweite Regel der Programmoptimierung (nur für Experten!): Tun Sie es noch nicht . "

- Michael A. Jackson

Um zu wissen, ob es jetzt an der Zeit ist, zu optimieren, müssen Benchmarking und Tests durchgeführt werden. Sie müssen wissen, wo Ihr Code ineffizient ist, damit Sie Ihre Optimierungen gezielt durchführen können.

Um herauszufinden, ob die bestimmen optimierte Version des Codes ist sie Seite an Seite mit den gleichen Daten tatsächlich besser als die naive Implementierung zu einem bestimmten Zeitpunkt, müssen Sie Benchmark.

Denken Sie auch daran, dass eine bestimmte Implementierung, die für die aktuelle Generation von CPUs effizienter ist, nicht bedeutet, dass dies immer der Fall sein wird. Meine Antwort auf die Frage Ist die Mikrooptimierung beim Codieren wichtig? beschreibt ein Beispiel aus eigener Erfahrung, bei dem eine veraltete Optimierung zu einer Verlangsamung um eine Größenordnung führte.

Auf vielen Prozessoren sind nicht ausgerichtete Speicherzugriffe erheblich teurer als ausgerichtete Speicherzugriffe. Das Packen einiger Shorts in Ihre Struktur kann bedeuten, dass Ihr Programm jedes Mal, wenn Sie einen der beiden Werte berühren , einen Pack- / Entpack-Vorgang ausführen muss.

Aus diesem Grund ignorieren moderne Compiler Ihre Vorschläge. Wie Nikie kommentiert:

Bei den Standardeinstellungen für den Pack- / Alignment-Compiler werden die Variablen ohnehin an 4-Byte-Grenzen ausgerichtet, sodass es möglicherweise überhaupt keine Unterschiede gibt.

Errate deinen Compiler als Zweites auf deine Gefahr.

Es gibt einen Platz für solche Optimierungen, wenn mit Terabyte-Datensätzen oder eingebetteten Mikrocontrollern gearbeitet wird, aber für die meisten von uns ist dies kein wirkliches Problem.

Mark Booth
quelle
3

Der Hauptunterschied besteht darin, dass short int 2 Bytes Speicher benötigt, während int 4 Bytes benötigt und short int einen geringeren Wert hat.

Das ist falsch. Sie können nicht davon ausgehen, wie viele Bytes jeder Typ enthält, außer chareinem Byte und mindestens 8 Bits pro Byte, wobei die Größe jedes Typs größer oder gleich der vorherigen ist.

Die Performance-Vorteile sind für Stack-Variablen unglaublich gering - sie werden wahrscheinlich trotzdem ausgerichtet / aufgefüllt.

Aus diesem Grund , shortund longhaben praktisch keine heutzutage verwenden, und Sie sind fast immer besser mit int.


Natürlich gibt es auch stdint.hwelche, die vollkommen in Ordnung sind, wenn intsie nicht geschnitten werden. Wenn Sie jemals riesige Arrays von Ganzzahlen / Strukturen zuweisen, intX_tist dies sinnvoll, da Sie effizient sein und sich auf die Größe des Typs verlassen können. Dies ist keineswegs verfrüht, da Sie Megabyte Speicher einsparen können.

Pubby
quelle
1
Tatsächlich kann es mit dem Aufkommen von 64-Bit-Umgebungen longanders sein int. Wenn Ihr Compiler LP64 intist, 32 Bit und long64 Bit, und Sie werden feststellen, dass ints möglicherweise noch 4 Byte ausgerichtet ist (mein Compiler zum Beispiel).
JeremyP
1
@ JeremyP Ja, habe ich etwas anderes gesagt oder so?
Pubby
Ihr letzter Satz, der kurz und lang ist, hat praktisch keinen Sinn. Lange hat sicherlich eine Verwendung, wenn auch nur als Basistyp vonint64_t
JeremyP
@ JeremyP: Du kannst gut mit int und long long leben.
gnasher729
@ gnasher729: Was verwenden Sie, wenn Sie eine Variable benötigen, die Werte von mehr als 65.000 enthalten kann, aber niemals eine Milliarde? int32_t, int_fast32_tUnd longsind alle gute Möglichkeiten, long longist nur verschwenderisch und intnicht tragbar.
Ben Voigt
3

Dies wird von einer Art OOP- und / oder Unternehmer- / Anwendungsgesichtspunkt sein und ist möglicherweise in bestimmten Bereichen / Domänen nicht anwendbar, aber ich möchte das Konzept der primitiven Besessenheit aufgreifen .

Es ist eine gute Idee, unterschiedliche Datentypen für unterschiedliche Arten von Informationen in Ihrer Anwendung zu verwenden. Es ist jedoch wahrscheinlich NICHT ratsam, die integrierten Typen für diesen Zweck zu verwenden, es sei denn, Sie haben ernsthafte Leistungsprobleme (die gemessen und überprüft wurden usw.).

Wenn wir in unserer Anwendung Temperaturen in Kelvin modellieren möchten, KÖNNEN wir ein ushortoder uintähnliches verwenden, um zu bezeichnen, dass "der Begriff negativer Grad Kelvin absurd und ein Domänenlogikfehler ist". Die Idee dahinter ist Ton, aber Sie werden nicht den ganzen Weg gehen. Was wir erkannt haben, ist, dass wir keine negativen Werte haben können. Es ist daher praktisch, wenn wir den Compiler dazu bringen, sicherzustellen, dass niemand einer Kelvin-Temperatur einen negativen Wert zuweist. Es ist AUCH wahr, dass Sie bei Temperaturen keine bitweisen Operationen ausführen können. Und Sie können einer Temperatur (K) kein Maß für das Gewicht (kg) hinzufügen. Aber wenn Sie sowohl Temperatur als auch Masse als uints modellieren , können wir genau das tun.

Die Verwendung von integrierten Typen zur Modellierung unserer DOMAIN-Entitäten führt zwangsläufig zu unordentlichem Code und einigen fehlenden Prüfungen und kaputten Invarianten. Selbst wenn ein Typ EINEN Teil der Entität erfasst (kann nicht negativ sein), kann er andere zwangsläufig übersehen (kann nicht in willkürlichen arithmetischen Ausdrücken verwendet werden, kann nicht als Array von Bits behandelt werden usw.).

Die Lösung besteht darin, neue Typen zu definieren, die kapseln die die Invarianten . Auf diese Weise können Sie sicherstellen, dass Geld Geld ist und Entfernungen Entfernungen sind, und Sie können sie nicht addieren, und Sie können keine negative Entfernung erstellen, aber Sie können einen negativen Geldbetrag (oder eine Verschuldung) erstellen. Natürlich werden diese Typen die eingebauten Typen intern verwenden, aber dies ist vor Clients verborgen . In Bezug auf Ihre Frage zu Leistung / Speicherverbrauch können Sie auf diese Weise die Art und Weise ändern, wie Dinge intern gespeichert werden, ohne die Oberfläche Ihrer Funktionen zu ändern, die auf Ihren Domain-Entitäten ausgeführt werden, falls Sie feststellen, dass a shorteinfach zu verdammt ist groß.

Sara
quelle
1

Ja natürlich. Es ist eine gute Idee zu verwendenuint_least8_t Wörterbücher, Arrays mit großen Konstanten, Puffer usw. zu verwenden. Es ist besser, sie uint_fast8_tfür Verarbeitungszwecke zu verwenden.

uint8_least_t (lagerung) -> uint8_fast_t (Verarbeitung) -> uint8_least_t(Lagerung).

Zum Beispiel nehmen Sie 8-Bit-Symbole von source, 16-Bit-Codes vondictionaries und einige 32-Bit-constants . Dann verarbeiten Sie 10-15-Bit-Operationen mit ihnen und geben 8-Bit aus destination.

Stellen wir uns vor, Sie müssen 2 Gigabyte verarbeiten source. Die Anzahl der Bitoperationen ist sehr groß. Sie erhalten einen hervorragenden Leistungsbonus, wenn Sie während der Verarbeitung auf schnelle Typen umsteigen. Schnelle Typen können für jede CPU-Familie unterschiedlich sein. Sie können einschließen stdint.hund verwendenuint_fast8_t , uint_fast16_t, uint_fast32_tetc.

Sie könnten uint_least8_tstatt uint8_tfür die Portabilität verwenden. Aber niemand weiß tatsächlich, welche moderne CPU diese Funktion verwenden wird. VAC-Maschine ist ein Museumsstück. Vielleicht ist es ein Overkill.

Puchu
quelle
1
Während Sie vielleicht einen Punkt mit den Datentypen haben, die Sie aufgelistet haben, sollten Sie erklären, warum sie besser sind, als nur zu sagen, dass sie sind. Für Leute wie mich, die mit diesen Datentypen nicht vertraut sind, musste ich sie googeln, um zu verstehen, wovon Sie sprechen.
Peter M