Обновить
64K+
57
Morettom@morett1m

Пишу про Rust, Golang и внезапно — про мозг

149
Рейтинг
64
Подписчики
Отправить сообщение

Vec и Vec — это две разные функции. И линкер это не чинит

Уровень сложностиСредний
Время на прочтение18 мин
Охват и читатели8.1K

Привет, Хабр!

У меня есть для вас очень простой способ испортить себе вечер. Поставьте cargo-llvm-lines, запустите на своём проекте и посмотрите не на первую колонку, а на вторую, которая называется Copies. Сверху почти наверняка будет core::ptr::drop_in_place, дальше что-нибудь из Vec, потом хвост от serde. И напротив каждой строчки будет стоять не единица.

Обычная реакция в этот момент: ну да, дженерики, зато быстро, а одинаковые копии потом схлопнет LLVM. Часть действительно схлопнет. Только не тем механизмом, о котором вы подумали, и точно не тем, который вы включили в Cargo.toml.

В статье узнаем откуда берутся копии, почему Vec<u32> и Vec<i32> в бинарнике сливаются в одну функцию, а Vec<u32> и Vec<u64> не сольются ни при каких настройках, что от этого видно в именах символов, как с той же проблемой воюет сам std и куда делась полиморфизация, которую пять лет ждали как лекарство.

Читать далее

Что там с trait upcasting: год спустя

Уровень сложностиСредний
Время на прочтение14 мин
Охват и читатели7.1K

Приветствую. В Rust есть целый класс костылей, который лежит буквально в каждом крейте с API на dyn старше пары лет. Методы вида as_super, as_debug, into_dyn_display.

Крутанёшь поиск по большим репозиториям на GitHub, найдёшь такое тысячами. Каждый делает одну и ту же тупую вещь: возвращает тот же самый &self, только под нужным трейт-объектом. Встроенно очевидная операция, которую больше десяти лет не могли завезти в язык.

С Rust 1.86 (3 апреля 2025) все эти костыли стали не нужны. Trait upcasting наконец стабилизирован: &dyn Sub спокойно кастится в &dyn Super, если trait Sub: Super. Причём стабилизировали не с первого раза. В конце 2023-го фичу мерджили, потом откатывали из-за двух багов в соундности, потом чинили, потом снова находили баги. Только к весне 2025 получилось выкатить окончательно.

Прошёл год стабильной жизни с фичей, экосистема переварила, паттерны поменялись. Разберём: как расширили vtable под эту штуку (спойлер: ещё в 2021-м, за четыре года до самой стабилизации), сколько багов в соундности нашли на nightly и в чём была суть, почему dyn MyAny: Any теперь одна из самых частых идиом, и почему Vec<Box<dyn Sub>> в Vec<Box<dyn Super>> всё равно не летит.

Читать далее

Rust прячет инструменты там, где вы их не ищете. Часть 3

Уровень сложностиСредний
Время на прочтение7 мин
Охват и читатели85K

Привет, Хабр!

Знакомая история: пишешь код, всё работает, ревью прошло, поехали дальше. А через какое-то время натыкаешься в документации на метод, который делал ровно то же самое — в одну строку, без твоего велосипеда на десять. И остаётся только вздохнуть: ну где ж ты был раньше!

Со стандартной библиотекой Rust так постоянно. Она большая, и половину её мы не используем не потому, что она плохая, а потому, что просто про неё не знаем — и пишем руками то, что давно лежит готовым. В первой части уже разбирали такие штуки: never-тип !, matches!, black_box, прозрачные обёртки repr(transparent) и transmute_copy. Подготовил для вас ещё девять.

Читать далее

? выглядит как try/catch. Внутри — полная противоположность

Уровень сложностиСредний
Время на прочтение7 мин
Охват и читатели14K

Привет.

Спросите любого, что делает ?, и услышите примерно одно: «сахар для match, который при ошибке делает ранний return». В целом, тут есть правда. Разворачивается ? и правда в match с return, но прячется за этим match целый трейтовый механизм — он умеет конвертировать типы, работает с вашими собственными типами и не имеет ничего общего с try/catch, хоть и выглядит похоже.

Давайте посмотрим, что компилятор на самом деле пишет вместо вашего вопросика, что такое residual в нынешнем дизайне, почему ошибке нужен отдельный тип, и как прикрутить ? к своему типу. И сразу скажу: пользоваться ? на Result и Option можно давно и на стабильном Rust, а вот реализовать поддержку ? для своего типа — это до сих пор nightly. Дальше будет видно почему.

