Как правильно положить в буфер число размером более чем 1 байт?

210
27 апреля 2018, 14:22

Работаю программистом. Мои программы успешно работают. Но иногда на меня находит, и я пытаюсь что-нибудь в них переделать "по уму".

Сегодня рассмотрим очень простую задачу. У нас есть массив байтов (допустим, пакет, который мы хотим отправить через сеть) и нам надо в определённое место этого массива положить 32-битное число. Используем порядок байтов little endian (т.е. "обычный").

Напрашиваются два варианта решения:

Первый вариант:

inline void put_uint32_le(unsigned char *dst, uint32_t n)
{
    *reinterpret_cast<uint32_t *> (dst) = n;
}

Второй вариант:

inline void put_uint32_le(unsigned char *dst, uint32_t n)
{
    dst[0] = static_cast<unsigned char> (n & 255);
    dst[1] = static_cast<unsigned char> ((n >> 8) & 255);
    dst[2] = static_cast<unsigned char> ((n >> 16) & 255);
    dst[3] = static_cast<unsigned char> (n >> 24);
}

Второй вариант более правильный в смысле портируемости и соблюдения стандартов. Проблема в том, что он неэффективный. Говорят, что компилятор умный, что он заметит, что все четыре строчки можно выполнить одной машинной командой, и соптимизирует. Однако, эксперименты показывают, что, например, GCC даже с оптимизацией -O3 по факту этого не делает. Замеры времени показывают, что второй вариант исполняется где-то в полтора раза медленнее, чем первый.

Лично мне неприятно так наплевательски относиться к быстродействию функции, которая у меня будет выполняться тысячу раз в секунду.

Первый вариант работает быстрее, но он работает только если у нас машина с little endian byte order. Но это ладно. Можно сделать так:

inline void put_uint32_le(unsigned char *dst, uint32_t n)
{
#ifdef WE_HAVE_LITTLE_ENDIAN_PROCESSOR
    *reinterpret_cast<uint32_t *> (dst) = n;
#else
    dst[0] = static_cast<unsigned char> (n & 255);
    dst[1] = static_cast<unsigned char> ((n >> 8) & 255);
    dst[2] = static_cast<unsigned char> ((n >> 16) & 255);
    dst[3] = static_cast<unsigned char> (n >> 24);
#endif
}

Хуже другое. Указатель 'dst' может быть невыровненным. В стандарте написано, что выравнивание это "a number of bytes between successive addresses at which a given object can be allocated". Во всём стандарте выравнивание рассматривается именно в таком ключе - ни одно место в стандарте не говорит о том, что к многобайтному числу можно обратиться по невыровненному указателю.

Более того, я и в документации на компилятор ("man gcc", "info gcc") не нахожу, где бы было написано, что компилятор это поддерживает хотя-бы на intel-архитектуре, хотя-бы в качестве расширения стандарта! Хоть-бы #ifdef'ы какие-нибудь сделать, но как именно их сделать, не могу понять!

То есть, то, что я делаю в первом варианте, приводит к undefined behavior. Да, я так уже делал в сотнях программ, и на intel-совместимых процессорах всегда работало. Да, так делаю не только я один. Но никто не обещал, что в следующей версии компилятора это не заглючит, поэтому так делать нельзя.

Более того, если прищуриться, становится видно, что и второй вариант не абсолютно портируемый. Во-первых, у нас может не быть типа uint32_t, поэтому вместо него надо использовать uint_fast32_t. Во-вторых, у нас, теоретически, unsigned char может состоять более чем из восьми битов. Если первая проблема легко решается, то со второй ума не приложу что делать.

Кто что посоветует?

Answer 1

Есди в первом варианте вы замените присвоение , обединением то вы избежите неопределености с выравниванием

union
{
 uint32_t   v;
 uint8_t    c[4];
}value;

Но если у вас есть четко оговореный протокол передачи , то нужно ставить каждый байт на свое место, а для этого преобразовывать между littel-endian и big-endian

Answer 2

VTT:

Что-то вы напридумывали про длинный ассемблер и укладку в стек...

А если подбавить реализьму?

Попробуйте с нижеследующим кодом. Только обязательно укажите версию GCC не позднее 6.3.

#include <string.h>
#include <stdint.h>
inline uint16_t get_uint16_le(const unsigned char *src)
{
    return *reinterpret_cast<const uint16_t *> (src);
}
inline void put_uint32_le(unsigned char *dst, uint_fast32_t n)
{
    memcpy(dst, &n, 4);
//    *reinterpret_cast<uint32_t *> (dst) = n;
}
extern const uint32_t crc32_table[256];
static uint32_t calculate_crc32(const unsigned char *buf, size_t len)
{
    uint32_t crc = 0;
    while (len--)
    {
        crc ^= crc32_table[*(buf++)];
    }
    return crc;
}
void set_crc32(unsigned char *begin)
{
    size_t sz = 3 + get_uint16_le(begin);
    put_uint32_le(begin + sz, calculate_crc32(begin, sz));
}
READ ALSO
Зависание одной формы во время работы другой

Зависание одной формы во время работы другой

Есть две формы: Form1 и Form2Они взаимодействую друг с другом, и при выполнении определенных действий во второй форме запускается цикл, идущий...

199
Объясните работу программы

Объясните работу программы

Решение задачи о рюкзакеУсловия: Одной из классических NP-полных задач является так называемая «Задача о рюкзаке»

198
Можно ли в QNetworkRequest задать кодировку?

Можно ли в QNetworkRequest задать кодировку?

Можно ли в QNetworkRequest задать кодировку? А то я загружаю с помощью него сайт а там вместо русских букв кракозяблы

226