Jaki jest prawidłowy sposób konwersji 2 bajtów na 16-bitową liczbę całkowitą ze znakiem?

31

W tej odpowiedzi , Zwol wykonane tego twierdzenia:

Prawidłowym sposobem konwersji dwóch bajtów danych ze źródła zewnętrznego na 16-bitową liczbę całkowitą ze znakiem jest użycie funkcji pomocniczych takich jak to:

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

Która z powyższych funkcji jest odpowiednia, zależy od tego, czy tablica zawiera małą reprezentację endianową, czy dużą. Bajt nie jest sprawa na pytanie tutaj, zastanawiam się dlaczego Zwol odejmuje 0x10000uod uint32_twartości przekształca się int32_t.

Dlaczego to jest właściwy sposób ?

Jak uniknąć zachowania zdefiniowanego w implementacji podczas konwersji na typ zwracany?

Ponieważ możesz założyć reprezentację dopełniacza 2, dlaczego ta prostsza rzutowanie nie powiedzie się: return (uint16_t)val;

Co jest złego w tym naiwnym rozwiązaniu:

int16_t le16_to_cpu_signed(const uint8_t data[static 2]) {
    return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
}
chqrlie
źródło
Dokładne zachowanie podczas rzutowania int16_tjest zdefiniowane w implementacji, więc naiwne podejście nie jest przenośne.
nwellnhof
@nwellnhof nie ma obsadyint16_t
MM
Na pytanie zawarte w tytule nie można odpowiedzieć bez określenia, którego mapowania użyć
MM
4
Oba podejścia opierają się na zachowaniu zdefiniowanym przez implementację (konwertowaniu niepodpisanej wartości na typ podpisany, który nie może reprezentować wartości). Na przykład. w pierwszym podejściu 0xFFFF0001unie można przedstawić jako int16_t, aw drugim podejściu 0xFFFFunie można przedstawić jako int16_t.
Sander De Dycker
1
„Ponieważ można założyć reprezentację uzupełnienia 2” [potrzebne źródło]. C89 i C99 z pewnością nie zaprzeczyły reprezentacjom 1s i reprezentacjom wielkości znaku. Qv, stackoverflow.com/questions/12276957/…
Eric Towers

Odpowiedzi:

20

Jeśli intjest 16-bitowy, wówczas twoja wersja opiera się na zachowaniu zdefiniowanym przez implementację, jeśli wartość wyrażenia w returninstrukcji jest poza zakresem int16_t.

Jednak pierwsza wersja ma podobny problem; na przykład jeśli int32_tjest typedef dla int, a bajty wejściowe są oba 0xFF, to wynikiem odejmowania w instrukcji return jest to, UINT_MAXco powoduje zachowanie zdefiniowane w implementacji po przekonwertowaniu na int16_t.

IMHO odpowiedź, do której linkujesz, ma kilka poważnych problemów.

MM
źródło
2
Ale jaki jest właściwy sposób?
idmean
@idmean pytanie wymaga wyjaśnienia, zanim będzie można na nie odpowiedzieć, poprosiłem w komentarzu pod pytaniem, ale OP nie odpowiedział
MM
1
@MM: Zredagowałem pytanie, aby określić, że endianizm nie jest problemem. IMHO problemem, który próbuje rozwiązać zwolnienie, jest zachowanie zdefiniowane w implementacji podczas konwersji na typ docelowy, ale zgadzam się z tobą: uważam, że jest w błędzie, ponieważ jego metoda ma inne problemy. Jak efektywnie rozwiązałbyś zdefiniowane zachowanie implementacji?
chqrlie
@chqrlieforyellowblockquotes Nie miałem na myśli konkretnie endianizmu. Czy chcesz po prostu wstawić dokładne bity dwóch oktetów wejściowych do int16_t?
MM
@MM: tak, to jest dokładnie pytanie. Napisałem bajty, ale poprawnym słowem powinny być oktety, bez względu na rodzaj uchar8_t.
chqrlie
7

