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)
Ответ на: комментарий от gaylord

И как бы решение этой задачи схематически выглядело на Rust?

Сформулирую более понятно, как это работает. Есть ringbuffer, куда добавляются и берутся куски по 32 байта. На эти куски нужно получать указатель в середину (на 5-й байт) и передавать этот указатель в SPI-периферал, куда он будет писать данные. Потом нужно записать в 4-й байт uint32. И потом всё закоммитить и в обратном порядке в другом месте читать и парсить.

Подозреваю, что на Rust я бы сразу и упёрся в реализацию ringbuffer без копирования буферов. На C у него есть 4 операции - alloc + put и get + free, причём все операции разные, к примеру я сначала делаю alloc, потом запускаю SPI, потом в обработчике прерывания делаю put. При парсинге я делаю get, делаю парсинг данных, потом делаю free. Думаю, что раст меня сведёт с ума ещё на этапе alloc + put, разнесённых между несвязанными кусками программы. Там же всё владение идёт лесом. Быстро проглядел 5 реализаций рингбуферов на расте из гугла, нигде ничего подобного нет.

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

На мой взгляд, дело не в языке, а в том, что все данные в одном адресном пространстве. Аналогом Си является Паскаль и там всё тоже самое.

А выравнивание переменной на адрес, кратный машинному слову (4 байта), это вообще заморочка, пришедшая от процессоров и спихнутая разработчиками Си компиляторов на программистов. Ибо дело даже не в скорости, а в том, что некоторые процессоры не правильно загружают слово (4 байтовое), если адрес не выровнен.

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

Про одно адресное пространство не совсем понял.

Про выравнивание - тут дело в том, что C лезет не в своё дело. Его дело - компилировать код. Почему Undefined behaviour-то? Это на 100% defined behaviour. В x86 всё будет работать. В ARM возникнет hardfault, в этом тоже никакой проблемы нет, это стабильное, повторяемое и определённое поведение. В том-то и проблема, что ничего они на программистов не спихнули. Они как раз на себя много берут.

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

В x86 всё будет работать.

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

а так да. все зашибись.

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

С этим как раз проблем нет. Ни по эффективности использования кеша, ни по атомарности C никаких гарантий не даёт. Нужно атомарное чтение - используй _Atomic. Нужно эффективное использование кеша - вообще хз, ползай с профайлером и дизассемблером.

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

Так вот UB на расте типичное...

https://godbolt.org/z/WT16v5vfM

extern "C" {
    fn printf(fmt: *const i8, ...) -> i32;
}

pub fn aliasing(a: *mut i32, b: *mut f32) -> i32 {
    unsafe {
        *a = 1;
        *b = 0f32;
        return *a;
    }
}

fn main() {
    let mut i = 0;
    i = aliasing(&mut i as *mut i32,  &mut i as *mut i32 as *mut f32);
    unsafe {
        // Печатает 1, должно 0
        printf("%d" as *const str as *const i8, i);
    }
}
MOPKOBKA ★★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от vbr

Про выравнивание - тут дело в том, что C лезет не в своё дело. Его дело - компилировать код. Почему Undefined behaviour-то?

Это нюансы разработчиков GCC и их видения. Им надо, чтобы в результате компиляции и вы, другой программист, получили работающий код. Да еще и на разных процессорах. Компилятор же вас не ограничивает, а лишь предупреждает о возможно не выровненном адресе переменной.

Есть общепринятые UB, например, не инициализированная переменная, принадлежащая программе, это UB. А, например, инициализировать память по адресу регистра чтения из условной «микросхемы SPI» (в драйвере), это уже UB с точки зрения правила работы с микросхемой в адресном пространстве.

Про одно адресное пространство не совсем понял.

Это мой комментарий к вашей фразе про «замену Си» и что «все этого хотят».

