LINUX.ORG.RU

То что сложно реализовать на других языках

 


0

3

Разбираюсь с библиотекой qsp и решил с вами поделится кусочком кода, а точнее реализация crc32. Автор кода, решил создать свой собственный crc32, на базе CRC-32/ISO-HDLC, взяв от туда таблицу, но изменил стартовое значение, и еще по мелочи:

int qspCRC(void *data, int len)
{
    unsigned char *ptr;
    int crc = 0;
    ptr = (unsigned char *)data;
    while (len--)
        crc = (qspCRCTable[(crc & 0xFF) ^ *ptr++] ^ crc >> 8) ^ 0xD202EF8D;
    return crc;
}

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

★★★★

использовать всякие архитектурные приколы это, конечно, забавно. Just for fun и всё такое. Но вот толкать это как преимущество языка это какая-то шиза.

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

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

а правильно на си писать так

пишется макра один раз в жизни:

typedef uint8_t u8;

#define for_each_u8(adr,len) \
for (\
  u8 *_curr = (u8*) adr, *_end = _curr + len;\
  _curr < _end; \
  ++_curr \
)\

и потом все ваши растовые «фолды на итераторах с лямбдами» по-человески выглядит так. дешево, просто и сердито.

uint qspCRC(void *data, int len) {
  uint crc = 0;
  for_each_u8 (data, len) { 
    crc = (qspCRCTable[(crc & 0xFF) ^ *_curr] ^ (crc >> 8)) 
          ^ 0xD202EF8D;
  }
  return crc;
}
alysnix ★★★
()

вроде это UB

Во-первых есть флаг -fwrapv, с которым это не UB. Если автор компилирует свой код с этим флагом, то проблем не будет.

Во-вторых на UB наткнуться не так уж просто. Код, как в примере выше, gcc откомпилирует без сюрпризов.

Вот код вида

int f(int x) {
  return x < x + 1 ? 1 : 2;
}

Компилятор скомпилирует просто в return 1

В-третьих я вообще в приведённом коде не вижу переполнений, но может быть я просто туплю.

If the left operand of a right-shift operator has a signed type and a negative value, the result is implementation-defined

Т.е. это не UB, а implementation-defined.

В gcc написано:

For a signed data type, GNU C performs “arithmetic shift,” which keeps the number’s sign unchanged by duplicating the sign bit.

Так что код не слишком хороший, и завязан на конкретный компилятор. Переписать его стоит. Но огромной проблемы в этом коде нет.

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

также в растовом коде длина не передается, а в сишном она есть.

&[u8] == std::span<uint8_t>, а std::span это не массив, и данные он не хранит. Это указатель на какой то элемент + размер. То есть ничем не отличается &[u8] от (u8 *p, int size)

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

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

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

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

а правильно на си писать так

«Правильно» на си писать через циклы, а не вот этот вот выпендрёж «а еще я могу аппендицит через гланды удалять» с нетипизированными макрами.

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

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

так этот спан нужно создать

Это ничем не будет отличаться от «создания» двух аргументов, структуры малых размеров передаются так же как и аргументы через регистры: https://godbolt.org/z/W1nf79rGn

По моему ссылка в синтаксисе Rust относится к данным, а не к самому span. Так что пример на С полностью идентичен ситуации в Rust.

причем спан-то типизированный как массив u8

Я сомневаюсь что в Rust есть void *, не думаю что преобразование в байт-форму это задача функции подсчета crc.

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

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

Это типа хорошо? Сишный void * это одна из самых больших жоп в программировании, когда-либо изобретённых, источник 90% всех ошибок. Понятно, что в 70х годах особо выбора не было. Но блин, продолжать этой жопой восхищаться в 2025 году это какой-то особый, неизлечимый вид мазохизма.

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

Если const void * используется только для последующей конвертации в const char *, то я не вижу какие там могут быть проблемы.

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

Что бы можно было написать

crc(&moya_data) вместо crc((const char *)&moya_data)

как в memcpy, и других. Какой смысл кастовать постоянно?

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

как в memcpy, и других. Какой смысл кастовать постоянно?

Для того, чтобы легче было понять, что хотел сделать программист?

А void * говорит о том, что программист решил «а, и так сойдёт», там как-нибудь разберемся.

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

Мне как то и так понятно, что хотел сделать программист когда вызывал memcpy, crc с указанием размера, а вот const char * лишь отвлекает.

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