Powinno to być pedantycznie poprawne i działać również na platformach, które używają bitu znaku lub reprezentacji uzupełnienia 1 , zamiast zwykłego uzupełnienia 2 . Zakłada się, że bajty wejściowe są uzupełnieniem 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;
}

Ze względu na oddział będzie droższy niż inne opcje.

Osiąga to to, że pozwala uniknąć jakichkolwiek założeń dotyczących tego, jak to zrobić int powiązania unsignedreprezentacji z reprezentacją na platformie. Rzutowanie intjest wymagane, aby zachować wartość arytmetyczną dla dowolnej liczby pasującej do typu docelowego. Ponieważ inwersja zapewnia, że ​​górny bit 16-bitowej liczby będzie wynosił zero, wartość będzie pasować. Następnie jednostkowe -i odejmowanie 1 stosuje zwykłą regułę dla negacji uzupełnienia 2. W zależności od platformy INT16_MINmoże nadal przepełniać się, jeśli nie pasuje do inttypu na celu, w którym to przypadku longnależy użyć.

Różnica w stosunku do oryginalnej wersji w pytaniu pochodzi z czasu powrotu. Podczas gdy oryginał po prostu zawsze odejmował, 0x10000a dopełnienie 2 pozwalało podpisanemu przepełnieniu zawinąć go do int16_tzakresu, ta wersja ma wyraźne, ifże pozwala uniknąć podpisanego zawijania (które jest niezdefiniowane ).

W praktyce prawie wszystkie obecnie używane platformy używają reprezentacji uzupełnienia 2. W rzeczywistości, jeśli platforma posiada zgodny ze standardami stdint.h, który definiuje int32_t, to należy użyć 2 dopełnienie dla niego. Podejście to czasami się przydaje w przypadku niektórych języków skryptowych, które w ogóle nie mają liczb całkowitych - możesz zmodyfikować operacje pokazane powyżej dla liczb zmiennoprzecinkowych i da to poprawny wynik.

jpa
źródło
Norma C wyraźnie nakazuje, aby int16_twszystkie dowolne intxx_ti ich niepodpisane warianty musiały używać reprezentacji uzupełnienia 2 bez bitów dopełniających. intHostowanie tego typu i używanie innej reprezentacji wymagałoby celowo przewrotnej architektury , ale myślę, że DS9K można skonfigurować w ten sposób.
chqrlie
@chqrlieforyellowblockquotes Dobra uwaga, zmieniłem, aby użyć, intaby uniknąć zamieszania. Rzeczywiście, jeśli platforma określa int32_t, musi być uzupełnieniem 2.
jpa
Typy te zostały znormalizowane w C99 w ten sposób: C99 7.18.1.1 Typy liczb całkowitych o dokładnej szerokości Nazwa typedef intN_t oznacza podpisany typ liczb całkowitych o szerokości N, bez bitów wypełniających i reprezentacji uzupełnienia do dwóch. Oznacza zatem int8_ttyp całkowity ze znakiem o szerokości dokładnie 8 bitów. Inne reprezentacje są nadal obsługiwane przez standard, ale dla innych typów całkowitych.
chqrlie
W zaktualizowanej wersji (int)valuema określone zachowanie implementacji, jeśli typ intma tylko 16 bitów. Obawiam się, że musisz użyć (long)value - 0x10000, ale w architekturach komplementarnych innych niż 2 wartość 0x8000 - 0x10000nie może być reprezentowana jako 16-bitowa int, więc problem pozostaje.
chqrlie
@chqrlieforyellowblockquotes Tak, właśnie zauważyłem to samo, naprawiłem ~, ale longdziałałoby równie dobrze.
jpa
6

Inna metoda - użycie union:

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

W programie:

...
B2I16 conv;

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

first_bytei second_bytemogą być zamieniane według małego lub dużego modelu endian. Ta metoda nie jest lepsza, ale jest jedną z alternatyw.

