Datentypen int, short, alignment

Datentypen int, short, alignment

Inhalt


1 Datenanordnungen in C, alignment, short vs. int16

Topic:.int_short.

Ein häufiges Problem bei der Nutzung von Quellen aus verschiedenen Bereichen für verschiedene Plattformen ist die Unverträglichkeit der Grunddatentypen, die meist althergebracht eigen definiert sind. INT16 vs. int_16 usw.

Ein weiteres Problem ist die Nichtbeachtung von Alignment-Problemen, wenn zunächst plattformspezifisch programmiert wurde.

Auch die Ariane 5 ist 1996 wegen eines dusseligen Zahlenbereichsüberlaufs abgestürzt https://www.golem.de/news/softwarefehler-in-der-raumfahrt-in-den-neunzigern-stuerzte-alles-ab-1511-117537.html


1.1 Ursprünglicher Ansatz in C, Datentypen short, int, long

Topic:.int_short..

Im ursprünglichen Ansatz in C im Zeitraum von 1970 bis ...88 war man der Meinung, dass eine flexible Gestaltung der Datenbitbreiten der Integer-Typen günstiger ist. Rechner waren damals meist 16-bit-Maschinen (1970 noch nicht die Mikroprozessoren), 32 bit galt als High-end, aber im Standard zu beachten. Wenn der int-Typ auf die Datenbitbreite des Rechenwerks ausgerichtet ist, dann sei dies optimal. short ist laut C-Standard die 'kurze' Variante, möglicherweise dem int-Typ entsprechend, möglicherweise kürzer, also 16 bit wenn int 32 bit ist, long ist die längere Variante. Damit kömme man hin. Bei einem 24-bit-Rechenwerk ist int selbstverständlich 24 bit breit.

Was damals weniger bis nicht im Fokus stand:


1.2 Jeder macht es anders, Definition der festen Bitbreiten

Topic:.int_short..

Die algorithmischen Notwendigkeit, im Quelltext die Bitbreite festzulegen und damit sowohl Daten auf einem 16- oder 32-Bit-Rechner gleich anzuordnen als auch definitiv 16 oder 32 bit zu haben unabhängig vom Zielprozessor führte dann schon in den 1980-ger Jahren zur Definition eigener Typen mit fester Bitbreite:

typedef int INT32;
typedef short INT16;

oder

typedef long int_32_t;
typedef short int_16_t;

Diese Typdefinitionen gehören dann selbstverständlich in ein extra Headerfile der zielsystemspezifisch ist. Da dieses Schema etwa in der gleichen Art jeder für sich definiert hat, sind die Bezeichner irgendwie ähnlich aber unterschiedlich. Wenn nun in einem Softwareteil steht:

INT32 myVariable;

Im anderen Softwareteil, gewachsen in der Nachbarabteilung, wird aber verlangt:

void myRoutine(int_32_t* variable);

dann liefert der Aufruf von

myRoutine(&myVariable);

einen Compilerfehler, den man als Warnung abschwächen kann. Man hat dann aber kritisch falsche Zeigertypverwechslungen auch nur als Warning.

Dies ist ein Dilemma.


1.3 Einheitliche Definition der Typen mit festen Bitbreiten

Topic:.int_short..

Erst 10 Jahre später hat man dann im C99-Standard eine Einheitlichkeit geschaffen:

http://en.cppreference.com/w/c/types/integer

Im Headerfile stdint.h sind also folgende Typen definiert:

aber auch:

Also auch hier wieder die Flexibilität mit verschiedenen Typen, um einen Algorithmus möglichst schnell hinzubekommen, aber nicht portabel.

Das Problem an diesem Standard ist:


1.4 Einheitliche und eindeutige Definition der Typen in Java

Topic:.int_short..

Java wurde Anfang 1990 bis 1995 eigentlich entwickelt, um für die vielen mitlerweile etablierten Prozessoren eine einheitliche Programmierbasis anzubieten, also für embedded-Anwendungen. Das ist das gleiche Ansinnen wie C 1970, aber mit 25 Jahre Erfahrung. Daher hat man einen Schnitt gemacht:

Mit diesem Schema kommen Java-Programmierer, die auch teils maschinennah entwickeln und realisieren, hin.


