Panic! At the 0xC0D3
177 subscribers
11 photos
24 links
Пишу что-то про разработку
Download Telegram
Channel created
Channel name was changed to «Panic! At the 0xC0D3»
Software developer, machine learner, playboy, devops
github
Сижу вот изучаю Rust
Как вы, наверное, знаете, в C/C++ есть switch, который работает для чисел
И компилятор там всеми силами пытается его соптимайзить, фигачит jump table какой-то или около того
В Rust есть по сути тоже самое, но еще и со строками (match str), и возник вопрос, а как оно там, собственно, оптимизируется

Короче - сам поиск примерно никак, используется линейный поиск, но оптимизируется сравнение строк.
Единственное, если размер строк разный, то он сделает jump table по их размеру (по сути, как с обычными числами)

А вот со сравнением строк уже интереснее. Да да, это скорее всего уже не сам раст, сколько llvm, но все равно интересно
1) Если строки <8 байт, то строка запишется как число, и компилятор просто прочитает строку как число и сделает сравнение
2) Если строки <16 байт, то сделает два сравнения (два числа 8 байт)
3) Если строки <32(?) байта, то фиганет SIMD сравнение (строки все еще числа)
4) Иначе, вызовет какую-то функцию сравнения длинных строк, которая, думаю, использует какие-то идеи из 1-3

Обидно конечно, что он не оптимайзит для длинных строк ничего. В моем случае (см картинку) очевидно, что можно понять, какая именно строка нужна по первому символу даже, и дальше сделать всего-лишь 1 проверку.
May be one day я решусь залезть в llvm...

Так что если вдруг кто-то решит переписать ejudge на раст, то придется и для него кодогенерировать бор для команд...
👍1
В Go можно написать простой rate-limiter, который ограничивает нагрузку, двумя способами: перед запуском горутины, или внутри нее. Я вот не задумывался, что они отличаются довольно сильно в плане порядка обработки запросов
За подробностями прошу сюда: https://leviska.notion.site/3dbe7633fd5449ff99c485d147f2d420
👍1
Написал что-то похожее на kwargs, но статически типизированное и на расте. Не спрашивайте меня зачем. playground
Немного макросов, и я думаю, что можно сделать даже красиво

https://twitter.com/leviska0/status/1536502738237931520
👍3
Я все долго хотел написать пост про раст, и, наверное, когда-то его напишу, но недавно нашел это прекрасное видео, которое рассказывает про тот самый safety, что дает Rust, сравнивая куски кода с C++. Если вы все задавались вопросом "да кто такая безопасность в этом вашем расте", то видео очень хорошее
https://www.youtube.com/watch?v=IPmRDS0OSxM
Где-то год назад я полностью осознал казалось бы очень простую мысль: "все программирование, все, с чем мы работаем, это все код. Код, который зачастую опенсорсный, куда ты можешь зайти, почитать его, и если вдруг нужно - поменять"
"Well, duh", скажете вы, но главная суть этой идеи в том, что менять опенсорсный код - это нормально и не так уж и сложно. Например, если вы выбираете какую-то библиотеку или инструмент для работы, и он вот чуть чуть не делает того, что вам надо, то у вас есть возможность просто сесть и доделать это. Не надо ныть, что "уу хорошая библиотека, но вот фичу не умеет", не надо искать другие менее популярные (и более багованые) аналоги, просто сядь и пофикси (лол)

Вчера вот я вышел на новый уровень какой-то: в Go есть хорошая ORM библиотека для БД gorm, и есть хороший инструмент для миграций goose, написанный на го, и в котором можно в том числе писать миграции на го. Но вторая штука не умеет работать из коробки с gorm, и в итоге по сути надо делать два соединения с бд и работать с gorm через глобальную переменную, что не является проблемой, но блин некрасиво!
Я пошел в репу и нашел PR, который за 10 строчек добавляет возможность удобно подрубить gorm. Но чел этот пр видимо не запускал, и когда я запустил его у себя локально, то у меня все ушло в вечный цикл и ничего не работало.
Три часа дебаггинга спустя, я нашел забытый return nil, форкнул форк, пофиксил в нем багу, и теперь все хорошо работает
Но почему я решил этим поделиться: для меня лично это был новый уровень, когда я фиксил даже не саму либу, а чей то пр этой либы. Т.е. мало того, что я решил не искать аналог и пофиксить уже хороший инструмент, так я не стал в очередной раз городить велосипед, а нашел уже чей-то другой велосипед и починил пару костылей в нем.