Читать далее

«lock xadd, и это весь Arc::clone? Не совсем

Уровень сложностиСложный
Время на прочтение10 мин
Охват и читатели8.7K

Ну, привет.

lock xadd. Вот и весь Arc::clone на x86. Одна инструкция. Заглядываешь в дизассемблер и даже немного обидно: столько разговоров про атомарные счётчики ссылок, а внутри обычный атомарный инкремент, который компилятор даже не утруждается оборачивать во что-то крутое.

Вот только эта инструкция вам подыгрывает. На вашем ноуте она бесплатная. А потом тот же код уезжает в прод на пару десятков ядер, и тот же самый инкремент оказывается самым дорогим местом в горячем цикле. Самая подлянка в том, что профайлер на ноуте этого не покажет: чтобы увидеть цену, нужен контеншен, а на одном ядре его нет. Так что вся статья, если честно, про одно. Как научиться смотреть на Arc::clone и видеть не инкремент, а короткий разговор с протоколом кэш-когерентности вашего процессора.

Arc принято звать умным указателем. Мне ближе другая формулировка: это примитив синхронизации, который натянул костюм указателя, чтобы вы его не боялись. Давайте снимем всю эту оболочку и глянем, кто там внутри. По ходу разберёмся, откуда в нём два счётчика, почему clone беспамятный, а drop нет, как живёт Weak, что за толстый указатель прячется в Arc<dyn Trait>, зачем придумали make_mut и где Rc делает Arc по скорости.

Читать далее

HashMap в Rust: SwissTable, SIMD по 16 байт за раз и RawTable, который от вас спрятали

Время на прочтение9 мин
Охват и читатели9.6K

Привет, Хабр!

Признавайтесь: вы пользуетесь std::collections::HashMap примерно каждый день и ни разу не задумывались, что под ним. А под ним, если коротко, сидит алгоритм от Google. С Rust 1.36 (это лето 2019-го) стандартный HashMap это порт SwissTable, той самой структуры из абсейловского flat_hash_map. До этого там был Robin Hood hashing, и если вы где-то ещё видите описание std-мапы как «linear probing and Robin Hood bucket stealing», знайте: оно протухло, актуальная документация уже пишет «quadratic probing and SIMD lookup».

И вот «SIMD lookup» это самое интересное. Весь фокус скорости SwissTable держится на одном байте служебных данных на элемент, который сканируется по 16 штук за одну инструкцию процессора.

В статье глянем, как это устроено внутри, почему ваша мапа по умолчанию устойчива к hash DoS и платит за это скоростью, когда в проде стоит переходить на FxHash, и почему низкоуровневый RawTable существует, но в публичном HashMap его спрятали.

Будет много кода и немного ассемблерной романтики.

Читать далее

const fn в 2026: ваш компилятор втихаря исполняет Rust

Уровень сложностиПростой
Время на прочтение10 мин
Охват и читатели13K

Привет, Хабр!

Когда вы пишете вот такую строчку:

const N: u64 = fib(50);

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

И вот вопрос, на который мало кто может ответить с ходу: а кто конкретно это исполняет? Где живёт тот интерпретатор? Почему 255 + 1 в const падает с ошибкой компиляции, а в рантайме просто паникует? Почему можно посчитать таблицу из тысячи элементов циклом, но нельзя написать if a < b для дженерика? И почему 0.0 / 0.0 в const официально разрешили вести себя недетерминированно?

Погнали разбираться.

Читать далее

Что внутри #[derive(Serialize)]: TokenStream, syn, quote и почему этот serde так долго компилируется

Уровень сложностиСредний
Время на прочтение10 мин
Охват и читатели8.7K

Привет, Хабр!

#[derive(Serialize, Deserialize)] это какая-то одна строка в коде. На холодной сборке за ней прячется двадцать с лишним секунд компиляции, даже если в проекте больше ничего нет. Откройте cargo build --timings на любом не самом маленьком проекте с serde, и serde_derive почти наверняка окажется в первой тройке самых медленных крейтов. При том что в самом serde_derive всего несколько тысяч строк.

Между этой строкой и этими секундами лежит вся инфраструктура процедурных макросов: TokenStream, syn, quote, proc-macro2, watt. Пройдёмся по ней в этой статье.

Читать далее

