Який правильний спосіб перетворити 2 байти в підписане 16-бітове ціле число?


31

У цій відповіді , zwol зробив цю заяву:

Правильний спосіб перетворення двох байтів даних із зовнішнього джерела в 16-бітове ціле число - це допоміжні функції:

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

Яка з перерахованих вище функцій є підходящою, залежить від того, чи містить масив невелике ендіанське або велике ендіанське подання. Порядок байтів не є проблемою на питання тут, я задаюся питанням, чому zwol віднімає 0x10000uз uint32_tзначення перетворюються в int32_t.

Чому це правильний шлях ?

Як уникнути визначеної реалізацією поведінки при переході до типу повернення?

Оскільки ви можете припустити подання 2-го доповнення, яким чином цей простіший показ не зможе: return (uint16_t)val;

Що не так у цьому наївному рішенні:

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

Точна поведінка під час кастингу int16_tвизначається реалізацією, тому наївний підхід не є переносним.
nwellnhof

@nwellnhof немає акторівint16_t
MM

На питання в заголовку не можна відповісти, не вказавши, яке відображення використовувати
MM

4
Обидва підходи покладаються на визначене реалізацією поведінку (перетворення неподписаного значення в підписаний тип, який не може представляти значення). Напр. у першому підході 0xFFFF0001uне можуть бути представлені як int16_t, а у другому підході 0xFFFFuне можуть бути представлені як int16_t.
Сандер Де Дікер

1
"Оскільки ви можете припустити представлення додатка 2" [потрібне цитування]. C89 та C99, безумовно, не заперечували подання доповнення та значення знаків 1s. Qv, stackoverflow.com/questions/12276957/…
Ерік Тауерс

Відповіді:


20

Якщо int16-розрядна, то ваша версія покладається на визначену реалізацією поведінку, якщо значення виразу в returnоператорі знаходиться поза діапазоном для int16_t.

Однак перша версія також має подібну проблему; наприклад, якщо int32_tє typedef для int, і байти вхідного сигналу є обома 0xFF, то результат віднімання у операторі return повертає те, UINT_MAXщо викликає поведінку, визначену реалізацією при перетворенні в int16_t.

ІМХО відповідь, на яку ви посилаєтесь, має кілька основних питань.


2
Але який правильний шлях?
idmean

@idmean на запитання потрібне уточнення, перш ніж на нього можна відповісти, я просив коментар під цим питанням, але ОП не відповів
MM

1
@MM: Я відредагував запитання, уточнюйте, що ендіантність не є проблемою. IMHO, яку намагається вирішити zwol, - це поведінка, визначена реалізацією при переході на тип призначення, але я згоден з вами: я вважаю, що він помиляється, оскільки у його методу є інші проблеми. Як би ви ефективно вирішили визначену поведінкою поведінку?
chqrlie

@chqrlieforyellowblockquotes Я конкретно не мав на увазі підступність. Ви просто хочете вставити точні біти двох вхідних октетів у int16_t?
ММ

@MM: так, саме в цьому питання. Я писав байти, але правильне слово справді має бути октетом, як тип uchar8_t.
chqrlie

7

Це повинно бути педантично правильним і працювати також на платформах, які використовують бітові знаки або представлення комплементу 1 , замість звичайного додатка 2 . Вхідні байти вважаються такими, що доповнюють 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;
}

Через галузь це буде дорожче інших варіантів.

Це досягає того, що це дозволяє уникнути будь-якого припущення про те, як intпредставлення стосується unsignedпредставлення на платформі. У ролях для intподачі потрібно зберегти арифметичне значення для будь-якого числа, яке буде відповідати цільовому типу. Оскільки інверсія гарантує, що верхній біт 16-бітного числа буде нульовим, значення буде відповідати. Тоді унар -і віднімання 1 застосовують звичайне правило для заперечення комплементу 2. Залежно від платформи, вона INT16_MINвсе ще може переповнюватись, якщо вона не вписується у intтип цілі, у такому випадку її longслід використовувати.

Відмінність від оригінальної версії у питанні виникає у момент повернення. Незважаючи на те, що оригінал завжди віднімається, 0x10000а доповнення 2 дозволяють підписати переповнення, щоб завершити його int16_t, але ця версія має явне, ifщо дозволяє уникнути підписання підпису (який не визначений ).