i486
źródło
2
Czy typ związku nie oznacza zachowania nieokreślonego ?
Maxim Egorushkin
1
@MaximEgorushkin: Wikipedia nie jest wiarygodnym źródłem interpretacji standardu C.
Eric Postpischil
2
@EricPostpischil Koncentrowanie się na komunikatorze zamiast na wiadomości jest nierozsądne.
Maxim Egorushkin
1
@MaximEgorushkin: o tak, oops, źle odczytałem twój komentarz. Zakładając, byte[2]i int16_tsą tej samej wielkości, to jest jeden lub drugi z dwóch możliwych porządków, nie jakaś arbitralna tasuje bitowe miejsce wartości. Dzięki temu możesz przynajmniej wykryć w czasie kompilacji, jaki endianizm ma implementacja.
Peter Cordes
1
Norma wyraźnie stwierdza, że ​​wartość elementu unii jest wynikiem interpretacji przechowywanych bitów w elemencie jako reprezentacja wartości tego typu. Istnieją aspekty zdefiniowane w ramach implementacji, o ile reprezentacja typów jest zdefiniowana w ramach implementacji.
MM
6

Operatory arytmetyczne przesuwają się i bitowo - lub w wyrażeniu (uint16_t)data[0] | ((uint16_t)data[1] << 8)nie działają na typach mniejszych niż int, więc te uint16_twartości są promowane do int(lub unsignedjeśli sizeof(uint16_t) == sizeof(int)). Mimo to powinno to dać poprawną odpowiedź, ponieważ tylko 2 dolne bajty zawierają wartość.

Inną pedantycznie poprawną wersją konwersji z big-endian na little-endian (przy założeniu, że procesor little-endian) to:

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

memcpysłuży do skopiowania reprezentacji int16_ti jest to zgodny ze standardami sposób. Ta wersja kompiluje się również w 1 instrukcji movbe, patrz instrukcja montażu .

Maxim Egorushkin
źródło
1
@MM Jednym z powodów __builtin_bswap16jest to, że zamiana bajtów w ISO C nie może być wdrożona tak skutecznie.
Maxim Egorushkin
1
Nie prawda; kompilator może wykryć, że kod implementuje zamianę bajtów i przetłumaczyć go jako wydajne narzędzie wbudowane
MM
1
Konwersja int16_tdo uint16_tjest dobrze zdefiniowana: wartości ujemne są konwertowane na wartości większe niż INT_MAX, ale konwersja tych wartości z powrotem uint16_tjest zachowaniem zdefiniowanym w ramach implementacji: 6.3.1.3 Liczby całkowite ze znakiem i bez znaku 1. Gdy wartość o typie całkowitym jest konwertowana na inny typ całkowity inny niż Bool, jeśli wartość może być reprezentowana przez nowy typ, pozostaje niezmieniona. ... 3. W przeciwnym razie nowy typ jest podpisany i nie można w nim reprezentować wartości; wynik jest albo zdefiniowany w implementacji, albo podniesiony jest sygnał w implementacji.
chqrlie
1
@MaximEgorushkin gcc nie wydaje się tak dobrze w wersji 16-bitowej, ale clang generuje ten sam kod dla ntohs/ __builtin_bswapi |/ <<pattern: gcc.godbolt.org/z/rJ-j87
PSkocik
3
@MM: Myślę, że Maxim mówi „nie mogę w praktyce z obecnymi kompilatorami”. Oczywiście kompilator nie mógł ani razu ssać i rozpoznać ładowania ciągłych bajtów do liczby całkowitej. GCC7 lub 8 w końcu ponownie wprowadziły funkcję koalescencji obciążenia / sklepu dla przypadków, w których odwracanie bajtów nie jest potrzebne, po tym jak GCC3 porzucił to dekady temu. Ale generalnie kompilatory zwykle potrzebują pomocy w praktyce przy wielu rzeczach, które procesory mogą zrobić wydajnie, ale których ISO C zaniedbało / odmawiało przenośnego ujawnienia. Przenośny ISO C nie jest dobrym językiem do wydajnej manipulacji bitami / bajtami kodu.
Peter Cordes
4