Вторая причина, почему я хотел этим поделиться, это попытаться завлечь вас делать так же. Я часто обсуждаю всякую прогу с друзьями и коллегами, и часто слышу как люди сталкиваются с похожими проблемами "вот есть штука, но она делает почти все, что надо", на что я отвечаю "ну просто пофикси лол))", и люди думают, что я шучу, мол "да зачем", "ой лезть еще туда", "я не смогу" и т.д., но при этом ни разу не пробовали так делать. За последние два года у меня уже накопилось несколько контрибьюшенов в опенсорс софт, и все эти контрибьюшены (не считая опенсорсных репозиториев с работы) были ровно такими же: есть хороший инструмент, но нужна какая-то мелочь, сел, разобрался, добавил эту мелочь, получил свои фичи и плюс в карму за помощь в разработке опенсорса. И скажу честно: в первые разы было очень тяжело, я действительно тратил много времени (несколько дней), чтобы просто разобраться в чужом коде, но это просто скилл, причем очень полезный, и я крайне рекомендую в следующий раз, когда от хорошего инструмента будет нужна еще какая-то мелочь, просто сесть и добавить ее
👍7
Как выражать логику через систему типов, или пишем код, который не дает багать
https://leviska.notion.site/33f6445acb704440b57faae727123572

Я долго хотел написать этот пост, но все не мог найти хороший пример и не скатиться в "объяснение что такое Rust и почему он крута" на 30 страниц. И вроде бы получилось.
Если вы программируете на C++ и не понимаете вот этого хайпа вокруг Rust насчет "компилируется - значит работает" - пост для вас, с примерами на C++ и сравнением двух реализаций
👍7
😁5🤔1
Еще когда я даже не учил Rust, но спрашивал у знающих знакомых "как работает XXX", меня часто не удовлетворял ответ: казалось, что для того, чтобы писать такой же эффективный код на Rust, как и на C++, мне бы пришлось постоянно использовать unsafe.
Возьмем Option/optional:
Плюсы говорят "вот тип, вы можете проверять, лежит ли в нем что-то, а можете не проверять, так как уже проверяли когда-то до этого, нам пофиг. Но если не проверите, то будет бобо"
Раст говорит "вот тип, вы или должны проверить, или должны явно указать, что вы умнее через unsafe"
И во времена, когда я был ярым C++сером, мне казалось, что это будет как в плюсах, только неудобнее, потому что я даже не подозревал, что можно лучше.

Простой пример, как в C++ проверить, что что-то лежит внутри optional и получить значение:
if (optional_value != nullopt) {
auto value = *optional_value; // используем "небезопасную" штуку без проверки, но мы же молодцы, мы проверили
std::cout << value << std::endl;
}
Если это в лоб переписать на Rust, получим:
if optional_value.is_some() {
let value = unsafe { optional_value.unwrap_unchecked() };
println!("{}", value);
}
И когда я получал ответ на свой вопрос, мне казалось что как-то так оно и работает и люди реально пишут такой код

Но госпади спасибо разрабам раста конечно же никто так не пишет. Вместо этого, в Rust вы пытаетесь показать свою идею через систему типов. В данном случае вы хотите показать, что "вот тут я проверил Option, и 100% у меня лежит там значение" <=> вы бы хотели вместо Option<T> иметь тип T. И вместо того, чтобы держать это все в голове/комментариях (как в случае C++), мы выражаем это в типах, и перекладываем ответственность следить за типами на компилятор:
if let Some(value) = optional_value {
// typeof(value) == T
println!("{}", value);
}