Зараз на практиці майже всі використовувані сьогодні платформи використовують 2 додаткове представлення. Насправді, якщо платформа має стандартну сумісність, stdint.hяка визначає int32_t, вона повинна використовувати для неї 2 доповнення. Там, де цей підхід іноді стане у нагоді, є деякі мови сценаріїв, які взагалі не мають цілих типів даних - ви можете змінювати операції, показані вище для плавців, і це дасть правильний результат.


Стандарт C спеціально передбачає, що int16_tі будь-який, intxx_tі їхні непідписані варіанти повинні використовувати представлення комплементу 2 без бітів. Для розміщення цих типів та використання іншого представлення потрібна цілеспрямована перекручена архітектура int, але я думаю, DS9K може бути налаштований таким чином.
chqrlie

@chqrlieforyellowblockquotes Добре, я змінив використання, intщоб уникнути плутанини. Дійсно, якщо платформа визначає, int32_tвона повинна бути доповненням 2.
jpa

Ці типи були стандартизовані таким чином у C99 таким чином: C99 7.18.1.1 Цілі типи точної ширини Ім'я typedef intN_t позначає підписаний цілочисельний тип із шириною N, відсутністю бітів прокладки та поданням доповнення двох. Таким чином, int8_tпозначає підписаний цілочисельний тип шириною рівно 8 біт. Інші представлення все ще підтримуються стандартом, але для інших цілих типів.
chqrlie

У вашій оновленій версії (int)valueвизначено поведінку, якщо тип intмає лише 16 біт. Я боюся, що вам потрібно використовувати (long)value - 0x10000, але в архітектурах доповнення, що 0x8000 - 0x10000не стосуються 2, значення не може бути представлене як 16-бітове int, тому проблема залишається.
chqrlie

@chqrlieforyellowblockquotes Так, я помітив те саме, я поправився з ~ натомість, але longпрацював би однаково добре.
jpa

6

Інший метод - використання union:

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

У програмі:

...
B2I16 conv;

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

first_byteі second_byteможе бути замінено відповідно до малої чи великої ендіанської моделі. Цей спосіб не кращий, але є однією з альтернатив.


2
Чи не тип союзу карає невказану поведінку ?
Максим Єгорушкін

1
@MaximEgorushkin: Вікіпедія не є авторитетним джерелом інтерпретації стандарту C.
Eric Postpischil

2
@EricPostpischil Орієнтуватися на месенджер, а не на повідомлення, нерозумно.
Максим Єгорушкін

1
@MaximEgorushkin: о так, ой, я неправильно прочитав ваш коментар. Припускаючи , byte[2]і int16_tмають той же розмір, що один або інший з двох можливих порядків, а не якісь - то довільні перемішуються бітові значень місця. Таким чином, ви можете принаймні виявити під час компіляції, яку небезпеку має реалізація.
Пітер Кордес

1
У стандарті чітко зазначено, що значення члена об'єднання є результатом інтерпретації збережених бітів члена як представлення значення цього типу. Існують аспекти, визначені реалізацією, оскільки представлення типів визначається реалізацією.
ММ

6

Арифметичні оператори зміщуються і порозрядно - або в виразі (uint16_t)data[0] | ((uint16_t)data[1] << 8)не працюють на типи, менші int, так що ці uint16_tзначення отримують int(або unsignedякщо sizeof(uint16_t) == sizeof(int)). І все-таки це має дати правильну відповідь, оскільки лише нижні 2 байти містять значення.

Інша педантично правильна версія для конвертації великих ендіан в малих ендіан (якщо припустити процесор мало ендіанських процесорів):

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

memcpyвикористовується для копіювання представництва, int16_tі це стандартний спосіб. Ця версія також складається в 1 інструкцію movbe, див. Складання .


1
@MM Одна з причин __builtin_bswap16є, оскільки заміна байтів в ISO C не може бути реалізована настільки ефективно.
Максим Єгорушкін

1
Неправда; компілятор міг би виявити, що код реалізує заміну байтів і переводить його як ефективний вбудований
MM

1
Конвертація int16_tв uint16_tчітко визначена: негативні значення перетворюються на значення, більші за INT_MAX, але перетворення цих значень назад у uint16_tце визначено реалізацією поведінки: 6.3.1.3 Підписані та непідписані цілі числа 1. Коли значення з цілим типом перетворюється на інший цілий тип, відмінний від_Bool, якщо значення може бути представлене новим типом, воно не змінюється. ... 3. В іншому випадку новий тип підписується і значення не може бути представлене в ньому; або результат визначений реалізацією, або підвищений сигнал, визначений реалізацією.
chqrlie