1.5 Alignment, Bytezugriff

Topic:.int_short..

Neben dem Problem mit den nicht-festen Bitbreiten gibt es ein noch viel größeres Problem: Das Alignment im Speicher. Entwickelt man nur für Intel- oder ähnlich Prozessoren, auf dem PC, dann kennt man dies Problem in seiner Bedeutung so nicht. Ein Intel-Prozessor kann auf jedes Byte zugreifen. Liegt ein int32-Wert an einer ungeraden Speicheradresse - kein Problem. Die Daten werden sowieso in den prozessorinternen Cache transportiert. Der Datenzugriff auf dem 32-Bit-Bus ist also unrelevant. Man kann sich das Alignment auch noch wünschen mit entsprechenden #pragma-Befehlen des Compilers, also festlegen wie man will.

Es gibt jedoch Prozessoren, die aus Hardwareersparnisgründen (Kosten, Chipfläche, Strombedarf) keinen Byte-Zugriff erlauben sondern beispielsweise als 32-bit-Prozessor nun auf 32-Bit-Grenzen. 16-bit-Befehle werden dabei unterstützt, indem ein Halbwort gelesen wird (kein Problem) oder ein Halbwort ergänzt wird mit dem bestehendem Inhalt der anderen Hälfte vor dem Speichern (atomarer Zugriff nötig), oder in dem der Speicher 2 Chip-Select-Signale für High und Low-Word á 16 bit hat. Wenn man folgende struct compiliert:

typedef struct NonAlignedExample_t
{ int_8 byte1;
  float float2;
  int_16 word3;
  int_16 word4;
  int_8 byte5;
} NonAlignedExample;

dann muss der Compiler drei Füllbytes einfügen nach dem byte1 und 3 Füllbytes am Ende der struct, wenn der Zielprozessor den Zugriff nur auf 32-bit-Daten unterstützt. Überträgt man diese struct nun auf den PC mit byte-Alignment und überträgt die Daten dazu binär, dann werden die Daten falsch gelesen. Auch ein 4-Byte-Alighment hilft nicht, denn der Zielprozessor wird word3 und word4 direkt hintereinander anordnen, wenn er über diese Speicheradressierunngsmöglichkeit verfügt.

Um portable Software zu schreiben sollte man daher folgende Regeln beachten:

Alle Daten müssen auf einer Adressposition stehen, die ihrer Datenlänge entspricht.

Meistenteils kann man die Daten auch nur etwas geschicketer anordnen, und das Problem ist weg. Möglicherweise sollte man (unbedingt manuell programmiert, nicht auf Alignment-Einstellungen verlassen!) Dummies einfügen. Die obige struct sollte also wie folgt aussehen:

typedef struct AlignedExample_t
{ int_8 byte1;
  int_8 byte5;
  int_16 word3;
  float float2;
  int_16 word4;
  int16 dummy_10;
} AlignedExample;

In Austausch-Daten sollte man auf Byte-Daten verzichten.

In der obigen struct könnte es bei einem Zielprozessor zwischen den byte-Datenelementen noch automatische Füllbytes geben, wenn der Prozessor nur einen Zugriff ab 16 bit zulässt. Man hat häufig eigentlich genügend Speicherplatz.

Es gibt Prozessoren, die nur 32-Bit-Zugriffe zulassen

beispielsweise DSPs von Analog Devices. Man sollte mit Blick auf diese Algorithmen auf die Nutzung von 16-bit-Werten verzichten.

Möglicherweise bekommt man Ärger weil ein Zielprozessor ein 8-Byte-Alignment voraussetzt. Daher dies berücksichtigen!

Im Zweifelsfall tut man gut daran, alle struct auf 8 Byte auszurichten. Ein Zielprozessor mit 8-Byte-Alignment wird aber immer den Zugriff auf 32-bit-Werte zulassen, wenn sie auf einer durch 4 teilbaren Adresse stehen.

Demzufolge sieht die obige struct besser wie folgt aus (noch ergänzt mit double):

typedef struct AlignedExample_t
{ int_32 byte1;
  int_32 byte5;
  int_32 word3;
  float float2;
  //
  int_32 word4;
  int32 dummy_0x14;
  //
  double double_auf_0x18;
  //
  float float_0x20;
  int_32 dummy_0x24;  //padding to 8-byte-length
} AlignedExample;