Oto kolejna wersja, która opiera się wyłącznie na przenośnych i dobrze zdefiniowanych zachowaniach (nagłówek #include <endian.h>nie jest standardem, kod jest):

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

Wersja Little-Endian kompiluje się do pojedynczej movbeinstrukcji clang, gccwersja jest mniej optymalna, patrz zestawienie .

Maxim Egorushkin
źródło
@chqrlieforyellowblockquotes Twoim głównym problemem wydaje się być uint16_tdo int16_tkonwersji, ta wersja nie posiada tej konwersji, więc tutaj jesteś.
Maxim Egorushkin
2

Chcę podziękować wszystkim autorom za ich odpowiedzi. Oto, co sprowadza się do pracy zbiorowej:

  1. Zgodnie z normą C 7.20.1.1 Typy liczb całkowitych o ścisłej szerokości : typy uint8_t,int16_t i uint16_tmuszą stosować uzupełnienie dwójkowe reprezentację bez bitów wypełniających, więc rzeczywiste bity reprezentacji są jednoznacznie te z 2 bajtów w tablicy, w kolejności określonej przez nazwy funkcji.
  2. obliczenie 16-bitowej wartości bez znaku za pomocą (unsigned)data[0] | ((unsigned)data[1] << 8)(dla małej wersji Endian) kompiluje się do pojedynczej instrukcji i daje 16-bitową wartość bez znaku.
  3. Zgodnie ze standardem C 6.3.1.3 Liczba całkowita ze znakiem i bez znaku : konwersja wartości typu uint16_tna typ podpisany int16_tma zachowanie zdefiniowane w ramach implementacji, jeśli wartość nie mieści się w zakresie typu docelowego. Nie przewiduje się specjalnych przepisów dla typów, których reprezentacja jest dokładnie zdefiniowana.
  4. aby uniknąć tego zachowania zdefiniowanego w implementacji, można przetestować, czy wartość bez znaku jest większa niż, INT_MAXi obliczyć odpowiednią wartość ze znakiem odejmując 0x10000. Wykonanie tej czynności dla wszystkich wartości sugerowanych przez zwolnienie może wygenerować wartości poza zakresem o int16_ttym samym zachowaniu zdefiniowanym dla implementacji.
  5. testowanie dla 0x8000 bitu wyraźnie powoduje, że kompilatory wytwarzają nieefektywny kod.
  6. bardziej wydajna konwersja bez zdefiniowanego zachowania implementacyjnego wykorzystuje wykrywanie czcionek za pomocą unii, ale debata dotycząca zdefiniowania tego podejścia jest nadal otwarta, nawet na poziomie komitetu C Standard.
  7. kasowanie typu może być wykonywane przenośnie i ze zdefiniowanym zachowaniem za pomocą memcpy.

Łącząc punkty 2 i 7, oto przenośne iw pełni zdefiniowane rozwiązanie, które skutecznie kompiluje się do pojedynczej instrukcji z gcc i 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;
}

Montaż 64-bitowy :

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
źródło
Nie jestem prawnikiem językowym, ale tylko chartypy mogą alias lub zawierać reprezentację obiektową dowolnego innego typu. uint16_tnie jest jednym z chartypów, dzięki czemu memcpyod uint16_tcelu int16_tnie jest dobrze określone zachowanie. Norma wymaga jedynie char[sizeof(T)] -> T > char[sizeof(T)]konwersji, memcpyaby była dobrze zdefiniowana.
Maxim Egorushkin
memcpyof uint16_tto int16_tjest w najlepszym wypadku zdefiniowane w implementacji, nieprzenośne, nie jest dokładnie zdefiniowane, dokładnie tak jak przypisanie jednego do drugiego, i nie można tego magicznie obejść memcpy. Nie ma znaczenia, czy uint16_tużywa reprezentacji uzupełnienia do dwóch, czy też nie są obecne bity dopełniające - nie jest to zachowanie zdefiniowane lub wymagane przez standard C.
Maxim Egorushkin
Przy tak wielu słowy, „rozwiązanie” sprowadza się do zastąpienia r = u, aby memcpy(&r, &u, sizeof u)jednak ten ostatni nie jest lepszy niż pierwszy, prawda?
Maxim Egorushkin