1
@MaximEgorushkin gcc, здається, не так добре в 16-бітній версії, але clang генерує той самий код для ntohs/ __builtin_bswapта |/ <<pattern: gcc.godbolt.org/z/rJ-j87
PSkocik

3
@MM: Я думаю, що Максим говорить "не можна на практиці з поточними компіляторами". Звичайно, компілятор не міг засвоїти один раз і розпізнати завантаження суміжних байтів у ціле число. GCC7 або 8 нарешті знову ввели коалесценцію завантаження / зберігання для випадків, коли реверс байтів не потрібен, після того, як GCC3 скинув його десятиліття тому. Але в цілому компілятори, як правило, потребують допомоги на практиці з багатьма речами, які процесори можуть зробити ефективно, але які ISO C знехтували / відмовлялися переносити портативно. Портативний ISO C не є гарною мовою для ефективної маніпуляції кодом біт / байт.
Пітер Кордес

4

Ось ще одна версія, яка покладається лише на портативну та чітко визначену поведінку (заголовок #include <endian.h>не стандартний, код є):

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

Версія з малим ендіаном компілюється в одну movbeінструкцію clang, gccверсія менш оптимальна, див. Складання .


@chqrlieforyellowblockquotes Здається, ваша головна проблема полягала uint16_tв int16_tперетворенні, у цій версії немає такої конверсії, тому ось ви йдете.
Максим Єгорушкін

2

Я хочу подякувати всім учасникам за їх відповіді. Ось що зводиться до колективних творів:

  1. Відповідно до C Стандарт 7.20.1.1 Точної шириною цілих типів : типів uint8_t, int16_tі uint16_tповинен використовувати комплемент уявлення двійкового без яких - або біт заповнення, так що фактичні біти уявлення однозначно є ті , з 2 -х байт в масиві, в порядку , визначеному назви функцій.
  2. обчислення непідписаного 16-бітного значення (unsigned)data[0] | ((unsigned)data[1] << 8)(для маленької версії ендіан) компілюється в одну інструкцію і дає неподписане 16-бітове значення.
  3. Відповідно до стандарту C 6.3.1.3 Цілі числа, що підписані та непідписані : перетворення значення типу uint16_tу підписаний тип int16_tмає поведінку, визначене реалізацією, якщо значення не знаходиться в діапазоні типу призначення. Особливих положень не передбачено для типів, представлення яких точно визначено.
  4. щоб уникнути цієї визначеної поведінки поведінки, можна перевірити, чи не підписане значення більше, INT_MAXі вирахувати відповідне підписане значення шляхом віднімання 0x10000. Виконання цього для всіх значень, запропонованих zwol, може створювати значення поза діапазоном int16_tз однаковою поведінкою, визначеною реалізацією.
  5. тестування на 0x8000біт явно змушує компілятори виробляти неефективний код.
  6. більш ефективна конверсія без визначеної поведінки поведінки використовує тип покарання через союз, але дебати щодо визначеності цього підходу залишаються відкритими навіть на рівні комітету стандарту C.
  7. типове покарання може виконуватися портативно та з визначеною поведінкою за допомогою memcpy.

Поєднуючи пункти 2 і 7, ось портативне і повністю визначене рішення, яке ефективно збирає одну інструкцію з gcc і 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;
}

64-розрядна збірка :

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

Я не мовний юрист, але лише charтипи можуть створювати псевдоніми або містити об'єктне представлення будь-якого іншого типу. uint16_tне є одним із charтипів, тому memcpyповедінка uint16_tдо int16_tне є чітко визначеною поведінкою. Стандарт вимагає лише char[sizeof(T)] -> T > char[sizeof(T)]конверсії, memcpyщоб бути чітко визначеною.
Максим Єгорушкін

memcpyof uint16_tto int16_t- це в кращому випадку визначено реалізацією, не переносною, не чітко визначеною, точно як присвоєння одного іншому, і ви не можете магічно обійти це зmemcpy . Не важливо, чи uint16_tвикористовується представлення комплементу двох чи ні, чи присутні біти підкладки чи ні - це не поведінка, визначена або не вимагається стандартом C.
Максим Єгорушкін

З такою великою кількістю слів, ваше «рішення» зводиться до заміни r = uна memcpy(&r, &u, sizeof u)але останній не краще , ніж перший, це?
Максим Єгорушкін
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.