Как правильно преобразовать 2 байта в 16-разрядное целое число со знаком? - PullRequest
31 голосов
/ 26 марта 2020

В этот ответ , 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.

Почему это правильно way ?

Как избежать поведения, определяемого реализацией, при преобразовании в тип возвращаемого значения?

Так как вы можете предположить представление дополнения до 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);
}

Ответы [ 6 ]

20 голосов
/ 26 марта 2020

Если int является 16-разрядным, то ваша версия полагается на поведение, определяемое реализацией, если значение выражения в операторе return выходит за пределы диапазона int16_t.

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

ИМХО ответ, на который вы ссылаетесь, имеет несколько основных проблем.

7 голосов
/ 27 марта 2020

Это должно быть педантично правильным и работать также на платформах, которые используют знак бита или 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 требуется для сохранения арифметического значения c для любого числа, которое будет соответствовать целевому типу. Поскольку инверсия гарантирует, что старший бит 16-разрядного числа будет равен нулю, значение будет соответствовать. Тогда унарный - и вычитание 1 применяют обычное правило для отрицания дополнения 2. В зависимости от платформы, INT16_MIN может по-прежнему переполняться, если не соответствует типу int на цели, и в этом случае следует использовать long.

Разница с исходной версией в вопрос приходит во время возврата. В то время как оригинал всегда вычитал 0x10000 и дополнение 2, позволяющее переполнению со знаком, обернуть его в диапазон int16_t, эта версия имеет явный if, который позволяет избежать перетаскивания со знаком (то есть undefined ).

Сейчас на практике почти все платформы, используемые сегодня, используют представление дополнения 2. Фактически, если у платформы есть стандартное соответствие stdint.h, которое определяет int32_t, она должна использовать дополнение 2 для этого. Иногда этот подход оказывается полезным при использовании некоторых языков сценариев, которые вообще не имеют целочисленных типов данных - вы можете изменить операции, показанные выше для чисел с плавающей запятой, и это даст правильный результат.

6 голосов
/ 26 марта 2020

Другой метод - использование 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 можно поменять местами в соответствии с младшей или большой порядковой моделью. Этот метод не лучше, но является одной из альтернатив.

6 голосов
/ 26 марта 2020

Арифметические c операторы shift и поразрядно или в выражении (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, см. сборка .

4 голосов
/ 30 марта 2020

Вот еще одна версия, которая опирается только на переносимые и четко определенные варианты поведения (заголовок #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 версия менее оптимальна, см. сборка .

2 голосов
/ 30 марта 2020

Я хочу поблагодарить всех участников за их ответы. Вот что сводит коллективная работа:

  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, вот переносное и полностью определенное решение, которое эффективно компилируется в одну инструкцию с g cc и 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
...