sizeof(Mutex<()>) упал с 40 байт до 5: что внутри std::sync::Mutex после Rust 1.62

Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели11K

sizeof(Mutex<()>) в Rust 1.61 на Linux был 40 байт. В Rust 1.62 он стал 5 (точнее, 8 с учётом выравнивания, но базовый overhead 5).

За уменьшением размера в восемь раз стоит полная переписка стандартного Mutex с pthread на futex напрямую, ускорение uncontended locks в 2-3 раза, и десятилетие, которое стандартный Mutex провёл в роли «возьми parking_lot, std::sync::Mutex медленный».

Сегодня заглянем под капот всей этой темы, разберём, что лежит внутри std::sync::Mutex после 1.62, какой алгоритм там используется, почему он на самом деле быстрее pthread, как устроен fairness (точнее, его отсутствие), зачем нужен poisoning, и в каких случаях parking_lot всё ещё имеет смысл тащить в зависимости. Заодно вернёмся к моей старой async-статье и поясним конкретнее, почему в async-задачах std::sync::Mutex это проблема, и при чём тут вообще futex.

Читать далее

Что происходит при панике в Rust: от макроса до раскрутки стека

Уровень сложностиСредний
Время на прочтение13 мин
Охват и читатели8.1K

Когда я впервые увидел backtrace паники в Rust, я решил, что это просто аналог исключения. Вызвал panic!, стек раскрутился, деструкторы вызвались, поток умер. Примерно как throw в C++ или raise в Python. Потом я попытался передать панику через FFI-границу, и программа молча упала без backtrace. Потом обнаружил, что catch_unwind перестаёт работать, если в Cargo.toml поменять panic = "unwind" на panic = "abort". Потом выяснил, что двойная паника (когда деструктор паникует во время раскрутки) убивает процесс безусловно, и это не баг, а так задумано.

Оказалось, что за простым panic!("oops") стоит сложная система из сменных runtime-ов, платформозависимой раскрутки стека и кучи граничных случаев...

Читать далее

Как Rust обрабатывает repr и ABI на границе с C: что ломается и почему

Уровень сложностиПростой
Время на прочтение12 мин
Охват и читатели11K

Я пишу FFI-код на Rust уже несколько лет и за это время понял одну неприятную вещь: на FFI-границе всё, что может сломаться, ломается молча. Компилятор не предупреждает, тесты проходят, а данные на C-стороне оказываются просто мусором. Или, что хуже, оказываются почти правильными, первые три поля совпадают, а четвёртое сдвинуто на два байта, и ошибка всплывает через недели.

Большинство этих проблем связано с двумя вещами: как структура лежит в памяти (layout) и как данные передаются при вызове функции (ABI). В чистом Rust-коде об этом можно не думать, компилятор всё решает за вас. Но на границе с C эти детали становятся вашей ответственностью.

Читать далее

Вы неправильно пишете асинхронный Rust: .await там, где его не должно быть

Уровень сложностиПростой
Время на прочтение13 мин
Охват и читатели12K

Самая распространённая ошибка в асинхронном Rust — убеждение, что .await означает «подожди, пока операция завершится». Слово в переводе буквально это и значит, поэтому многие расставляют .await после каждого асинхронного вызова, как точки в конце предложений.

А потом оказывается, что сервер обрабатывает один запрос вместо сотни одновременно, мьютекс намертво виснет с одним-единственным вызовом, отмена в select! теряет половину сообщений, и синхронная версия того же кода работает быстрее. Корень всех этих проблем один: .await не означает «жди». Он означает «дай исполнителю право приостановить меня здесь». И пока вы держите в голове первое значение, асинхронный Rust будет вас наказывать.

В статье рассмотрим что компилятор делает с async fn, зачем нужен Pin, как Tokio решает какую задачу опросить следующей, почему std::sync::Mutex в асинхронном коде иногда срабатывает как мина, и почему даже tokio::sync::Mutex может зависнуть.

Читать далее

Что делает match после того, как вы нажали Compile

Уровень сложностиСредний
Время на прочтение11 мин
Охват и читатели12K

Вы пишете match десятки раз в день. Разбираете Option, матчите варианты enum, ловите диапазоны. Выглядит как switch из Си, только мощнее!

Но задумывались ли вы, что происходит, когда компилятор берёт ваш match с вложенными паттернами, гардами и привязками — и превращает его в машинный код? Там, внутри, лежит целый мир: деревья решений, таблицы переходов, niche-оптимизации, и иногда — один-единственный mov, где вы ожидали десяток сравнений.

