Was ist der richtige Weg, um 2 Bytes in eine vorzeichenbehaftete 16-Bit-Ganzzahl umzuwandeln?

31

In dieser Antwort machte zwol diese Behauptung:

Die korrekte Methode zum Konvertieren von zwei Datenbytes von einer externen Quelle in eine 16-Bit-Ganzzahl mit Vorzeichen besteht in folgenden Hilfsfunktionen:

#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 8) | 
                   (((uint32_t)data[1]) << 0);
    return ((int32_t) val) - 0x10000u;
}

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    uint32_t val = (((uint32_t)data[0]) << 0) | 
                   (((uint32_t)data[1]) << 8);
    return ((int32_t) val) - 0x10000u;
}

Welche der oben genannten Funktionen geeignet ist, hängt davon ab, ob das Array eine Little-Endian- oder eine Big-Endian-Darstellung enthält. Endianness ist hier nicht das fragliche Thema, ich frage mich, warum zwol0x10000u von dem uint32_tumgerechneten Wert subtrahiert int32_t.

Warum ist das der richtige Weg ?

Wie wird das implementierungsdefinierte Verhalten bei der Konvertierung in den Rückgabetyp vermieden?

Wie würde diese einfachere Besetzung scheitern, da Sie die Komplementdarstellung von 2 annehmen können: return (uint16_t)val;

Was ist los mit dieser naiven Lösung:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
chqrlie
quelle
Das genaue Verhalten beim Casting in int16_tist implementierungsdefiniert, sodass der naive Ansatz nicht portierbar ist.
Nwellnhof
@wellnhof gibt es keine Besetzung zuint16_t
MM
Die Frage im Titel kann nicht beantwortet werden, ohne anzugeben, welches Mapping verwendet werden soll
MM
4
Beide Ansätze basieren auf einem implementierungsdefinierten Verhalten (Konvertieren eines vorzeichenlosen Werts in einen vorzeichenbehafteten Typ, der den Wert nicht darstellen kann). Z.B. im ersten Ansatz 0xFFFF0001ukann nicht als dargestellt werden int16_t, und im zweiten Ansatz 0xFFFFukann nicht als dargestellt werden int16_t.
Sander De Dycker
1
"Da Sie die Komplementdarstellung von 2 annehmen können" [Zitieren erforderlich]. C89 und C99 bestritten sicherlich nicht die 1s-Komplement- und Vorzeichengrößen-Darstellungen. Qv, stackoverflow.com/questions/12276957/…
Eric Towers

Antworten:

20

Wenn intes sich um 16-Bit handelt, stützt sich Ihre Version auf ein implementierungsdefiniertes Verhalten, wenn der Wert des Ausdrucks in der returnAnweisung außerhalb des Bereichs für liegt int16_t.

Die erste Version hat jedoch auch ein ähnliches Problem; Wenn int32_tbeispielsweise ein typedef für intist und die Eingabebytes beide sind 0xFF, führt das Ergebnis der Subtraktion in der return-Anweisung UINT_MAXzu einem implementierungsdefinierten Verhalten bei der Konvertierung in int16_t.

IMHO hat die Antwort, auf die Sie verlinken, mehrere Hauptprobleme.

MM
quelle
2
Aber was ist der richtige Weg?
idmean
@idmean die Frage muss geklärt werden, bevor sie beantwortet werden kann. Ich habe in einem Kommentar unter der Frage darum gebeten, aber OP hat nicht geantwortet
MM
1
@MM: Ich habe die Frage bearbeitet und angegeben, dass Endianness nicht das Problem ist. IMHO ist das Problem, das zwol zu lösen versucht, das implementierungsdefinierte Verhalten bei der Konvertierung in den Zieltyp, aber ich stimme Ihnen zu: Ich glaube, er irrt sich, da seine Methode andere Probleme hat. Wie würden Sie das implementierungsdefinierte Verhalten effizient lösen?
Chqrlie
@chqrlieforyellowblockquotes Ich bezog mich nicht speziell auf Endianness. Möchten Sie nur die genauen Bits der beiden Eingangsoktette in das einfügen int16_t?
MM
@ MM: Ja, das ist genau die Frage. Ich habe Bytes geschrieben, aber das richtige Wort sollte in der Tat Oktette sein wie der Typ ist uchar8_t.
Chqrlie
7

Dies sollte pedantisch korrekt sein und auch auf Plattformen funktionieren, die Vorzeichenbit- oder 1-Komplementdarstellungen anstelle des üblichen 2-Komplements verwenden . Es wird angenommen, dass die Eingangsbytes im Zweierkomplement sind.

