Qual è il modo corretto per convertire 2 byte in un numero intero a 16 bit con segno?


31

In questa risposta , zwol ha affermato:

Il modo corretto di convertire due byte di dati da una fonte esterna in un intero con segno a 16 bit è con funzioni di supporto come questa:

#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;
}

Quale delle funzioni sopra è appropriata dipende dal fatto che l'array contenga una rappresentazione little endian o big endian. L'endianness non è il problema in questione qui, mi chiedo perché zwol sottrae 0x10000udal uint32_tvalore convertito int32_t.

Perché è questo il modo corretto ?

Come evita il comportamento definito dall'implementazione durante la conversione nel tipo restituito?

Dato che puoi assumere la rappresentazione del complemento di 2, come fallirebbe questo cast più semplice: return (uint16_t)val;

Cosa c'è di sbagliato in questa ingenua soluzione:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}

Il comportamento esatto durante il casting int16_tè definito dall'implementazione, quindi l'approccio ingenuo non è portatile.
nwellnhof,

@nwellnhof non c'è cast perint16_t
MM

Non è possibile rispondere alla domanda nel titolo senza specificare quale mappatura utilizzare
MM

4
Entrambi gli approcci si basano sul comportamento definito dall'implementazione (conversione di un valore non firmato in un tipo firmato che non può rappresentare il valore). Per esempio. nel primo approccio, 0xFFFF0001unon può essere rappresentato come int16_t, e nel secondo approccio 0xFFFFunon può essere rappresentato come int16_t.
Sander De Dycker,

1
"Dato che puoi assumere la rappresentazione del complemento di 2" [citazione necessaria]. C89 e C99 certamente non hanno negato le rappresentazioni del complemento e della grandezza del segno. Qv, stackoverflow.com/questions/12276957/...~~V~~singular~~3rd
Eric Torri

Risposte:


20

Se intè a 16 bit, la versione si basa sul comportamento definito dall'implementazione se il valore dell'espressione returnnell'istruzione non è compreso nell'intervallo int16_t.

Tuttavia, anche la prima versione presenta un problema simile; per esempio se int32_tè un typedef per int, e i byte di input sono entrambi 0xFF, allora il risultato della sottrazione nell'istruzione return è UINT_MAXche provoca un comportamento definito dall'implementazione quando convertito in int16_t.

IMHO la risposta a cui ci si collega ha diversi problemi importanti.


2
Ma qual è il modo corretto?
idmean

@idmean la domanda necessita di chiarimenti prima che possa essere data risposta, ho richiesto in un commento sotto la domanda ma OP non ha risposto
MM

1
@MM: ho modificato la domanda specificando che l'endianness non è il problema. IMHO il problema che zwol sta cercando di risolvere è il comportamento definito dall'implementazione durante la conversione nel tipo di destinazione, ma sono d'accordo con te: credo che si sbagli perché il suo metodo ha altri problemi. Come risolvereste il comportamento definito dall'implementazione in modo efficiente?
Chqrlie,

@chqrlieforyellowblockquotes Non mi riferivo specificamente all'endianness. Vuoi solo inserire i bit esatti dei due ottetti di input nel int16_t?
MM

@MM: sì, questa è esattamente la domanda. Ho scritto byte ma la parola corretta dovrebbe effettivamente essere ottetti come è il tipo uchar8_t.
Chqrlie,

7

Questo dovrebbe essere pedanticamente corretto e funzionare anche su piattaforme che usano le rappresentazioni del complemento del bit di segno o 1 , invece del solito complemento di 2 . Si presume che i byte di input siano nel complemento di 2.

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;
}

A causa della filiale, sarà più costoso di altre opzioni.

Ciò che questo compie è che evita qualsiasi ipotesi su come la intrappresentazione si rapporta alla unsignedrappresentazione sulla piattaforma. Il cast a intè necessario per preservare il valore aritmetico per qualsiasi numero che si adatta al tipo di destinazione. Poiché l'inversione garantisce che il bit superiore del numero a 16 bit sarà zero, il valore si adatterà. Quindi l'unario -e la sottrazione di 1 applicano la solita regola per la negazione del complemento di 2. A seconda della piattaforma, INT16_MINpotrebbe comunque traboccare se non si adatta al inttipo sulla destinazione, nel qual caso longdovrebbe essere usato.

La differenza rispetto alla versione originale nella domanda arriva al momento del ritorno. Mentre l'originale viene sempre sottratto 0x10000e il complemento di 2 lascia che il overflow firmato lo avvolga int16_tnell'intervallo, questa versione ha l'esplicito ifche evita il wrapping firmato (che non è definito ).