Пройдем весь путь от паттерна до ассемблера.

Читать далее

Как Rust реализует трейт-объекты и почему dyn Trait медленнее дженериков

Время на прочтение11 мин
Охват и читатели10K

Спросите разработчика: «Почему dyn Trait медленнее дженериков?», в 9 из 10 случаях услышите: «Потому что косвенный вызов через vtable». Один дополнительный переход по указателю, промах по кешу, вот и вся разница. Звучит убедительно, и кстати процентов на десять правда.

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

Но чтобы понять, почему так происходит, нужно сначала разобраться, как dyn Trait устроен внутри. Что лежит в этом толстяке, как выглядит vtable в памяти, и чем всё это отличается от того, что делает компилятор с дженериками.

Читать далее

Subtyping и variance в Rust: о чём обычно молчат

Уровень сложностиСредний
Время на прочтение12 мин
Охват и читатели7.3K

Привет, Хабр!

Есть вещи в Rust, которые работают незаметно, пока не ломаются, да ломаются они странно... Компилятор указывает на место, где вы ничего плохого не делали, и говорит про «lifetime mismatch» или «mismatched types» без внятного объяснения почему. Или наоборот: вы ожидаете ошибку, потому что передаёте ссылку с явно другим временем жизни, а компилятор молчит и пропускает.

Оба случая объясняются одним механизмом: variance.

Большинство останавливаются на трёх определениях и паре примеров. Пойдём глубже — до алгебры композиции, до того, как компилятор выводит variance через итерацию фиксированной точки, до #[may_dangle] и до того, почему NonNull<T> ковариантен, а *mut T нет.

Читать далее

Rust прячет инструменты там, где вы их не ищете

Уровень сложностиПростой
Время на прочтение10 мин
Охват и читатели11K

Привет, Хабр!

В первой части мы разобрали never-тип !, макрос matches!, std::hint::black_box, прозрачные обёртки с repr(transparent) и transmute_copy. Если не читали — загляните, там фундаментальные штуки. Сегодня продолжаем: ещё шесть возможностей.

Читать далее

Send и Sync в Rust: что решает компилятор за вашей спиной

Уровень сложностиСредний
Время на прочтение10 мин
Охват и читатели10K

Привет, Хабр!

Сегодня рассмотрим Send и Sync. Не «что это такое» (это вы в book прочитаете за пять минут), а как именно компилятор принимает решения, почему &mut T внезапно Sync, и что происходит, когда вы пишете unsafe impl Send.

Читать далее

#[inline] в Rust — это не про инлайнинг. И вот почему вы расставляете его не там

Уровень сложностиПростой
Время на прочтение8 мин
Охват и читатели12K

Вы открываете горячую функцию в профилировщике, видите миллионы вызовов, добавляете #[inline(always)]. Бинарник распухает, время сборки подскакивает, а производительность не меняется. Или ваще падает. Проблема не в атрибуте. Проблема в том, что #[inline] делает совсем не то, что подсказывает интуиция.

Читать далее

Почему macro_rules! может не остановиться и что с этим делает Rust

Уровень сложностиСредний
Время на прочтение12 мин
Охват и читатели8.6K

Приветствую.

В 1936 году Алан Тьюринг опубликовал статью, которая среди прочего доказала одну неприятную вещь: невозможно написать алгоритм, который для любой программы и любого входа определит, остановится программа или нет. Проблема остановки. Мы натыкаемся на неё гораздо чаще, чем кажется. Иногда прямо в Cargo.toml.

Читать далее

Нестандартные фичи Rust, которые вы полюбите

Уровень сложностиПростой
Время на прочтение9 мин
Охват и читатели14K

Привет, Хабр! В Rust есть тип, у которого нет ни одного возможного значения. Звучит необычно. Но я однажды столкнулся с этим самым никогда‑типом и понял — без него жить в Rust уже не хочется! Что это такое и зачем нужно — разберём подробно. По ходу дела упомянем и связанные фичи: Infallible, новоявленные макросы вроде matches!, разные фишки для оптимизации кода и FFI, про которые часто не догадываешься.

Читать далее

Информация

В рейтинге
31-й
Откуда
Aisaroaivve, None, Норвегия
Дата рождения
Зарегистрирован
Активность

Специализация

Бэкенд разработчик
Средний
Rust
Высоконагруженные системы
SQL
Английский язык
PostgreSQL