int le16_to_cpu_signed(const uint8_t data[static 2]) {
    unsigned value = data[0] | ((unsigned)data[1] << 8);
    if (value & 0x8000)
        return -(int)(~value) - 1;
    else
        return value;
}

Aufgrund der Branche ist es teurer als andere Optionen.

Dies führt dazu, dass jegliche Annahme vermieden wird, wie sich intRepräsentation auf unsignedRepräsentation auf der Plattform bezieht . Die Umwandlung in intist erforderlich, um den arithmetischen Wert für jede Zahl beizubehalten, die in den Zieltyp passt. Da die Inversion sicherstellt, dass das oberste Bit der 16-Bit-Zahl Null ist, passt der Wert. Dann -wenden die Unäre und die Subtraktion von 1 die übliche Regel für die Komplementnegation von 2 an. Je nach Plattform kann INT16_MINes immer noch zu einem Überlauf kommen, wenn es nicht in den intTyp auf dem Ziel passt. In diesem Fall longsollte es verwendet werden.

Der Unterschied zur Originalversion in der Frage ergibt sich aus der Rückgabezeit. Während das Original nur immer subtrahiert wird 0x10000und das 2er-Komplement den signierten Überlauf in den int16_tBereich ifumschließt , hat diese Version das explizite , das signierte Wrapover vermeidet (was undefiniert ist ).

In der Praxis verwenden heute fast alle heute verwendeten Plattformen die Komplementdarstellung von 2. In der Tat, wenn die Plattform standardkonform iststdint.h definiert int32_t, muss sie das 2er-Komplement verwenden. Manchmal ist dieser Ansatz bei einigen Skriptsprachen nützlich, die überhaupt keine ganzzahligen Datentypen haben. Sie können die oben gezeigten Vorgänge für Floats ändern und erhalten das richtige Ergebnis.

jpa
quelle
Der C-Standard schreibt ausdrücklich vor, dass int16_tund alle intxx_tund ihre vorzeichenlosen Varianten die Zweierkomplementdarstellung ohne Auffüllbits verwenden müssen. Es würde eine absichtlich perverse Architektur interfordern , um diese Typen zu hosten und eine andere Darstellung zu verwenden , aber ich denke, der DS9K könnte auf diese Weise konfiguriert werden.
Chqrlie
@chqrlieforyellowblockquotes Guter Punkt, ich habe geändert int, um die Verwirrung zu vermeiden. In der Tat, wenn die Plattform definiert int32_t, muss es die Ergänzung von 2 sein.
jpa
Diese Typen wurden in C99 folgendermaßen standardisiert: C99 7.18.1.1 Ganzzahlentypen mit exakter Breite Der Typedeedef-Name intN_t bezeichnet einen vorzeichenbehafteten Ganzzahltyp mit Breite N, keinen Füllbits und einer Zweierkomplementdarstellung. Somit int8_tbezeichnet einen vorzeichenbehaftete Ganzzahl - Typen mit einer Breite von genau 8 Bits. Andere Darstellungen werden vom Standard weiterhin unterstützt, jedoch für andere Ganzzahltypen.
Chqrlie
(int)valueHat mit Ihrer aktualisierten Version das Implementierungsverhalten definiert, wenn der Typ intnur 16 Bit hat? Ich fürchte, Sie müssen verwenden (long)value - 0x10000, aber auf Komplement-Architekturen von Nicht-2 kann der Wert 0x8000 - 0x10000nicht als 16-Bit dargestellt werden int, sodass das Problem bestehen bleibt.
Chqrlie
@chqrlieforyellowblockquotes Ja, habe gerade das Gleiche bemerkt, ich habe stattdessen mit ~ behoben, longwürde aber genauso gut funktionieren.
jpa
6

Eine andere Methode - mit union :

union B2I16
{
   int16_t i;
   byte    b[2];
};

Im Programm:

...
B2I16 conv;

conv.b[0] = first_byte;
conv.b[1] = second_byte;
int16_t result = conv.i;

first_byteund second_bytekann nach Little- oder Big-Endian-Modell getauscht werden. Diese Methode ist nicht besser, aber eine der Alternativen.

i486
quelle
2
Ist der Unionstyp nicht ein nicht spezifiziertes Verhalten ?
Maxim Egorushkin
1
@ MaximEgorushkin: Wikipedia ist keine maßgebliche Quelle für die Interpretation des C-Standards.
Eric Postpischil
2
@EricPostpischil Es ist unklug, sich eher auf den Messenger als auf die Nachricht zu konzentrieren.
Maxim Egorushkin
1
@ MaximEgorushkin: Oh ja, hoppla, ich habe deinen Kommentar falsch verstanden. Unter der Annahme , byte[2]und int16_tdie gleiche Größe haben , ist es das eine oder das andere der beiden möglichen Anordnungen, nicht einige willkürlich gemischt bitweise Stellenwerte. So können Sie zumindest zur Kompilierungszeit feststellen, welche Endianness die Implementierung hat.
Peter Cordes
1
Der Standard besagt eindeutig, dass der Wert des Vereinigungselements das Ergebnis der Interpretation der im Element gespeicherten Bits als Wertdarstellung dieses Typs ist. Es gibt implementierungsdefinierte Aspekte, sofern die Darstellung von Typen implementierungsdefiniert ist.
MM
6