«сишный» void* это просто дженерик адрес, зерокост преобразуемый к любому указателю, и наоборот. он есть не только в си.

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

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

Мне как то и так понятно, что хотел сделать программист когда вызывал memcpy, crc, а вот const char * лишь отвлекает.

В 50% случаев да. Пусть даже в 80%. В остальных это источник ошибок. Явное указание типов везде и всегда может быть не то, чтобы сильно отвлекает при чтении. Зато дисциплинирует пишущего код и позволит не допустить глупых ошибок. Да, в сях приходится страдать. Либо потом дебажить глючный код с тонной указатедей на воиды. Ну, кому как больше нравится.

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

«сишный» void* это просто дженерик адрес

Да какой это нафиг дженерик. При отсутствующей рефлексии, ага. Это зияющая дыра в системе типов.

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

А void * говорит о том, что программист решил «а, и так сойдёт», там как-нибудь разберемся.

без нетипизированного указателя вы просто не можете написать функцию навроде getmem в языке со строгими типами. Или она должна воззвращать стразу типизированный указатель, навроде new в c++.

void* это «указатель на любой тип» или просто тип address, как раз нужный тогда, когда с адресом нужно работать, но адрес еще не имеет связи с конкретным типом.

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

В const char * можно скастить что угодно, как и в const void *, так что разницы между ними нету, просто для второго не нужен лишний каст в коде который будет его мусорить. Если ты ошибся и не то поле, или не ту структуру скастовал, то const char * тебя не защитит.

Вместо (const char *) можно требовать делать (const void *), так же «полезно», и бессмысленно.

Я не знаю ни одного проекта где требуется дополнительный каст в const char *, и думаю не просто так. Может ты знаешь?

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

без нетипизированного указателя вы просто не можете написать функцию типа getmem в языке со строгими типами. Или она должна воззвращать стразу типизированный указатель, навроде new в c++.

Так я и говорю о том, что в си приходится страдать. Да, в си по-другому никак. Ну, можно юнионы изобразить, или другие костыли. Великолепное решение, error-prone во все поля.

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

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

ну вот скажите как в языке с ООП, вы будете писать менеджер кучи, который просто выделяет память, не представляя, что в нее будет положено?

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

то есть от выделения куска памяти, до инициализации его как корректного значения некоторого типа, дистанция огромного размера. вот на этой дистанции и нужен address, он же void*.

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

все эти memset, getmem, memcpy и прочие, используют void* и не могут использовать char*(или u8*), потому что пришлось бы или везде втыкать преобразования типов в u8*, или делать u8* совместимым со всеми прочими указателями, и сломать строгую типизацию тут. то есть если T* будет по дефолту приводиться к u8* - это будет страшно вообще.

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

Менеджер кучи (редкий частный случай, где вообще без костылей не обойтись, в любом языке) и подсчёт CRC (и вообще обработка какого-то буфера, использование void* в котором говорит о плохом дизайне).

Одно и то же, конечно.

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

Ну вот ты покажи как упакованную структуру в crc передать на Rust, полный код, сравним с С.

Не понял вопрос. А в чём проблема? Скинуть структуру в u8 вектор и вызвать crc?

Или надо именно как в си, через void*? Так раст как раз не про это, и это сознательный выбор при дизайне языка. Ну, насколько я знаю.

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

Не понял вопрос. А в чём проблема?

В том что длинно и уродливо, а в С красиво и понятно. Излишняя подробность и дополнительные конструкции приводят к ухудшению читабельности и неудобству.

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

В том что длинно и уродливо, а в С красиво и понятно

Разные подходы

Излишняя подробность и дополнительные конструкции приводят к ухудшению читабельности и неудобству.

А излишнее сование void* во все дыры приводит к скрытым, трудно выявляемым ошибкам. Зато кратко, да, и компилятору пофиг. Только потом на отладке, если что, приходится брейкпоинтами обкладываться.

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

Менеджер кучи (редкий частный случай, где вообще без костылей не обойтись, в любом языке) и подсчёт CRC (и вообще обработка какого-то буфера, использование void* в котором говорит о плохом дизайне).

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

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

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

Я лично не помню, чтобы его применяли всегда и везде(он не удобен для «всегда и везде», поскольку раз нет реального базового типа - то любой шаг в сторону - и это ошибка комплияции). Применяют там где надо. В принципе для параметра при подсчете crc от произвольного обьекта - это то что-надо.

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

он не удобен для «всегда и везде»

Применяют там где надо. В принципе для параметра при подсчете crc от произвольного обьекта - это то что-надо.

