Inhalt
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
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:
Datenaustausch binary-Daten über Rechnergrenzen, in binary-Files, über Telegramme, über Dual-Port-Ram. In Unix hatte man stark auf den textuellen Austausch von Daten orientiert.
Flexibilität der Rechner, Rechenwerk kann 16 oder 32 bit.
64-bit-Operationen
Den algorithmischen Notwendigkeiten für bestimmte Datenbreiten wurde wohl zuwenig Aufmerksamkeit geschenkt. Möglicherweise
ist dies auch im Zusammenhang mit Software-Reusing zu sehen. Wenn für eine 32-bit-Maschine im high-end-Bereich mit int
als 32 bit gearbeitet wird, dann kann wohl dieses Programm nie auf einer 16-bit-Maschnine laufen können ...?
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.
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:
int8_t
, int16_t
, int32_t
, int64_t
: feste Bitbreiten
uint8_t
, uint16_t
, uint32_t
, uint64_t
: feste Bitbreiten unsigned
uintptr_t
: Integer Typ mit der Bitbreite eines Pointers.
aber auch:
uint_least16_t
etc.: smallest unsigned integer type with width of at least 8, 16, 32 and 64 bits respectively
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:
Kam viel zu spät, die verschiedenen Bezeichner sind schon längst etabliert und nicht einfach wegzubekommen.
Wird kaum von einem Compiler aus dem Jahre 1999 sofort unterstützt, von manchen Compilern erst nach Jahren.
Es werden oft ältere Compiler benutzt, die Software muss sich nach dem ältesten Compiler (...für ein bestimmtes Zielsystem) richten.
Viel zu viele Varianten, lange unschöne Bezeichnungen fördert nicht die Verwendung
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:
Keine unsigned-Datentypen, da sich diese häufig als Fehlerquelle erwiesen haben. Man braucht kaum den erweiterten Zahlenbereich
2147483648 bis 4294967295 als Grund für ein uint32_t
.
Nur Typen mit festen Bitbreiten: byte
, short
, int
, long
für 8, 16, 32, 64 bit.
Weniger ist mehr.
Mit diesem Schema kommen Java-Programmierer, die auch teils maschinennah entwickeln und realisieren, hin.
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.
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.
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.
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.