Die arithmetischen Operatoren verschieben sich und bitweise - oder im Ausdruck - (uint16_t)data[0] | ((uint16_t)data[1] << 8)funktionieren nicht bei Typen, die kleiner als sind int, sodass diese uint16_tWerte auf int(oder) hochgestuft werdenunsigned wenn sizeof(uint16_t) == sizeof(int)) heraufgestuft werden . Dies sollte jedoch die richtige Antwort liefern, da nur die unteren 2 Bytes den Wert enthalten.

Eine andere pedantisch korrekte Version für die Konvertierung von Big-Endian in Little-Endian (unter der Annahme einer Little-Endian-CPU) ist:

#include <string.h>
#include <stdint.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    memcpy(&r, data, sizeof r);
    return __builtin_bswap16(r);
}

memcpywird verwendet, um die Darstellung von zu kopieren, int16_tund dies ist die standardkonforme Methode, um dies zu tun. Diese Version wird auch in 1 Anweisung kompiliert movbe, siehe Assembly .

Maxim Egorushkin
quelle
1
@MM Ein Grund __builtin_bswap16dafür ist, dass Byte-Swapping in ISO C nicht so effizient implementiert werden kann.
Maxim Egorushkin
1
Nicht wahr; Der Compiler konnte erkennen, dass der Code das Austauschen von Bytes implementiert, und es als effizientes integriertes System übersetzen
MM
1
Die Konvertierung int16_tin uint16_tist gut definiert: Negative Werte werden in Werte konvertiert, die größer als sind INT_MAX, aber die Konvertierung dieser Werte in uint16_tist ein implementierungsdefiniertes Verhalten: 6.3.1.3 Vorzeichenbehaftete und vorzeichenlose Ganzzahlen 1. Wenn ein Wert mit Ganzzahltyp in einen anderen Ganzzahltyp als_Bool konvertiert wird, wenn Der Wert kann durch den neuen Typ dargestellt werden, er bleibt unverändert. ... 3. Andernfalls wird der neue Typ signiert und der Wert kann nicht darin dargestellt werden. Entweder ist das Ergebnis implementierungsdefiniert oder es wird ein implementierungsdefiniertes Signal ausgelöst.
Chqrlie
1
@MaximEgorushkin gcc scheint in der 16-Bit-Version nicht so gut zu funktionieren, aber clang generiert den gleichen Code für ntohs/ __builtin_bswapund das |/ <<pattern: gcc.godbolt.org/z/rJ-j87
PSkocik
3
@MM: Ich denke, Maxim sagt "kann in der Praxis nicht mit aktuellen Compilern". Natürlich konnte ein Compiler nicht einmal saugen und das Laden zusammenhängender Bytes in eine ganze Zahl erkennen. GCC7 oder 8 haben die Last / Speicher-Koaleszenz für Fälle wieder eingeführt, in denen keine Byte-Umkehrung erforderlich ist, nachdem GCC3 sie vor Jahrzehnten fallen gelassen hat. Im Allgemeinen benötigen Compiler jedoch in der Praxis Hilfe bei vielen Aufgaben, die CPUs effizient ausführen können, die ISO C jedoch vernachlässigt / nicht portabel verfügbar macht. Portable ISO C ist keine gute Sprache für eine effiziente Manipulation von Codebits / Bytes.
Peter Cordes
4

Hier ist eine andere Version, die sich nur auf tragbare und genau definierte Verhaltensweisen stützt (Header #include <endian.h>ist nicht Standard, der Code ist):

#include <endian.h>
#include <stdint.h>
#include <string.h>

static inline void swap(uint8_t* a, uint8_t* b) {
    uint8_t t = *a;
    *a = *b;
    *b = t;
}
static inline void reverse(uint8_t* data, int data_len) {
    for(int i = 0, j = data_len / 2; i < j; ++i)
        swap(data + i, data + data_len - 1 - i);
}

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
#if __BYTE_ORDER == __LITTLE_ENDIAN
    uint8_t data2[sizeof r];
    memcpy(data2, data, sizeof data2);
    reverse(data2, sizeof data2);
    memcpy(&r, data2, sizeof r);
#else
    memcpy(&r, data, sizeof r);
#endif
    return r;
}

