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

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

Срань господня что это мать вашу

Сейчас смотрю код и ржу. Это валидный код, просто для ардуинки, только uint8_t temp; объявить ещё в начале.

Вернулся из заведения под шафе, пока ожидал супругу из душа чтобы не отрубиться открыл с телефона ЛОР и ответил в первую попавшуюся тему. Внутренний компилятор признал код валидным и скомпилировал. Комменты даже не читал. У меня такое редко, но бывает, когда начинаешь писать код на другом языке или диалекте. Python в PHP файле и тд.

Obezyan
()

Это генерирует одну инструкцию, но это UB по стандарту, хотя по факту работать будет…

Наличие UB зависит от того на что этот uint8_t* указывает.

Указатель на переменную типа uint8_t на стеке? Несомненный UB независимо от выравнивания (доступ к объекту по указателю не совпадающему с эффективным типом объекта)

Указатель на аллоцированную память? Зависит от того что в этой аллоцированной памяти хранится. Если эффективный тип объекта по адресу в указателе это uint32_t, то можно читать писать без проблем. А что там хранится определяется первой записью в эту область памяти.

Указатель на 32-х битный аппаратный регистр отмапленный в этот адрес? По-моему, UB.

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

Наличие UB зависит от того на что этот uint8_t* указывает.

Не, тут вопрос в другую сторону. По стандарту любой указатель может быть представлен как unsigned char *. А вот в обратную сторону – нет.

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

А вот в обратную сторону – нет.

Почему? *(int*)(char*)&int_var - валидный код.

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

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

Почему? (int)(char*)&int_var - валидный код.

В таком виде – да.

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

Лучше-то от этого не становится.

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

Вопрос не в этом коде. А в

char buf[4];
*(int*)buf = 1;

Это код с UB. И никакой alignas это не спасёт. Нет в стандарте разрешения кастовать массив байтов в int* ни при каких условиях.

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

Я не могу его так поменять, там API библиотеки сделано так, что работает с uint8_t и возвращает куски из этого буфера. Я могу этот буфер выровнять через alignas, но указатели проходят через несколько функций, у компилятора нет шансов отследить это выравнивание. Я, конечно, сам знаю, что они выровнены, но для компилятора это uint8_t*.

Сейчас я сделал через __builtin_aligned_as и код, который просто через битовые сдвиги читает/пишет uint32 в le. Компилятор распознаёт всё это дело и заменяет на одну инструкцию. Хотя это и не стандартный C. Видимо в рамках стандартного C этого никак не сделать. Ну если ту наркоманию с union не учитывать, может оно и работает, но так делать я точно не буду.

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

у компилятора нет шансов отследить это выравнивание

По-моему, если компилятор не может отследить значения какого типа было записано в область памяти, то он должен трактовать его как «an object having no declared type», то есть предполагать, что там лежит то, что мы сказали типом указателя и что всё выровнено как надо (а если на самом деле лежит не то и выравнено не так, то мы получаем UB).

Впрочем, да, это слишком глубокий уровень language lawyering для меня. Так что утверждать не буду.

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

если компилятор не может отследить значения какого типа было записано в область памяти, то он должен трактовать его

Нет. Одна из особенностей си - компилятор не гарантирует соблюдение семантики во всех синтаксически корректных инструкциях. Эта работа лежит на программисте. Компилятор будет трактовать данные в соответствии с объявленным типом. При касте указателя гарантии компилятора снимаются.

там лежит то, что мы сказали типом указателя и что всё выровнено как надо (а если на самом деле лежит не то и выравнено не так, то мы получаем UB).

Всё хуже. Мы получаем UB, если производим запись по указателю одного типа, а читаем - другого. Вообще не зависимо ни от чего это 100% UB, потому что компилятор не может гарантировать в этом сценарии семантическую корректность прочитанных данных.

Вот если пишем + читаем последовательно по указателям разного типа в одну область памяти - то ок.

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

Хотя это и не стандартный C. Видимо в рамках стандартного C этого никак не сделать.

можно прямо по определению сделать не трогая битшифт(который тоже УБ обвешан):

uint32_t conv_le(uint8_t *p) {
    uint32_t r = p[0]+256*p[1]+65536*p[2]+16777216*p[3];
    return r;
}

и использовать как референсную реализацию для отладки логики, и даже компилятор распочухал и соптимизировал O_O: https://godbolt.org/z/zWsfzb4Es

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

Не, тут дело не в компиляторе, а в выравнивании. На 32-битном ARM нельзя читать/писать без выравнивания, совсем. На x86 можно и это, видимо, будет в любом случае быстрей, чем собирать/разбирать по байтам. Поэтому если с __builtin_assume_aligned код переписать, то будет везде хорошо.

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

Rust это в другую сторону от того, что я хочу.

Что я хочу в отличие от C:

  1. Тщательно отобранные оптимизации, которые не приносят удивления. К примеру запретить перестановку операций записи в память в сгенерированном коде, если они в разных инструкциях. Убрать весь бред с тем, что разыменование нуля это UB. Если я хочу разыменовать нуль, значит надо сгенерировать инструкцию чтения нулевого адреса. В том же ARM это абсолютно валидная инструция.

  2. Убрать UB с тех вещей, где это неуместно. Вот прям этот топик - живая иллюстрация. Не должно быть никаких проблем при касте u8* в u32*. Просто компилируй это. Если будет hardfault при выполнении, то и ладно. Может быть я того и хочу? Убрать бред вроде запрета на переполнение signed integer. Просто компилируй плюс в ADD и не морочь мне голову.

  3. Убрать кучу этих тупых типов. short, int, long, long long и тд. Просто i8, u8 и тд. Конкретные типы конкретного размера и всё.

  4. Убрать тупую стандартную библиотеку. Вообще и полностью. Она отстой и не нужна.

  5. Убрать кучу лишнего синтаксиса. Все эти 3 типа циклов, break, continue - зачем всё это? Есть же if и goto. Зачем нужны static переменные в функции, если их можно вынести за пределы функции? Абсолютно ненужная фича. Зачем нужна инициализация глобальных переменных нулями по умолчанию? Моему процессору есть чем заняться и без этого. Вот лучше бы добавили возврат нескольких значений, ей-богу.

Так можно долго продолжать.

А фичи раста прикольные, не спорю, но они не решают тех проблем, с которыми я сталкиваюсь. Ну типа - да, можно писать код без проблем с памятью. Но мне это не нужно. Я и так пишу код без проблем с памятью. Неимоверное усложнение языка ради избавления от редких багов мне не интересно. Может тем, кто пишет софт для марсоходов и интересно.

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

Ну типа - да, можно писать код без проблем с памятью. Но мне это не нужно.

Все это время что ты потратил что UB, а что не UB, можно было бы не тратить! Классное же.

gaylord
()