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 0x10000u
od uint32_t
wartoś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);
}
źródło
int16_t
jest zdefiniowane w implementacji, więc naiwne podejście nie jest przenośne.int16_t
0xFFFF0001u
nie można przedstawić jakoint16_t
, aw drugim podejściu0xFFFFu
nie można przedstawić jakoint16_t
.Odpowiedzi:
Jeśli
int
jest 16-bitowy, wówczas twoja wersja opiera się na zachowaniu zdefiniowanym przez implementację, jeśli wartość wyrażenia wreturn
instrukcji jest poza zakresemint16_t
.Jednak pierwsza wersja ma podobny problem; na przykład jeśli
int32_t
jest typedef dlaint
, a bajty wejściowe są oba0xFF
, to wynikiem odejmowania w instrukcji return jest to,UINT_MAX
co powoduje zachowanie zdefiniowane w implementacji po przekonwertowaniu naint16_t
.IMHO odpowiedź, do której linkujesz, ma kilka poważnych problemów.
źródło
int16_t
?uchar8_t
.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.
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ązaniaunsigned
reprezentacji z reprezentacją na platformie. Rzutowanieint
jest 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 platformyINT16_MIN
może nadal przepełniać się, jeśli nie pasuje doint
typu na celu, w którym to przypadkulong
należy użyć.Różnica w stosunku do oryginalnej wersji w pytaniu pochodzi z czasu powrotu. Podczas gdy oryginał po prostu zawsze odejmował,
0x10000
a dopełnienie 2 pozwalało podpisanemu przepełnieniu zawinąć go doint16_t
zakresu, 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 definiujeint32_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.źródło
int16_t
wszystkie dowolneintxx_t
i ich niepodpisane warianty musiały używać reprezentacji uzupełnienia 2 bez bitów dopełniających.int
Hostowanie tego typu i używanie innej reprezentacji wymagałoby celowo przewrotnej architektury , ale myślę, że DS9K można skonfigurować w ten sposób.int
aby uniknąć zamieszania. Rzeczywiście, jeśli platforma określaint32_t
, musi być uzupełnieniem 2.intN_t
oznacza podpisany typ liczb całkowitych o szerokościN
, bez bitów wypełniających i reprezentacji uzupełnienia do dwóch. Oznacza zatemint8_t
typ 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.(int)value
ma określone zachowanie implementacji, jeśli typint
ma tylko 16 bitów. Obawiam się, że musisz użyć(long)value - 0x10000
, ale w architekturach komplementarnych innych niż 2 wartość0x8000 - 0x10000
nie może być reprezentowana jako 16-bitowaint
, więc problem pozostaje.long
działałoby równie dobrze.Inna metoda - użycie
union
:W programie:
first_byte
isecond_byte
mogą być zamieniane według małego lub dużego modelu endian. Ta metoda nie jest lepsza, ale jest jedną z alternatyw.źródło
byte[2]
iint16_t
są 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.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 teuint16_t
wartości są promowane doint
(lubunsigned
jeślisizeof(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:
memcpy
służy do skopiowania reprezentacjiint16_t
i jest to zgodny ze standardami sposób. Ta wersja kompiluje się również w 1 instrukcjimovbe
, patrz instrukcja montażu .źródło
__builtin_bswap16
jest to, że zamiana bajtów w ISO C nie może być wdrożona tak skutecznie.int16_t
douint16_t
jest dobrze zdefiniowana: wartości ujemne są konwertowane na wartości większe niżINT_MAX
, ale konwersja tych wartości z powrotemuint16_t
jest 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.ntohs
/__builtin_bswap
i|
/<<
pattern: gcc.godbolt.org/z/rJ-j87Oto 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):Wersja Little-Endian kompiluje się do pojedynczej
movbe
instrukcjiclang
,gcc
wersja jest mniej optymalna, patrz zestawienie .źródło
uint16_t
doint16_t
konwersji, ta wersja nie posiada tej konwersji, więc tutaj jesteś.Chcę podziękować wszystkim autorom za ich odpowiedzi. Oto, co sprowadza się do pracy zbiorowej:
uint8_t
,int16_t
iuint16_t
muszą 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.(unsigned)data[0] | ((unsigned)data[1] << 8)
(dla małej wersji Endian) kompiluje się do pojedynczej instrukcji i daje 16-bitową wartość bez znaku.uint16_t
na typ podpisanyint16_t
ma 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.INT_MAX
i obliczyć odpowiednią wartość ze znakiem odejmując0x10000
. Wykonanie tej czynności dla wszystkich wartości sugerowanych przez zwolnienie może wygenerować wartości poza zakresem oint16_t
tym samym zachowaniu zdefiniowanym dla implementacji.0x8000
bitu wyraźnie powoduje, że kompilatory wytwarzają nieefektywny kod.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 :
Montaż 64-bitowy :
źródło
char
typy mogą alias lub zawierać reprezentację obiektową dowolnego innego typu.uint16_t
nie jest jednym zchar
typów, dzięki czemumemcpy
oduint16_t
celuint16_t
nie jest dobrze określone zachowanie. Norma wymaga jedyniechar[sizeof(T)] -> T > char[sizeof(T)]
konwersji,memcpy
aby była dobrze zdefiniowana.memcpy
ofuint16_t
toint16_t
jest 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, czyuint16_t
uż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.r = u
, abymemcpy(&r, &u, sizeof u)
jednak ten ostatni nie jest lepszy niż pierwszy, prawda?