Die Little-Endian - Version kompiliert auf einzelne movbeAnweisung mit clang, gccVersion ist weniger optimal, siehe Montage .

Maxim Egorushkin
quelle
@chqrlieforyellowblockquotes Ihr Hauptanliegen gewesen zu sein scheint , uint16_tum int16_tUmwandlung, diese Version nicht , dass die Umwandlung nicht haben, können Sie so hier.
Maxim Egorushkin
2

Ich möchte allen Mitwirkenden für ihre Antworten danken. Das, worauf die kollektiven Werke hinauslaufen:

  1. Gemäß den C Standard 7.20.1.1 Exact-Breite Integer - Typen : Typen uint8_t, int16_tund uint16_t, ohne Füllbits Komplementdarstellung Zweierkomplement verwenden muß , so dass die tatsächlichen Bits der Darstellung eindeutig diejenige des 2 Bytes in der Anordnung sind, in der angegebenen Reihenfolge durch die Funktionsnamen.
  2. Berechnen des vorzeichenlosen 16-Bit-Werts mit (unsigned)data[0] | ((unsigned)data[1] << 8) (für die Little-Endian-Version) wird zu einem einzelnen Befehl kompiliert und ergibt einen vorzeichenlosen 16-Bit-Wert.
  3. Gemäß C-Standard 6.3.1.3 Ganzzahlen mit und ohne Vorzeichen : Konvertieren eines Wertes vom Typ uint16_tin einen vorzeichenbehafteten Typint16_t hat die Implementierung ein definiertes Verhalten, wenn der Wert nicht im Bereich des Zieltyps liegt. Für Typen, deren Darstellung genau definiert ist, sind keine besonderen Vorkehrungen getroffen.
  4. Um dieses implementierungsdefinierte Verhalten zu vermeiden, kann man testen, ob der vorzeichenlose Wert größer als ist, INT_MAXund den entsprechenden vorzeichenbehafteten Wert durch Subtrahieren berechnen 0x10000. Wenn Sie dies für alle von zwol vorgeschlagenen Werte tun, können Werte außerhalb des Bereichs von erzeugt werdenint16_t mit demselben implementierungsdefinierten Verhalten erzeugt.
  5. Das 0x8000explizite Testen des Bits führt dazu, dass die Compiler ineffizienten Code erzeugen.
  6. Eine effizientere Konvertierung ohne implementierungsdefiniertes Verhalten verwendet Typ-Punning über eine Gewerkschaft, aber die Debatte über die Definiertheit dieses Ansatzes ist selbst auf der Ebene des C-Standard-Ausschusses noch offen.
  7. Typ Punning kann portabel und mit definiertem Verhalten mit durchgeführt werden memcpy.

In Kombination der Punkte 2 und 7 finden Sie hier eine tragbare und vollständig definierte Lösung, die effizient zu einer einzelnen Anweisung mit gcc und clang kompiliert werden kann :

#include <stdint.h>
#include <string.h>

int16_t be16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[1] | ((unsigned)data[0] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

int16_t le16_to_cpu_signed(const uint8_t data[2]) {
    int16_t r;
    uint16_t u = (unsigned)data[0] | ((unsigned)data[1] << 8);
    memcpy(&r, &u, sizeof r);
    return r;
}

64-Bit-Assembly :

be16_to_cpu_signed(unsigned char const*):
        movbe   ax, WORD PTR [rdi]
        ret
le16_to_cpu_signed(unsigned char const*):
        movzx   eax, WORD PTR [rdi]
        ret
chqrlie
quelle
Ich bin kein Sprachanwalt, aber nur charTypen können Alias ​​oder die Objektdarstellung eines anderen Typs enthalten. uint16_tist nicht einer der charTypen, so dass das memcpyvon uint16_tto int16_tkein genau definiertes Verhalten ist. Der Standard erfordert nur eine char[sizeof(T)] -> T > char[sizeof(T)]Konvertierung mit memcpyeiner genauen Definition.
Maxim Egorushkin
memcpyof uint16_tto int16_tist bestenfalls implementierungsdefiniert, nicht portabel, nicht genau definiert, genau wie die Zuordnung von einem zum anderen, und das kann man mit nicht magisch umgehen memcpy. Es spielt keine Rolle, ob uint16_tdie Zweierkomplementdarstellung verwendet wird oder nicht oder ob Füllbits vorhanden sind oder nicht - das ist kein Verhalten, das vom C-Standard definiert oder verlangt wird.
Maxim Egorushkin
Bei so vielen Worten läuft Ihre "Lösung" darauf hinaus, zu ersetzen r = u, memcpy(&r, &u, sizeof u)aber die letztere ist nicht besser als die erstere, oder?
Maxim Egorushkin