Эээ… Это как раз и есть «всегда и везде». Для CRC нужно пройтись по массиву байтов. Я понимаю приводить в массиву байтов в общем случае массив целых, например. Составные объекты перед посчётом лучше в массив байтов сериализовать.

А произвольный объект передашь - произвольный результат и получишь. Не всегда: в тестах всё гладко будет, разумеется. Но гейзенбаг не дремлет.

Я искренне не понимаю, зачем упорствовать, и сознательно продолжать крутить дырку в стене гвоздём, когда существует дрель.

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

Лучше сравнить Rust с молотком который требует заполнения и подписи десятка документов перед возможностью ударить по гвоздю. Документы рассматриваются компилятором на корректность.

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

Как я уже сказал, (const char *) никак не спасет от неправильной передачи. Сувать надо не брекпоинты а ватчи.

Один фиг потом по шагам нужно будет проверять, что же оно там считает на самом деле вместо того, что планировалось.

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

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

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

Лучше сравнить Rust с молотком который требует заполнения и подписи десятка документов перед возможностью ударить по гвоздю. Документы рассматриваются компилятором на корректность.

Да мне даже не нравится раст, но вот тот момент, что он дисциплинирует программиста и не даёт писать код «на от*бись», «а, и так сойдёт, потом отдебажим» - мне как раз нравится.

Да, может быть медленнее, зато осмысленнее.

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

Я просто советую, не знаю имел ли ты виду сам watch

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

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

Для CRC нужно пройтись по массиву байтов.

ну а если у вас массив float - вы можете от него считать crc? как вы будете подставлять в функцию подсчета crc такой массив? для void* вы ничего и не заметите. для например u8* - вам придется заниматься явно сомнительными преобразованиями float* в u8*.

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

ну а если у вас массив float - вы можете от него считать crc? как вы будете подставлять в функцию подсчета crc такой массив? для void* вы ничего и не заметите. для например u8* - вам придется заниматься явно сомнительными преобразованиями float* в u8*.

В Си версии изначально кривой API ввиду ограниченности ЯП. Для Rust идиоматично (и в целом правильнее) будет как-то так. Пример использования:

fn main() {
    use qsp::{Crc, crc};

    dbg!(crc(0_u8));
    dbg!(crc(0.0_f32));
    dbg!(crc(&[1_u8, 2, 3, 4][..]));
    dbg!(crc([1.0_f32, 2.0, 3.0, 4.0]));

    #[repr(C)]
    struct Foo {
        x: u8,
        // pad: [u8; 3],
        y: u32,
    }

    impl Crc for Foo {
        fn crc(&self, state: &mut qsp::CrcState) {
            self.x.crc(state);
            self.y.crc(state);
        }
    }

    dbg!(crc(Foo { x: 1, y: 2 }));
    dbg!(crc([1_u8, 2, 0, 0, 0]));
    dbg!(crc(&(1_u8, 2_u32)));
}>

Можно ещё обмазаться процедурными макросами для автоматического derive(Crc), но это уже сам копай если интересно.

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

Ахах, вот оно будущее с Rust, вместо crc(&my_struct) нужно описать crc_my_struct где собрать все поля, и не забыть подструктуры, и не забыть при добавлении нового поля изменить все crc32_my_struct, crc64_my_struct, xxx_my_struct.

Это же так круто дублировать код, и выполнять самую тупую работу машинистки переписывая все поля. Отягощается все отсутствием X Macro из сишки.

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

Это же так круто дублировать код, и выполнять самую тупую работу машинистки переписывая все поля.

У тебя всегда есть возможность сделать как в Си.

pub unsafe fn crc_all<T: Sized>(value: T) -> u32 {
    crc(unsafe {
        std::slice::from_raw_parts(
            &value as *const T as *const u8,
            std::mem::size_of::<T>(),
        )
    })
}

dbg!(crc(Foo { x: 1, y: 2 })); // 763816754
dbg!(unsafe { crc_all(Foo { x: 1, y: 2 }) }); // 763734900, т.к. выравнивание, короче мусор
numas13
()
Ответ на: комментарий от numas13

Уже прочел, хорошо ты вспомнил, ведь именно так у растовиков: Нет, нам не нужен void *, ведь мы можем написать тоже самое в несколько раз длиннее. Нет, нам не нужен препроцессор, ведь мы можем вручную описывать каждую структуру.

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

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

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

