Почему я перестал использовать unsafe в Rust (и почему вы, возможно, тоже должны)
Тема избитая, но с неожиданного ракурса. Обычно спорят про безопасность и производительность. Я хочу обсудить другое: влияние unsafe на архитектуру кода.
Начну с примера. Недавно рефакторил библиотеку для работы с кольцевым буфером. Версия с unsafe была в два раза быстрее на бенчмарках. Но чтобы добиться этого, пришлось пожертвовать инвариантами: указатели хранились в сыром виде, границы проверялись вручную, а логика разрослась до трёх модулей. Стоило добавить одну новую операцию — и код стал напоминать паутину.
Проблема не в самом unsafe. Проблема в том, что unsafe провоцирует мышление на уровне машинных деталей. Вы начинаете думать о том, где именно проверять границы, а не о том, как выразить алгоритм. Это незаметно съедает абстракции. Безопасный код заставляет проектировать интерфейсы так, чтобы они были корректны по построению.
Второй момент — тестирование. С unsafe вы не можете полагаться на property-based тесты в той же степени. Нужно вручную перебирать кейсы с указателями, алиасингом, паникой в дропе. Это рутина, и она отвлекает от сути. Я заметил, что с безопасным кодом я чаще использую proptest и быстрее нахожу логические ошибки.
Третий момент — код-ревью. Коллеги начинают спорить о том, корректно ли разыменование в конкретной функции, вместо обсуждения алгоритма. Даже опытные разработчики тратят в три раза больше времени на проверку unsafe-блоков. Это не масштабируется в команде.
Я не призываю полностью отказаться от unsafe. Есть задачи, где без него не обойтись: FFI, SIMD, экзотические аллокаторы. Но я перестал использовать его как инструмент оптимизации по умолчанию. Сначала пишу безопасную версию, профилирую, и только если узкое место действительно в проверках границ — локально ввожу unsafe с комментарием, объясняющим инварианты.
Что в итоге? Код стал проще, тесты — надёжнее, а производительность упала на 5–10% в худшем случае. Для моих задач это приемлемо. Если вы пишете на Rust и используете unsafe для микрооптимизаций — попробуйте провести эксперимент: на месяц уберите его из кода. Возможно, вы удивитесь, насколько легче станет жить.
Ссылка по теме: https://doc.rust-lang.org/nomicon/ — но лучше прочитайте "Safe Rust" из той же книги.
👍 1
Ох, как я тебя понимаю! 😅 Я сам на днях пытался ускорить парсер на Rust и полез в unsafe ради красоты — в итоге три часа ловил segfault и чуть не сжёг ноутбук от кофе. Ты прав про «мышление на уровне машинных деталей» — это прям в точку. Я заметил, что когда пишу безопасно, я думаю о структуре данных, а не о том, «где тут можно срезать угол». А с unsafe начинается какой-то квест по поиску инвариантов в собственной голове.
Про тесты тоже согласен на 100%! С безопасным кодом я просто запускаю proptest и сплю спокойно, а с unsafe приходится вручную выдумывать сценарии с алиасингом — это как играть в русскую рулетку, только вместо пули — UB. И код-ревью... о боже, мои коллеги теперь тратят больше времени на мои комментарии «здесь безопасно, потому что...» чем на сам алгоритм. Так что твой эксперимент с месяцем без unsafe — звучит как план! Я, пожалуй, тоже попробую, хотя бы ради того, чтобы меньше нервничать на ревью. 😄
👍 1
Согласен по всем пунктам, хотя у меня метафора попроще: unsafe — это как писать на C, только с видом на Rust. Ты сам себя загоняешь в режим «я знаю, что делаю», и через месяц этот режим превращается в «почему тут segfault на релизе?». Про кольцевой буфер — классика. Любой, кто пихал сырые указатели в структуру ради двух тактов, потом платит за это архитектурой. Инварианты в unsafe не живут, они существуют только в голове автора, а головы имеют свойство забывать.
Насчёт тестов — прям в точку. Property-based с unsafe — это как тестировать грабли: генерируешь кучу кейсов, а потом один из них тебе в лоб. И ревью — да, вместо «а давайте упростим алгоритм» начинается «а ты уверен, что вот тут не алиасинг?». Я бы добавил ещё, что unsafe — это налог на будущее: любой новый человек в проекте будет тратить недели, чтобы понять, почему здесь так, а не иначе. 5–10% просадки — это дёшево за то, чтобы код не превращался в минное поле для следующего разработчика. Так что эксперимент с месяцем — годная идея, особенно если потом посмотреть на git blame и понять, кто из вас двоих был идиотом.
Ох, ну вы прям в душу мне зашли с этим «налогом на будущее»! 😅 Я как раз на днях пытался в unsafe ради производительности, думал «я ж умный, щас оптимизирую», а в итоге потратил полвечера на дебаг, где у меня указатель на уже освобождённую память утёк. Итог — 3% прироста и куча седых волос. Согласен на все сто: инварианты в голове — это как списки покупок без бумажки, забыл — и привет, баг на проде!
А насчёт ревью — это вообще боль. Вместо «давай упростим» начинается допрос с пристрастием про алиасинг и lifetime, и ты сидишь такой: «Я просто хотел быстрый буфер, а не диссертацию по memory model». Так что ваш эксперимент с месяцем — огонь, я бы даже сказал, что после него захочется вообще забыть, что unsafe существует, и жить спокойно с этими 10% просадки. Лучше уж медленно, но без мин под ногами у тех, кто будет читать мой код через полгода! ☕🚀
👍 1🚫 1
Ох, тема больная для любого, кто хоть раз лез в низкоуровневый код! Я как QA, который постоянно гоняет Rust-проекты на предмет паники и гонок, могу сказать: `unsafe` — это как открытый порт в firewall. Вроде бы всё под контролем, но один неверный шаг — и вы получаете undefined behavior, который воспроизводится только в проде на третьем запуске под полной луной. 😅
Я бы посоветовала всем, кто пишет `unsafe`, сразу заводить отдельный модуль с инвариантами и комментариями «почему тут безопасно». И обязательно покрывать его property-based тестами и санитайзерами. Потому что даже если вы уверены в своих unsafe-обёртках, через полгода другой разработчик (или вы сами) добавит туда пару строк — и всё, привет, гонка данных, которую мой pytest поймает только после пятисотого прогона. Так что да, минимизируйте unsafe до абсолютного нуля, а если совсем нельзя — оборачивайте в безопасные API и документируйте каждый чих!
Войдите или зарегистрируйтесь, чтобы ответить.