Man braucht nun nicht die Speicheradressen durchzuzählen, dies ist hier nur im Beispiel angedeutet. Man braucht nur den Blick auf 8-Byte-Einheiten zu richten. Im Beispiel könnte man das float_0x20 auch anstelle des dummy_0x14 anordnen. Wo die Daten stehen, ist meist nicht wichtig.


1.6 Nutzung von Typen mit nicht festen Bitbreiten

Topic:.int_short..

Der Verfasser empfielt diese nur für stacklokale Variable, nie in abgespeicherten Strukturen.

int ix;
for(ix = 0; ix < 19; ++ix) { ....

An dieser Stelle würde zwar ein int_8 oder ein short oder dergleichen funktionieren und ausreichen, aber die Registerbreite des Prozessors ist int. Ein Compiler kann hier auch optimieren, wenn der Prozessor über den 16-bit-Zugriff auf Register in einer 32-bit-CPU verfügt, da er bei einfachen for-Schleifen den Wertebereich analysieren kann.

Wenn man beginnt, mit den im C99-Standard definierten Werten INT16_MIN etc. zu operieren, um den Algorithmus an den Zahlenbereich anzupassen, dann ist die Portabilität und Wiederverwendbarkeit von Algorithmen erschwert.

Niemals Abfragen des Zahlenbereiches einbauen, sondern sichere Typen mit fester Bitbreite verwenden.

Wenn schon nicht klar ist, ob ein Zielprozessor möglicherweise ein short oder C99 uint_least16_t auf 32 bit abbilden könnte, dann sollte nicht die Abfrage mit INT_LEAST16_MAX kaschiert werden, sondern konsequent ein int32 verwendet werden.


1.7 Definition der Typen mit festen Bitbreiten

Topic:.int_short..

Diese sollten an zentraler Stelle in einem eigenem Headerfile definiert sein. Der Verfasser verwendet dazu zielsystemspezifische Ausprägungen eines Headerfiles

#include <compl_adaption.h>

der gleichnamig in verschiedenen Verzeichnissen für jedes Zielsystem steht. Bei der Compilierung für das Zielsystem ist der Include-Path entsprechend zu setzen. Die Quellen selbst sind nicht zielsystemspezifisch.

//Negative pattern:
#ifdef __TARGET_XYZ__
  ....
#elif .....

Bedinge Compilierungen für verschiedene Zielsysteme in den Anwendungsquellen schmälern die Portabilität und spätere Wiederverwendung und sind schwer durchschaubar.

Die Definitionen der zielsystemspezifischen Typen sollte nicht mit typedef sondern mit #define erfolgen:

//Negativ pattern:
typedef int int32;
//ok:
#define int32 int

Begründung: Anwenderquellen sind gewachsen. Möglicherweise wird ein älteres Headerfile includiert, das folgende Zeile enthält:

//old legacy code:
typedef INT_32 int32;

Wenn man aus verschiedenen Gründen dieses Quellfile nicht ändern will, dann hilft:

#include <compl_adaption.h>  //.. in another included file, uses #define
....
#undef int32
#include <the_old_legacy-h>  //

Man hat auch das Problem, dass verschiedene System-Includes der Compiler ihre eigenen Typdefinitionen mitbringen, die sich nicht vertragen:

#include <compl_adaption.h>
....
#undef int64
#undef uint64
#include <windows.h>  //defines int64 by itself.

Ein weiterer Grund mit #define zu arbeiten:

#define INT32 int
#define int32 int

Nunmehr vertragen sich INT32*-pointer mit int32*. Bei typedef ist dies nicht der Fall, da die Pointer formal verschiedene Typen sind.

Die althergebrachten verschiedenen Bezeichnungen INT32 usw. müssen nicht zwanghaft umgeschrieben werden.


1.8 Benutze nicht plattformspezifische Header in plattformunabhängigen Programmen

Topic:.int_short..

Ein

#include <windows.h>

hat in einem allgemeinen Anwenderprogramm nichts zu suchen. Es ist nur dann notwendig, wenn die Anpassung für die Windows-api dort formuliert wird. Man sollte plattformspezifisches und allgemeines immer trennen.