Ora in pratica, quasi tutte le piattaforme in uso oggi utilizzano la rappresentazione del complemento di 2. In effetti, se la piattaforma ha una norma conforme stdint.hche definisce int32_t, deve usare il complemento di 2 per essa. Laddove questo approccio a volte è utile è con alcuni linguaggi di scripting che non hanno affatto tipi di dati interi - è possibile modificare le operazioni mostrate sopra per i float e darà il risultato corretto.


Lo standard C impone in modo specifico che int16_te tutte le intxx_tloro varianti senza segno debbano utilizzare la rappresentazione del complemento di 2 senza bit di riempimento. Ci vorrebbe un'architettura volutamente perversa per ospitare questi tipi e utilizzare un'altra rappresentazione int, ma immagino che il DS9K possa essere configurato in questo modo.
Chqrlie,

@chqrlieforyellowblockquotes Buon punto, ho cambiato per usare intper evitare la confusione. Infatti se la piattaforma lo definisce int32_tdeve essere il complemento di 2.
jpa

Questi tipi sono stati standardizzati in C99 in questo modo: C99 7.18.1.1 Tipi interi di larghezza esatta Il nome typedef intN_t indica un tipo intero con Nint8_tsegno con larghezza , senza bit di riempimento e una rappresentazione di complemento a due. Pertanto, indica un tipo intero con segno con una larghezza di esattamente 8 bit. Altre rappresentazioni sono ancora supportate dallo standard, ma per altri tipi di numeri interi.
Chqrlie,

Con la versione aggiornata, (int)valueha un comportamento definito dall'implementazione se il tipo intha solo 16 bit. Temo che tu debba usare (long)value - 0x10000, ma sulle architetture di complemento di non 2, il valore 0x8000 - 0x10000non può essere rappresentato come un 16 bit int, quindi il problema rimane.
Chqrlie,

@chqrlieforyellowblockquotes Sì, ho notato lo stesso, ho risolto con ~ invece, ma longavrebbe funzionato ugualmente bene.
jpa,

6

Un altro metodo - utilizzando union:

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

Nel programma:

...
B2I16 conv;

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

first_bytee second_bytepuò essere scambiato secondo il modello little o big endian. Questo metodo non è migliore ma è una delle alternative.


2
Il tipo di unione non punisce il comportamento non specificato ?
Maxim Egorushkin,

1
@MaximEgorushkin: Wikipedia non è una fonte autorevole per l'interpretazione dello standard C.
Eric Postpischil,

2
@EricPostpischil Concentrarsi sul messaggero piuttosto che sul messaggio non è saggio.
Maxim Egorushkin,

1
@MaximEgorushkin: oh sì, oops ho letto male il tuo commento. Supponendo byte[2]che abbiano int16_tle stesse dimensioni, è l'uno o l'altro dei due possibili ordini, non alcuni valori di posizione bit a bit mescolati arbitrariamente. Quindi puoi almeno rilevare in fase di compilazione quale endianness ha l'implementazione.
Peter Cordes,

1
Lo standard afferma chiaramente che il valore del membro del sindacato è il risultato dell'interpretazione dei bit memorizzati nel membro come rappresentazione del valore di quel tipo. Vi sono aspetti definiti dall'implementazione nella misura in cui la rappresentazione dei tipi è definita dall'implementazione.
MM

6

Gli operatori aritmetici si spostano e in modo bit a bit o in espressione (uint16_t)data[0] | ((uint16_t)data[1] << 8)non funzionano su tipi più piccoli di int, in modo che tali uint16_tvalori vengano promossi int(o unsignedse sizeof(uint16_t) == sizeof(int)). Tuttavia, ciò dovrebbe fornire la risposta corretta, poiché solo i 2 byte inferiori contengono il valore.

Un'altra versione pedanticamente corretta per la conversione da big-endian a little-endian (supponendo CPU little-endian) è:

#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);
}

memcpyviene utilizzato per copiare la rappresentazione int16_te questo è il modo conforme allo standard per farlo. Questa versione si compila anche in 1 istruzione movbe, vedere assembly .


1
@MM Un motivo __builtin_bswap16esiste perché lo scambio di byte in ISO C non può essere implementato in modo efficiente.
Maxim Egorushkin,

1
Non vero; il compilatore potrebbe rilevare che il codice implementa lo scambio di byte e tradurlo come un builtin efficiente
MM

1
La conversione int16_tin uint16_tè ben definita: i valori negativi vengono convertiti in valori maggiori di INT_MAX, ma la riconversione di questi valori uint16_tè un comportamento definito dall'implementazione : 6.3.1.3 Intero con segno e senza segno 1. Quando un valore con tipo intero viene convertito in un altro tipo intero diverso da _Bool, se il valore può essere rappresentato dal nuovo tipo, è invariato. ... 3. Altrimenti, il nuovo tipo è firmato e il valore non può essere rappresentato in esso; il risultato è definito dall'implementazione o viene generato un segnale definito dall'implementazione.
Chqrlie,