Я уже выше написал про процедурные макросы и всё будет автоматизированно, корректно, быстро и безопасно.

ПыСы: Очень неудобно, что ты постоянно редактируешь сообщения. :)

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

Я уже выше написал про процедурные макросы и всё будет автоматизированно, корректно, быстро и безопасно.

Есть пример такого макроса? Я думаю что он выйдет намного длиннее чем макрос на С. И непонятно почему ты его описываешь как «быстро» (=производительно?) и «безопасно», по сравнению с чем?

Предлагаю написать вот это на Rust, с этими твоими макросами, и можно будет быстро сравнить.

#define X_POINT \
  X(int, x) \
  X(int, y) \
  X(int, z) \
  X(int, w)

struct point {
#define X(T, N) T N;
  X_POINT
#undef X
};

void point_crc32(const struct point *p, crc32_state_t *state)
{
#define X(_, N) crc32(state, &p->N, sizeof(p->N));
  X_POINT
#undef X
}

void point_crc64(const struct point *p, crc64_state_t *state)
{
#define X(_, N) crc64(state, &p->N, sizeof(p->N));
  X_POINT
#undef X
}

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

Есть пример такого макроса? Я думаю что он выйдет намного длиннее чем макрос на С.

Да, процедурные макросы потребуют больше кода. Но это для автоматизации реализации трейта Crc для произвольных структур.

«безопасно», по сравнению с чем?

По сравнению с Си. play.rust-lang.org

    #[repr(C, packed)]
    struct Foo {
        x: u8,
        y: u32,
    }

    impl Crc for Foo {
        fn crc(&self, state: &mut qsp::CrcState) {
            // SAFETY: мамой клянусь что безапасна, т.к. packed :)
            unsafe { state.write_as_bytes(self) }
        }
    }
numas13
()
Ответ на: комментарий от numas13

Да, процедурные макросы потребуют больше кода.

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

По сравнению с Си. play.rust-lang.org

Ну ты просто спрятал то что выше показывал, std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>()), это в С писать не надо. В чем безопасность? Что в С конвертируем в char, что в Rust.

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

считать такие хеши надо максимально быстро,

Вот поэтому и надо брать раст, это ты чё там гадаешь по размеру ассемблерного выхлопа? Такое правильно так сравнивать:

const qspCRCTable: [u32; 256] =[ ... ];

fn qspCRC(data: &[u8]) -> i32 {
    data.iter().fold(0, |crc, b| {
            let tbl = qspCRCTable[(b ^ crc as u8) as usize];
            (tbl as i32^crc>>8) ^ 0xD202EF8Du32 as i32
        })
}

use std::time::*;
fn main() {
    let len = 1_000_000_000;
    let data: Vec<u8> = (0..len).map(|i| (i%256) as u8).collect();
    let t0 = Instant::now();
    let crc = qspCRC2(data.as_slice());
    println!("time {} s", (t0.elapsed().as_micros() as f64)/1e6f64 );
    println!("crc {}", crc);
}
time 2.112588 s
crc 2031280483

hyperfine ./crcr
Benchmark 1: ./crcr
  Time (mean ± σ):      2.640 s ±  0.007 s    [User: 2.163 s, System: 0.477 s]
  Range (min … max):    2.630 s …  2.654 s    10 runs
int qspCRCTable[256] = {...};
int qspCRC(void *data, int len)
{
    unsigned char *ptr;
    int crc = 0;
    ptr = (unsigned char *)data;
    while (len--)
        crc = (qspCRCTable[(crc & 0xFF) ^ *ptr++] ^ crc >> 8) ^ 0xD202EF8D;
    return crc;
}
int main(void) {
    int32_t len = 10000000;
    uint8_t* data = (uint8_t*)malloc(len);
    for(uint32_t i=0; i<len; i++) { data[i] = i%256; }

    clock_t start_time = clock();
    int crc = qspCRC(data, len);
    double elapsed_time = (double)(clock() - start_time) / CLOCKS_PER_SEC;
    
    printf("time %f s\n", elapsed_time);
    printf("crc %i\n", crc);
}
time 2.113202 s
crc 2031280483

hyperfine ./crctst
Benchmark 1: ./crctst
  Time (mean ± σ):      2.717 s ±  0.009 s    [User: 2.243 s, System: 0.474 s]
  Range (min … max):    2.705 s …  2.734 s    10 runs

hyperfine для оценки погрешности, короче разницы между llvm и gcc, и между раст и си для функции qspCRC в пределах погрешности нет.

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