Вопросы по unsafe и производительности Rust на собеседовании
Блок отделяет тех, кто читал про язык, от тех, кто отлаживал его в проде. Здесь проверяют, понимаешь ли ты, что unsafe не выключает проверки заимствования, а лишь разрешает пять конкретных операций, и умеешь ли ты доказывать выигрыш замером, а не ощущением.
Что спрашивают
- +unsafe: какие пять операций разрешает, почему это не «выключить компилятор», роль безопасной обёртки
- +Сырые указатели: чем отличаются от ссылок, что такое UB и почему его нельзя «проверить тестом»
- +FFI: repr(C) и стабильная раскладка, передача строк через границу, кто владеет памятью и кто её освобождает
- +Память: size_of и выравнивание, packed и его цена, оптимизация ниш в Option<&T>
- +Скорость: что значит zero-cost, итераторы против индексов, границы массива и когда проверка исчезает
- +Замеры: criterion и разогрев, black_box, профиль вместо догадки, чем бенчмарк отличается от профилирования
Из чего состоит тема
Так тема разложена в тренажёре: движок ведёт прогресс по каждой подтеме отдельно и возвращает те, где вы ошибаетесь.
- Оптимизация и zero-cost14
- unsafe: правила12
- Раскладка памяти12
- Сырые указатели и UB12
- FFI и repr(C)11
- Бенчмарки и профилирование11
Разборы подтем
Конспект по каждой: что это, как отвечать вслух, на чём валятся, плюс вопросы для самопроверки.
- Сырые указатели и UB в Rust12 вопросов
- FFI и repr(C) в Rust11 вопросов
- Раскладка памяти в Rust12 вопросов
- Zero-cost абстракции в Rust14 вопросов
- Бенчмарки и профилирование в Rust11 вопросов
Примеры вопросов с разбором
- Почему бессмысленно измерять производительность на debug-сборке?A)В debug добавлены счётчики времени, искажающие замерB)Debug использует другой аллокатор памятиC)Оптимизации выключены, разница с release — десятки разD)В debug код выполняется интерпретатором, а не машинными инструкциями
показать ответ и разбор
+C)Оптимизации выключены, разница с release — десятки раз// разбор: В debug нет инлайнинга и оптимизаций, зато есть проверки переполнения — абстракции, бесплатные в release, здесь стоят реальных денег. Замеры дают не просто смещённые числа, а другой порядок и другой профиль: горячим окажется совсем не то место. Мерить нужно release, а лучше тем же профилем, что уходит в прод.
- Зачем структуре, уезжающей в C-библиотеку, атрибут #[repr(C)]?A)Он переводит имена полей в кодировку, понятную CB)Он запрещает компилятору добавлять байты выравнивания между полямиC)Он делает структуру переносимой между языками через сериализациюD)Он фиксирует порядок полей и выравнивание как в C
показать ответ и разбор
+D)Он фиксирует порядок полей и выравнивание как в C// разбор: Раскладка по умолчанию не определена: компилятор вправе переставлять поля, чтобы уплотнить структуру. Для обмена с C это недопустимо, поэтому repr(C) фиксирует порядок объявления и выравнивание по правилам C. Убирает промежутки другой атрибут — repr(packed), и он опасен: чтение невыровненного поля стоит дороже, а ссылку на него брать нельзя.
- Структура { a: u8, b: u32, c: u8 } занимает 8 байт, а с #[repr(C)] — 12. Почему?A)repr(C) выравнивает структуру по границе 16 байтB)repr(C) добавляет заголовок с информацией о типеC)Без repr поля хранятся упакованно, без выравниванияD)Без repr компилятор переставил поля и уплотнил раскладку
показать ответ и разбор
+D)Без repr компилятор переставил поля и уплотнил раскладку// разбор: В раскладке по умолчанию порядок полей не гарантирован: компилятор ставит u32 первым, два u8 — следом, и всё влезает в 8 байт. repr(C) обязан сохранить порядок объявления, поэтому между a и b появляются три байта выравнивания, а в конце — ещё три. Выигрыш обычной раскладки — как раз в свободе перестановки.
- Что означает лозунг zero-cost abstractions?A)Абстракции не занимают память в рантаймеB)Код на Rust работает быстрее аналога на CC)Компилятор убирает неиспользуемые абстракции из бинарникаD)Абстракция не дороже такого же кода, написанного руками
показать ответ и разбор
+D)Абстракция не дороже такого же кода, написанного руками// разбор: Формулировка Страуструпа: за то, чем не пользуешься, не платишь, а тем, чем пользуешься, не написал бы лучше руками. Итератор с map и filter разворачивается в тот же цикл, дженерик мономорфизируется в прямые вызовы, Option<Box<T>> занимает столько же, сколько указатель. Но нулевая цена не универсальна: dyn Trait, Rc и RefCell стоят вполне заметно.
- Какая операция с сырым указателем требует unsafe?A)Разыменование: создать и передать можно и без unsafeB)Печать адреса через Debug-форматированиеC)Создание из ссылки: именно тут теряются гарантииD)Сравнение двух указателей на равенство
показать ответ и разбор
+A)Разыменование: создать и передать можно и без unsafe// разбор: Пока указатель просто лежит в переменной, ничего опасного не происходит: его можно создать из ссылки, скопировать, сравнить, напечатать. Опасно обращение по нему — компилятор больше не знает, жив ли объект и не занят ли он кем-то ещё. Поэтому unsafe требуется ровно на разыменовании.
- Что именно разрешает блок unsafe?A)Действия с памятью в обход проверок компилятораB)Пять операций: сырые указатели, unsafe-функции, static mut, union, трейтыC)Обход правил заимствования там, где программист уверен в корректностиD)Прямой доступ к ассемблеру и системным вызовам ядра
показать ответ и разбор
+B)Пять операций: сырые указатели, unsafe-функции, static mut, union, трейты// разбор: Список закрытый: разыменовать сырой указатель, вызвать unsafe-функцию или функцию из FFI, обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Всё остальное работает как обычно — проверка типов, владение и заимствование никуда не деваются. Отсюда правило: чем короче блок, тем точнее видно, что именно взято под ответственность.
- Бенчмарк показал ноль наносекунд на вычислении. Что случилось?A)Функция была выполнена на этапе компиляции как constB)Результат не использован — оптимизатор выбросил вычислениеC)Компилятор закэшировал результат между итерациями циклаD)Таймер не видит интервалов короче микросекунды
показать ответ и разбор
+B)Результат не использован — оптимизатор выбросил вычисление// разбор: Классика микробенчмарков: значение никому не нужно, входные данные — константы, всё сворачивается ещё при сборке. Лечится black_box вокруг входа и результата: он прячет значения от оптимизатора. Именно поэтому берут criterion — там прогрев, статистика по множеству итераций и защита от свёртки уже встроены.
- Почему String нельзя передать в C-функцию как есть?A)String лежит в куче Rust, а C ожидает данные на стекеB)String хранит текст в UTF-16, C — в однобайтовой кодировкеC)У C-строки нужен завершающий ноль, а String хранит длину отдельноD)Передача String нарушает владение: C освободит память чужим аллокатором
показать ответ и разбор
+C)У C-строки нужен завершающий ноль, а String хранит длину отдельно// разбор: String — это указатель, длина и ёмкость без завершающего нуля, а C ждёт указатель на последовательность байт, оканчивающуюся нулём. Для перехода есть CString (владеющая, с нулём на конце) и CStr (заимствованный вид). Дополнительная тонкость: внутри строки Rust ноль может встретиться, и тогда преобразование вернёт ошибку.
- Чем опасен #[repr(packed)]?A)Структура перестаёт работать с derive-макросамиB)Поля переупорядочиваются непредсказуемо между сборкамиC)Поля перестают быть выровненными, ссылку на них не взятьD)Компилятор запрещает класть такую структуру в Vec
показать ответ и разбор
+C)Поля перестают быть выровненными, ссылку на них не взять// разбор: Упаковка убирает промежутки, и поле u32 может оказаться по нечётному адресу. Ссылка обязана быть выровненной, поэтому &packed.field — ошибка компиляции; читать можно только копированием значения. На части архитектур невыровненный доступ ещё и дороже. Оправдано это в разборе бинарных форматов и протоколов, где раскладка задана извне.
это 9 из 72
Ещё 63 вопросов по теме — в тренажёре, с движком повторения
Прочитать разбор и ответить самому — разные навыки. В Сеньорчике вопросы идут сессиями, а движок возвращает подтемы, где вы ошибаетесь, пока они не начнут отскакивать. Бесплатно, лимит по энергии.
Частые вопросы
Что именно разрешает unsafe?
Пять вещей: разыменовать сырой указатель, вызвать unsafe-функцию, обратиться к изменяемой статике, реализовать unsafe-трейт и прочитать поле union. Проверки заимствования и типов продолжают работать — unsafe снимает не их, а запрет на эти операции.
Почему бенчмарк показал нулевое время?
Компилятор увидел, что результат никому не нужен, и выбросил вычисление целиком. Поэтому значение прогоняют через black_box, а сам замер делают на прогретом цикле с достаточным числом итераций, иначе меряется шум планировщика.
Действительно ли абстракции бесплатны?
Для итераторов и дженериков — как правило да: мономорфизация и инлайн дают код, сравнимый с ручным циклом, и проверка границ часто исчезает. Но dyn Trait, Box в горячем пути и Arc с атомарным счётчиком стоят вполне измеримо, и на собесе ждут именно этого уточнения.