Все данные и сам код находятся в одном адресном пространстве процессора для работающей программы. Из-за этого и Си и С++ и Паскаль такие, какие есть. Т.е. с возможностью написать программу, меняющую/читающую любой байт в адресном пространстве. Собственно, это их основное назначение, делать именно так.

Поэтому, я не хочу замены Си.

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

Ни по эффективности использования кеша, ни по атомарности C никаких гарантий не даёт. Нужно атомарное чтение - используй _Atomic. Нужно эффективное использование кеша - вообще хз, ползай с профайлером и дизассемблером.

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

_Atomic на невыровненые данные вообще забивает небось, не обеспечивая атомарности.

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

Ничего специфичного для раста. На сишечке это такой-же UB, так как &mut i32 соответствует int32_t * restrict. А, и без restrict тоже UB из-за strict aliasing.

https://godbolt.org/z/TGj9re8sP

Корректный (кажется) вариант на расте должен использовать &raw mut i.

https://godbolt.org/z/Ee6vjoeGq

В расте кажется strict aliasing нет, но точно не помню, потому что такой код я точно писать не стану: там есть функции чтобы сказать компилятору, что нужно прочитать содержимое памяти независимо от того что компилятор думает по поводу provenance или aliasing.

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

Ничего специфичного для раста. На сишечке это такой-же UB.

Ну так я про это и пишу. В чем смысл Rust если там те же UB, в контексте этого треда, где автор хочет обойти UB, просто что бы компилятор ему не мешал и делал как надо.

restrict

Если поменять тип второго аргумента на i32 то работает уже «правильно». Так что дело не в нем.

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

Это нюансы разработчиков GCC и их видения

Нет, это нюансы разработчиков стандарта C. gcc как раз в интересующих меня версиях компилирует код как я ожидаю. Но полагаться я на это не могу.

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

Компилятор меня ни о чём не предупреждает.

Про одно адресное пространство не совсем понял.

Это мой комментарий к вашей фразе про «замену Си» и что «все этого хотят».

Всё равно не понял, ну да ладно.

Все данные и сам код находятся в одном адресном пространстве процессора для работающей программы.

Это неправда в общем случае. Есть процессоры, у которых не одно адресное пространство. Да взять банальный DOS с сегментами, это не совсем то, но там уже не одно адресное пространство. Процессоры, у которых данные и код находятся в разных сегментах, для работы с ними используются разные инструкции процессора, тоже существуют, хотя и не так известны. И C для них существует.

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

У меня печатает 0, но я не очень понимаю где здесь UB.

Вот она читаемость Rust... Ну мне тоже на это тяжко смотреть, вот тебе пример на С который делает тоже самое, и тоже фейлиться: https://godbolt.org/z/c9x43rbdf

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

Ещё раз, в чем UB-то? У тебя есть функция, в которой ты тремя разными способами мутируешь переменную. Где-нибудь определен строгчий порядок, или ты просто породил хтонь, которая законным образом ведет себя как хтонь?

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

Ещё раз, в чем UB-то? У тебя есть функция, в которой ты тремя разными способами мутируешь переменную.

Нельзя ссылаться указателями разных типов на один кусок памяти как делается в примере. Без разницы сколько там изменений, замени float* на на int* и будет правильный ответ.

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

Нельзя ссылаться указателями разных типов на один кусок памяти как делается в примере.

А интернет говорит, что ты неправ:

Yes, in Rust *mut f32 and *mut i32 may alias, and that results in worse assembly that the same function with &mut f32 and &mut i32. Godbolt link

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

Я скинул ссылку на пример который печатает 1 в консоль, и там консольный вывод есть. Что пишут в интернете меня не особо интересует, я показал самый лучший пруф из возможных.

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

Я скинул ссылку на пример который печатает 1 в консоль, и там консольный вывод есть.

Я вижу. Вопрос в том почему: потому что UB или IDB? Интернеты говорят, что UB нет и ты просто породил хтонь.

gaylord
()