LINUX.ORG.RU

Как быстро сконвертировать uint32_t в uint8_t

 , ,


1

4

У меня есть указатель на uint8_t. Я знаю, что он выровнен по адресу 4. Я хочу быстро читать и писать туда uint32_t значение.

Правильный по стандарту вариант использовать memcpy, но он очень медленный.

Быстрый вариант - кастовать uint8_t * в uint32_t * и полагаться на то, что он выровнен. Это генерирует одну инструкцию, но это UB по стандарту, хотя по факту работать будет…

Как тут можно поступить? Меня интересует конкретный компилятор gcc 9.2. Может быть gcc даёт какие-то дополнительные гарантии сверх стандарта для данного случая?

Код:

#include <stdint.h>
#include <string.h>

void save1(uint8_t *p, uint32_t v) {
    memcpy(p, &v, 4);
}

void save2(uint8_t *p, uint32_t v) {
    uint32_t *p32 = (uint32_t *)p;
    *p = v;
}

uint32_t load1(uint8_t *p) {
    uint32_t v;
    memcpy(&v, p, 4);
    return v;
}

uint32_t load2(uint8_t *p) {
    uint32_t *p32 = (uint32_t *)p;
    return *p32;
}

Во что он компилируется с -Os:

save1:
        push    {r0, r1, r2, lr}
        mov     r2, #4
        str     r1, [sp, #4]
        add     r1, sp, r2
        bl      memcpy
        add     sp, sp, #12
        ldr     lr, [sp], #4
        bx      lr
save2:
        strb    r1, [r0]
        bx      lr
load1:
        push    {r0, r1, r2, lr}
        mov     r2, #4
        mov     r1, r0
        add     r0, sp, r2
        bl      memcpy
        ldr     r0, [sp, #4]
        add     sp, sp, #12
        ldr     lr, [sp], #4
        bx      lr
load2:
        ldr     r0, [r0]
        bx      lr

godbolt

★★★★★

Быстрый вариант - кастовать uint8_t * в uint32_t * и полагаться на то, что он выровнен.

сам же сказал, что он выровнен.

У меня есть указатель на uint8_t. Я знаю, что он выровнен по адресу 4.

просто кастуешь к uint32* и усьо. ну или борешься с раздвоением личности. когда одна половина говорит, что выровнен, а другая сомневается в этом.

alysnix ★★★
()

Быстрый вариант - кастовать uint8_t * в uint32_t * и полагаться на то, что он выровнен.

если выровнен то не уб

Я знаю, что он выровнен по адресу 4

откуда? для гарантии alignas ещё можно использовать

zurg
()
Ответ на: комментарий от zurg

если выровнен то не уб

Нет, это по-любому UB, нарушение Strict Aliasing Rule и вообще в стандарте про конкретное выравнивание ничего не сказано.

Я знаю, что он выровнен по адресу 4

откуда? для гарантии alignas ещё можно использовать

Потому, что это указатель на кусок в статическом буфере, который как раз и выровнен через alignas. И смещение кодом гарантируется кратным четырём.

Условно говоря

alignas(4) static uint8_t buffer[1024];
size_t offset = x * 4 % 1024;
store1(buffer + offset, value1);
value2 = load(buffer + offset);
vbr ★★★★★
() автор топика
Ответ на: комментарий от vbr

Проблема в том, что это считается Undefined Behaviour и это плохо.

да лана. кастом можно void* преобразовывать в какой угодно поинтер. и наоборот без каста

логически строя цепочку uint8* -> void* -> uint32* получаешь ответ. что можно и это не обязатно ub. выровнен он должен быть на правильную границу.

alysnix ★★★
()

У меня есть указатель на uint8_t. Я знаю, что он выровнен по адресу 4. Я хочу быстро читать и писать туда uint32_t значение.

То что три байта после uint8_t перезапишутся вас устраивает? Поддержка big endian не нужна?

X512 ★★★★★
()
Ответ на: комментарий от X512

То что три байта после uint8_t перезапишутся вас устраивает?

Мне это нужно.

Поддержка big endian не нужна?

Не нужна. Мне вообще без разницы, какой там endian, лишь бы один и тот же, это читается и пишется в одной программе.

vbr ★★★★★
() автор топика
Ответ на: комментарий от X512

То что три байта после uint8_t перезапишутся вас устраивает?

он харкорно кастит на другой тип и, или гарантирует, что там с памятью все нормально - тогда все будет пучком. или там все ненормально, и будет бага. а может и «спец эффект». это его выбор.

alysnix ★★★
()
Ответ на: комментарий от vbr

That is, a pointer of type char* is permitted to alias any other pointer

Все так. Но, вообще говоря, ты можешь просто выключить это через -fno-strict-aliasing и спокойно приводить uint8_t * к нужным структурам без опасений получить странное.

gaylord
()
Ответ на: комментарий от gaylord

Да, я уже обнаружил, что я и так компилирую проект с -fno-strict-aliasing (т.к. используемая библиотека собирается с этим флагом) и в целом, видимо, зря переживаю.

vbr ★★★★★
() автор топика

Я знаю, что он выровнен

#define save(x,y) *((uint32_t *)x) = y;

#define load(x,y) y = *((uint32_t *)x);

Быстрее наверное некуда. Если в цикле, то 1 раз за ранее привести и всё. Чище будет. Я так и не понял проблемы.

LINUX-ORG-RU ★★★★★
()
Последнее исправление: LINUX-ORG-RU (всего исправлений: 1)
Ответ на: комментарий от vbr

полезный флаг-то. может поймать какие-то баги. из-за какой-то ерунды, его отключать???

всякие игры с флагами(их отключением) есть свидетельство скорей кривого кода, или мутного легаси, чем рекомендуемый образ жизни.

alysnix ★★★
()
Ответ на: комментарий от gaylord

у ТС ни один из перечисленных случаев. ТС ГАРАНТИРУЕТ правильность преобразования. и у него указатель на скалярный тип.

отключение же варнинга еще хуже. мы просто ничего нигде не проверяем. даже там где надо проверять

alysnix ★★★
()
Ответ на: комментарий от gaylord

Ну формально конечно никто тебя не заставляет его ставить. Но компиляция с -O2 и без -fno-strict-aliasing это либо авторы ещё не знают про эту подлянку от авторов gcc, либо они не совсем разумные и думают что ничего страшного, либо они фанаты комитета (что тоже ведёт в предыдущему пункту). И для того чтобы отключать алиасинг - не надо перед этим сталкиваться с багами, которые он создаёт (ты их можешь и не заметить сходу), его надо отключать превентивно одновременно с прописыванием -O2 вне зависимости от того, что ты компилируешь. Для настройки алиасинга есть restrict.

firkax ★★★★★
()
Ответ на: комментарий от alysnix

полезный флаг-то. может поймать какие-то баги. из-за какой-то ерунды, его отключать???

-fstrict-aliasing не ловит никакие баги, он их создаёт и не всегда сразу заметные. То, что его сделали дефолтно-включённым с -O2 - это диверсия.

firkax ★★★★★
()
Ответ на: комментарий от alysnix

Если стрикт-алиасинг включён - то как ни кастуй, всё может сломаться. При алиасинге компилятор проверяет только тип данных, а не историю его кастов. Какое-то мутное исключение там есть только для union, потому что не могли ж они стандартную фичу языка целиком объявить UB (а наверно хотели) - пришлось таки делать поддержку.

firkax ★★★★★
()
Ответ на: комментарий от firkax

То, что его сделали дефолтно-включённым с -O2 - это диверсия.

северокорейские диверсанты штоле?

включать надо все возможные варнинги. ибо делали их не просто так. а запрещать варнинги, чтобы не сыпало - верный путь к социальной безответственности.

alysnix ★★★
()

Добавлю ещё один вариант, написать функции-обёртки на ассемблере. Ассемблер уже оптимизациям никаким не подлежит (я надеюсь, лол), но в то же время компилятор эти функции прекрасно инлайнит. Вроде должно работать в любой ситуации…

static void uint32_encode_aligned(uint8_t *location, uint32_t value) {
    __asm__ volatile (
        "str %1, [%0]"
        :
        : "r" (location), "r" (value)
        : "memory"
    );
}

static uint32_t uint32_decode_aligned(uint8_t *location) {
    uint32_t result;
    __asm__ volatile (
        "ldr %0, [%1]"
        : "=r" (result)
        : "r" (location)
        : "memory"
    );
    return result;
}

godbolt

vbr ★★★★★
() автор топика
Ответ на: комментарий от firkax

То, что его сделали дефолтно-включённым с -O2 - это диверсия.

Хуже того, он дефолтно-включён даже с -O0. Это вообще часть языка:



    An object shall have its stored value accessed only by an lvalue that has one of the following types:

        the declared type of the object,

        a qualified version of the declared type of the object,

        a type that is the signed or unsigned type corresponding to the
        declared type of the object,

        a type that is the signed or unsigned type corresponding to a
        qualified version of the declared type of the object,

        an aggregate or union type that includes one of the aforementioned
        types among its members (including, recursively, a member of a
        subaggregate or contained union), or

        a character type.

Цитата прямо из C89, т.е. в Си это было буквально вообще всегда. Просто сишные говнокодеры не могут не перекастовать через пятнадцать указателей и три залупы всё подряд, поэтому приходится это выключать.

Другой вопрос, что uint8_t – алиас на unsigned char, поэтому strict aliasing здесь вполне удовлетворён. ТС, у тебя нет UB, расслабь булки.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)
Ответ на: комментарий от alysnix

включать надо все возможные варнинги. ибо делали их не просто так. а запрещать варнинги, чтобы не сыпало - верный путь к социальной безответственности.

Ты зря вот это начал. У @firkax аллергия на -Wall, этот флаг ему код ломает.

hateyoufeel ★★★★★
()
Ответ на: комментарий от vbr

Анонимный union.

Что означает эта фраза?

Что надо, наконец, прочитать букварь по си.

#include <stdint.h>
#include <stdio.h>

enum { BUFFER_SIZE = 1024 };

union
{
    char buf[BUFFER_SIZE];
    uint32_t data[BUFFER_SIZE / sizeof(uint32_t)];
} u = {0};


int main(void)
{
    char *p = u.buf;
    uint32_t *q = u.data;

    *q = 0x31323334;

    printf("Char: %c\n", *p);
    return 0;
}

LamerOk ★★★★★
()

Возможно, я не совсем понял задачу в 3 часа ночи, но сонный обезьянний мозг выдаёт что-то типа:

uint8_t data[4];

uint32_t val;

uint8_t *convert_32_to_8(uint32_t data, uint8_t flag);

void test() {
    val = 0x12345678;

    for (uint8_t i=0; i<4; i++)
    {
      data[i] =
(*convert_32_to_8(val, i)); 
    }

    // остальная логика работы с data 
} 

uint8_t *convert_32_to_8(uint32_t data, uint8_t flag)
{
  switch(flag)
  {
    case 0: temp = (uint32_t) (data>>24); break;
    case 1: temp = (uint32_t) (data>>16); break;
    case 2: temp = (uint32_t) (data>>8); break;
    case 3: temp = (uint32_t) (data); break;
    default: temp = 0xFF; break;
  }
  return &temp;
}

цикл можете сами развернуть по желанию.

Логика: левую границу окна байт контролируем сдвигом вправо, правую - кастом uint8_t.

Obezyan
()
Последнее исправление: Obezyan (всего исправлений: 1)
Ответ на: комментарий от gaylord

Нет. Компилятор знает семантику memcpy и может полностью исключить обращение к библиотечным вызовам. Это делается на основе информации о размере копируемых регионов памяти; на x86 для uint32_t на любом уровне оптимизации там будет просто load/store.

Сложнее ситуация для ARM, так как для обращения требуется выравнивание, о котором компилятор в общем случае не знает, значит будет вынужден исходить из worst case и приседать для загрузки невыровненных данных. Что для uint32_t скорее всего приведет к нескольким load/store по частям.

Если ты выравнивание обеспечиваешь сам, можно сделать каст с разыменованием. Если бояться «UB» и идти по пути memcpy, в C++ есть std::assume_aligned, в gcc – __builtin_assume_aligned.

Siborgium ★★★★★
()
Последнее исправление: Siborgium (всего исправлений: 1)

Бред. Листинги асма бесполезны на 100%, так как это микроскопические функции, скомпилированные для вызова извне без какой-либо информации о контексте использования, с поддержкой всех вариантов входных данных. Тогда как тебе, особенно если ты пытаешься в производительность, нужен контекст. Который ты получишь только инлайном.

Добавь static inline (или даже __attribute__((always_inline))), и смотри на влияние в том контексте, в котором ты реально это используешь. В противном случае смотреть на асм бессмысленно и даже вредно.

Siborgium ★★★★★
()