В первую очередь мне нравится то, что это тупо короче и читаемее (имхо). Мы буквально написали "если в Option лежит что-то, то дай значение, иначе пропусти ветку" в одну строчку.
Но помимо этого, как я говорил, мы переложили ответственность следить за типами на компилятор. Поэтому, например, если кто-то удалит строчки с if (или переместит/скопирует тело if), то первые два варианта нормально скомпилируются, но последний - нет (т.к. у вас просто нет переменной value)

Лично я называю это "писать код в стиле Rust". С одной стороны - по сути это просто синтаксический сахар, реальной разницы после компиляции (вроде бы) нет. Но с другой стороны, последний вариант читаемее и безопаснее. Иногда у меня возникает ощущение, что некоторые люди очень поверхностно изучают Rust и пытаются писать "как на плюсах", расстраиваются, и возвращаются обратно. Но если бы они приложили больше усилий, то вполне возможно, их мнение было бы совсем другим.
👍6😁1🤔1
Panic! At the 0xC0D3
Еще когда я даже не учил Rust, но спрашивал у знающих знакомых "как работает XXX", меня часто не удовлетворял ответ: казалось, что для того, чтобы писать такой же эффективный код на Rust, как и на C++, мне бы пришлось постоянно использовать unsafe. Возьмем…
Когда выложил этот пост, скинули статью, которая довольно хорошо объясняет подход "писать код в стиле Rust"/"type driven design"
В статье все примеры кода на хаскеле, но параллельно с этим объясняется, что он делает, поэтому я не зная хаскеля в целом все смог понять
https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
Не знаю, насколько много людей отсюда ее прочитают, но все таки решил поделиться
👍2
Panic! At the 0xC0D3
Когда выложил этот пост, скинули статью, которая довольно хорошо объясняет подход "писать код в стиле Rust"/"type driven design" В статье все примеры кода на хаскеле, но параллельно с этим объясняется, что он делает, поэтому я не зная хаскеля в целом все смог…
А поделиться я ей решил, потому что ее суть напомнила мне об одной мысли из какого-то древнего доклада, который я не могу откопать теперь(
Если верить моей памяти, то суть его была в том, что чуваку досталась какая-то легаси кодовая база с кучей багосов, и он решил поанализировать, какого рода баги там есть
И пришел к тому, что бОльшая часть самых неприятных ошибок, это... условия. Конкретно одинаковые по смыслу условия, разбросанные по коду
Ну например, вы проверяете, что строка начинается с какого-то префикса при обработке запроса, и в какой-то внутренней функции
Основная проблема в том, что код постоянно меняется, и вы, например, поменяете префикс в одном месте, но не в другом, хоп и получили баг

По сути, есть разные категории ошибок: проезды по памяти, гонки, логические ошибки и прочее. И многие ошибки мы умеем довольно быстро находить: (safe) Rust не даст вам скомпилировать код с проездом по памяти или гонкой, всякие санитайзеры помогут найти в C++
Но вот логические ошибки, это не поймать инструментами особо. Самый лучший инструмент - это написание тестов, что как мы все знаем, довольно часто игнорируется.

Еще после просмотра того доклада, я как-то начал относиться к любым "логическим" ifам с осторожностью: по сути, каждый из них может быть причиной бага.
И вот как раз этот подход из статьи выше, как мне кажется, помогает минимизировать количество таких ошибок: нужно пытаться выносить валидацию ("логические" проверки) в одно место, и после этого в системе типов как бы помечать этот объект "провалидированым", а в функциях и прочем уже использовать этот провалидированный тип

В статье в целом есть хорошие примеры, но я все равно приведу еще один: допустим, мы пишем какую-то библиотеку с математическими функциями, и многие функции могут принимать только положительные числа.
Если это целочисленные типы, то у нас уже(!) есть встроенное решение: unsized int и его вариации:
fn sqrt(val: u32) -> u32
Все, нам не нужны никакие проверки внутри функции, система типов гарантирует это. И я думаю, что вы согласитесь, что да, это разумное решение и у него нет особо минусов.
Что делать, если мы получаем число от пользователя (из json реквеста, ввод с клавиатуры, не важно)? Провалидировать его как можно раньше и засунуть в unsigned int тип, а дальше работать только с ним.
Забавно то, что если вы начнете писать библиотеку в такой идеологии, то все функции такого рода будут принимать uint, а следовательно они как бы будут заставлять вас провалидировать значение как можно раньше:
fn sqrt(val: u32) -> u32 // базовая функция
fn round_sqrt(val: u32) -> u32 // какая-то функция, которая использует первую
fn handle(val: String) { // наш обработчик
let res = round_sqrt(val???); // хотим вызвать "сложную" функцию, но она сразу требует провалидированный тип
}

Но что если мы хотим еще и работать с числами с плавающей точкой? В большинстве языков нет встроенного типа для этого. Дак давайте сделаем свой!
pub struct uf32 {
val: f32, // приватное
}
fn new(val: f32) -> uf32 {
if val < 0.0 {
panic!("expected positive value"); // или, еще лучше, можно возвращать ошибку
}
return uf32{ val };
}
unsafe fn new_unchecked(val: f32) -> uf32 { // явно помечаем, что это небезопасно
return uf32{ val };
}
// арифм операции и прочее
По сути, это тоже самое, что и uint: чтобы его получить, (по хорошему) нужно проверить, что значение действительно соответсвует типу. И после этого, можно его использовать уже без всяких проверок
fn sqrt(val: uf32) -> uf32; // без проверок
👍3
Panic! At the 0xC0D3
А поделиться я ей решил, потому что ее суть напомнила мне об одной мысли из какого-то древнего доклада, который я не могу откопать теперь( Если верить моей памяти, то суть его была в том, что чуваку досталась какая-то легаси кодовая база с кучей багосов, и…
По началу это может показаться странным. "Да никто так не делает", "это какой-то оверинжиниринг". Но по факту - это тот же uint, но ufloat, а первый используют повсеместно, чем этот тип хуже?
И на самом деле, нет, это используют, но больше в функциональных языках: вспомнить NonEmptyList из статьи из хаскеля.
"Но мой набор легаси библиотек не умеет с этим работать!" - возможно. Но это не мешает писать новые библиотеки в таком стиле.
И тут как раз хочется вернуться к Rust: в стандартной библиотеке с давних времен есть "стандартные" решения подобных проблем: Option; Result; Box (unique_ptr), который не может быть null - ровно по этой причине. И из-за того, что это было все (долгое?) время с языком, все библиотеки активно это используют

Есть ли перформанс оверхед от этого? It depends
Например, в случае uf32, вы наоборот можете выиграть в перформансе, не проверяя на отрицательные числа в каждой функции.
"Но я могу просто не проверять, и перенести это на плечи разработчика": да, но либо вам итак нужно будет проверять это (если это ввод от пользователя), и тогда разницы нет, будете вы проверять это в конструкторе, или внутри обработчика; либо вы на 100% уверены, что все ок, и тогда можно воспользоваться unsafe (который буквально показывает, что это небезопасно)
В случае NonEmptyList (NonEmptyVec) перформанс действительно может(!) быть хуже, потому что последовательная память, кеши и все такое.
Но это довольно простые примеры.
В реальности, зачастую это намного более сложные типы, которые показывают какие-то сложные инварианты. Самый банальный пример: парсить json в структуру, и использовать ее везде, вместо использования абстрактного json::Value, который бы пришлось валидировать в каждом месте (да да я смотрю на тебя python)

Но в итоге, использовать библиотеки, написанные в type driver design намного намного приятнее и проще. Одни флешбеки с numpy функций после курса МО, в которых нужно заглянуть в документацию, чтобы увидеть value: int/float/array/list/object/ndarray, передать туда какой-нибудь pandas.Series и получить ошибкой в рантайме спустя 10 минут вычислений, потому что "а этого нет в списке извините", заставляют вздрогнуть.
Вместо этого вам часто даже не нужна документация! Вы пишете foo(bar), и либо оно скомпилировалось и скорее всего работает, либо нет. Помните слова "компилируется - значит работает" про Rust, да? :)

P. S. хотел кратко написать мысли по статье, проиграл
P. P. S. статью то все равно прочитайте!
👍5