1
@MaximEgorushkin gcc non sembra funzionare così bene nella versione a 16 bit, ma clang genera lo stesso codice per ntohs/ __builtin_bswape il |/ <<pattern: gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM: Penso che Maxim stia dicendo "impossibile in pratica con gli attuali compilatori". Naturalmente un compilatore non può aspirare per una volta e riconoscere il caricamento di byte contigui in un numero intero. GCC7 o 8 hanno finalmente reintrodotto la coalescenza di carico / deposito per i casi in cui il byte-reverse non è necessario, dopo che GCC3 l'ha lasciato cadere decenni fa. Ma in generale i compilatori tendono ad avere bisogno di aiuto in pratica con molte cose che le CPU possono fare in modo efficiente ma che ISO C ha trascurato / rifiutato di esporre in modo portabile. La ISO C portatile non è un buon linguaggio per una manipolazione efficiente di bit / byte di codice.
Peter Cordes,

4

Ecco un'altra versione che si basa solo su comportamenti portatili e ben definiti (intestazione #include <endian.h> non è standard, il codice è):

#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;
}

La versione little-endian viene compilata con una singola movbeistruzione clang, la gccversione è meno ottimale, vedi assembly .


@chqrlieforyellowblockquotes vostra preoccupazione principale sembra essere stato uint16_tper int16_tla conversione, questa versione non ha che la conversione, così qui si va.
Maxim Egorushkin,

2

Voglio ringraziare tutti i collaboratori per le loro risposte. Ecco cosa si riduce alle opere collettive:

  1. Come per gli standard C 7.20.1.1 tipi interi esatta larghezza : tipi uint8_t, int16_te uint16_tdeve utilizzare la rappresentazione in complemento a due senza bit di riempimento, quindi i bit reali della rappresentazione sono inequivocabilmente quelle dei 2 byte nella matrice, nell'ordine specificato da i nomi delle funzioni.
  2. calcolare il valore di 16 bit senza segno con (unsigned)data[0] | ((unsigned)data[1] << 8) (per la versione little endian) viene compilato in un'unica istruzione e produce un valore a 16 bit senza segno.
  3. Secondo lo standard C 6.3.1.3 Intero con segno e senza segno : la conversione di un valore di tipo uint16_tin tipo con segnoint16_t ha un comportamento definito dall'implementazione se il valore non è compreso nell'intervallo del tipo di destinazione. Non è prevista alcuna disposizione speciale per i tipi la cui rappresentazione è definita con precisione.
  4. per evitare questo comportamento definito dall'implementazione, si può verificare se il valore senza segno è maggiore di INT_MAXe calcolare il valore con segno corrispondente sottraendo 0x10000. Questo per tutti i valori suggeriti da zwol può produrre valori al di fuori dell'intervallo int16_tcon lo stesso comportamento definito dall'implementazione.
  5. il test per il 0x8000bit provoca esplicitamente la compilazione di codice inefficiente da parte dei compilatori.
  6. una conversione più efficiente senza comportamenti definiti dall'implementazione utilizza la punzonatura di tipo tramite un sindacato, ma il dibattito sulla definizione di questo approccio è ancora aperto, anche a livello di Comitato C Standard.
  7. la punzonatura di tipo può essere eseguita in modo portabile e con un comportamento definito utilizzando memcpy.

Combinando i punti 2 e 7, ecco una soluzione portatile e completamente definita che si compila in modo efficiente in una singola istruzione con gcc e clang :

#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;
}

Assemblaggio a 64 bit :

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

Non sono un avvocato di lingua, ma solo i chartipi possono alias o contenere la rappresentazione di oggetti di qualsiasi altro tipo.uint16_tnon è uno dei chartipi, quindi quello memcpydi uint16_tto int16_tnon è un comportamento ben definito. Lo standard richiede solo che la char[sizeof(T)] -> T > char[sizeof(T)]conversione memcpysia ben definita.
Maxim Egorushkin,

memcpyof uint16_tto int16_tè definito al meglio dall'implementazione, non portatile, non ben definito, esattamente come assegnazione dell'uno all'altro, e non è possibile aggirarlo magicamente con memcpy. Non importa se uint16_tusa o meno la rappresentazione del complemento a due, o se sono presenti o meno i bit di padding - che non è un comportamento definito o richiesto dallo standard C.
Maxim Egorushkin,

Con così tante parole, la tua "soluzione" si riduce a sostituire r = ual memcpy(&r, &u, sizeof u)ma quest'ultimo non è migliore del precedente, è vero?
Maxim Egorushkin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.