AV1 становится всё более значимым видеоформатом, которому требуется безопасный и производительный декодер. Исходя из этой идеи, мы в тандеме с командой из Immutant создалиrav1d
, портировав на Rust написанный на С декодерdav1d
. Перед вами первая из двух статей, посвящённых решению этой задачи.
— Джош Аас, глава проекта Prossimo организации ISRG
В современном мире программного обеспечения парсинг сложных данных в плане безопасности является одной из наиболее критических операций. Браузерам нужно декодировать недоверенный ввод аудио/видео, кодируемый в реальном времени в крайне сложные форматы. В итоге при декодировании баги, связанные с безопасностью памяти, становятся не только чудовищными, но и частыми. Например, исследователи реализаций декодера H.264, продемонстрировали, что они являются опасными источниками багов. AV1 тоже является сложным и широко используемым форматом. И чтобы избежать уязвимостей в активно используемых программах вроде браузеров, нам нужна безопасная по памяти и производительная реализация парсинга этого формата.
Для создания этого быстрого и безопасного декодера AV1, который получил название
rav1d
, мы портировали существующую библиотеку декодирования dav1d
с её нативного языка C на Rust. Наша реализация полностью совместима с API dav1d
. Парсеры форматов, которые были написаны на небезопасном С, теперь стали безопасными по памяти реализациями на Rust. Для сохранения быстродействия мы оставили в коде (небезопасные) нативные процедуры ассемблера, которые выполняют низкоуровневые операции декодирования. Эти процедуры работают преимущественно с буферами примитивных значений, используя проверенные данные из кода парсинга, реализованного на Rust. Как правило, наиболее эксплуатируемые баги находятся в высокоуровневом коде парсинга форматов, а не в низкоуровневых операциях обработки данных. Мы продолжим разбирать и анализировать процедуры ассемблера для устранения багов с повреждением памяти на этом уровне.
В отношении реализации
rav1d
мы поставили перед собой такие цели:- Перенести код
dav1d
на Rust для повышения безопасности памяти. - Добиться полной совместимости с API языка C.
- Добиться производительности, сравнимой с реализацией на С.
- Повторно использовать код из
dav1d
, чтобы упростить его частую синхронизацию. - Обеспечить поддержку X86-64 и ARM64.
Наш подход к миграции
Написать высокопроизводительный и полноценный декодер AV1 с нуля сложно. Для этого нужно хорошо разбираться в стандарте AV1 и предметно понимать, как лучше реализовать формат кодека, чтобы он получился и быстрым, и совместимым.
Реализация
dav1d
, развиваемая сообществами VideoLAN и FFmpeg при спонсорской поддержке Alliance for Open Media, разрабатывается уже 6 лет. Она содержит около 50 К строк С и 250 К строк ассемблера. Это зрелая, быстрая и широко распространённая реализация. И вместо того, чтобы пытаться заново создать аналогичный декодер на Rust, мы предпочли перенести на этот язык существующий код dav1d
. Мы хотим предоставить совместимый с С API, чтобы сделать портирование кода в новую библиотеку на Rust более плавным. Мы также хотим повторно использовать код ассемблера
dav1d
для сохранения быстродействия. Эти ограничения в плане совместимости означают, что мы должны частично сохранить внутреннюю и внешнюю структуру данных dav1d
. Кроме того, нам нужно вызывать процедуры ассемблера в то же время, когда это делает dav1d
. С функциональной точки зрения это требует реализации декодирования практически тем же образом, каким оно реализовано в dav1d
. Мы могли бы вручную переписать
dav1d
на Rust по одной функции или модулю за раз. Однако, учитывая необходимость сохранения совместимости, это бы стало очень утомительной задачей. Чтобы достичь точки, где мы могли бы вносить во внутренние структуры данных сквозные изменения, необходимые для повышения безопасности памяти, потребуется переписать значительную часть базы кода. Вместо этого мы решили использовать c2rust
, чтобы изначально транспилировать код С в равнозначный, но небезопасный код Rust. Это позволило нам начать переписывание с полностью рабочей и легко совместимой базы кода Rust, не внося в процессе новые логические баги. В итоге основная часть работы заключалась в ручном рефакторинге и переписывании небезопасного кода Rust в его безопасную, идеоматичную форму.Транспиляция в небезопасный Rust с последующим переписыванием обеспечила для нашего проекта два важных преимущества: 1) начало с полностью рабочей реализации на Rust позволило нам тщательно протестировать функциональность декодирования, производя поэтапный рефакторинг; 2) транспиляция сложной логики декодирования сократила потребность в предметном знании спецификации AV1.
В результате оказалось очень полезным проводить полноценное тестирование непрерывной интеграции с самого начала, параллельно переписывая и дорабатывая код Rust. Мы могли вносить сквозные изменения в базу кода и проводить существующие тесты
dav1d
при каждом коммите. Между статической проверкой компилятора Rust и интеграционными тестами всего декодера мы проводили сравнительно меньше времени за отладкой, чем провели бы в случае реализации декодера с нуля. Основную часть команды этого проекта составляли эксперты по системному программированию и Rust, но у них не было столь важного опыта работы с кодеками AV. Наш эксперт по кодекам, Фрэнк Боссен, предоставил бесценные рекомендации, но в основную работу вовлечён не был.Когда мы впервые приступили к реализации проекта, то предположили, что он займёт около 7 месяцев. Но в итоге стало ясно, что придётся вложить в него намного больше ручного труда, чем предполагалось. В общей сложности наша команда из 3 разработчиков затратила более 20 человеко-месяцев усилий. Процесс переписывания, особенно в попытке сохранить производительность, оказался намного более трудозатратным, чем ожидалось. Мы столкнулись с рядом проблем, связанных со своеобразностью базы кода
dav1d
и инструментом транспиляции c2rust
. Например, структура кода dav1d
привела к значительному дублированию трансилированного кода Rust для 8- и 16-битных изображений, который нам пришлось вручную унифицировать и удалять повторы. Взаимодействие с существующим кодом ассемблера безопасным и эргономичным способом с сохранением быстродействия потребовало серьёзных усилий и внимательности. Многие из других сложностей оказались больше связаны с переносом кода С на Rust, поэтому текущая статья будет акцентироваться именно на этих проблемах и их решениях.
Сложности
Мы столкнулись с различными проблемами, связанными с расхождениями между паттернами С и безопасного Rust. Управление жизненным циклом требовало подробного понимания существующей базы кода, но в итоге жизненные циклы и заимствование стали не самыми серьёзными проблемами. Безопасность потоков в Rust, которая усложняет обмен изменяемыми данными между рабочими потоками, никак не сочеталась с моделью потоков
dav1d
, где потоки неявно обмениваются практически всеми данными. Кроме того, ощутимыми источниками затруднений стали владение памятью и указатели буферов, а также объединения и прочие небезопасные паттерны C.▍ Потоки
В
dav1d
используется пул рабочих потоков, которые конкурентно выполняют задачи, не зависящие друг от друга. Тем не менее эти задачи воздействуют на общие структуры в глобальном контексте и контексте фрейма, в том числе имеют общий изменяемый доступ к одним и тем же буферам. Например, в листинге 1а показан фрагмент корневой структуры, к которой должны обращаться все потоки. Каждый обрабатываемый фрейм содержит Dav1dFrameContext
, который является общим для работающих с ним потоков. Каждый объект
Dav1dTaskContext
используется только одним потоком, но в С каждый из них посредством корневого Dav1dContext
доступен для всех других потоков. Наконец, этот корневой контекст содержит множество других полей состояний, некоторые из которых должны быть изменяемы основным потоком и считываемы в рабочих потоках.Листинг 1a. Фрагмент из структуры корневого контекста:
struct Dav1dContext {
Dav1dFrameContext *fc;
unsigned n_fc;
Dav1dTaskContext *tc;
unsigned n_tc;
struct Dav1dTileGroup *tile;
int n_tile_data_alloc;
int n_tile_data;
int n_tiles;
// ...
}
Rust требует, чтобы все данные, совместно используемые потоками, были
Sync
. Это означает, что к ним должны иметь возможность безопасно обращаться конкурентно несколько потоков. Нам нужно обмениваться заимствуемым корневым контекстом между всеми потоками, поэтому все данные в этом контексте должны быть иммутабельны. Чтобы сделать возможным изменение общих данных, мы должны ввести блокировки, которые позволят сохранить безопасность потоков в среде выполнения. Для этого мы добавили Mutex
и RwLock
как необходимые. Если предположить, что изначальный код С не имеет состояний data race (в dav1d
мы их не наблюдали), за эти новые блокировки никогда не должно возникать соперничества. Мы активно использовали Mutex::try_lock()
и RwLock::try_read()
/ RwLock::try_write()
, чтобы проверить в среде выполнения, может ли поток безопасно обращаться к данным, не внося задержек из-за ожидания освобождения блокировки.Листинг 1b. Соответствующая выдержка контекста из
rav1d
:pub struct Rav1dContext {
pub(crate) state: Mutex<Rav1dState>,
pub(crate) fc: Box<[Rav1dFrameContext]>,
pub(crate) tc: Box<[Rav1dContextTaskThread]>,
// ...
}
pub struct Rav1dState {
pub(crate) tiles: Vec<rav1dTileGroup>,
pub(crate) n_tiles: c_int,
// ...
}
Как показывает листинг 1b, нам пришлось перестроить структуры
dav1d
, чтобы они лучше вписывались в модель потокобезопасности Rust. Мы отрефакторили изменяемое состояние в новой структуре Rav1dState
и обернули его в мьютекс. Также стоит отметить, что tc
больше не содержит локальных для каждого потока данных, а только хэндл потока и метаданные Sync
для их координации. Все локальные данные потоков из Dav1dTaskContext
теперь управляются каждым рабочим потоком независимо, так что ему не обязательно быть Sync
.Добавление дополнительных блокировок позволяет обработать случай, в котором только одному потоку нужно изменять конкретное поле структуры. Версия
dav1d
во многих сценариях опирается на конкурентный, но не пересекающийся доступ к одному буферу. Один поток должен считывать или записывать из одного участка буфера, в то время как другой поток обращается к другому его участку, не связанному с первым. Этот паттерн, хоть и лишён на практике состояний data race, не сопоставляется чётко с безопасными идиомами Rust. В Rust мы бы сначала разбили буфер на непересекающиеся части, после чего распределили их между разными потоками для обработки. Такой паттерн требует знания точного размера каждого буфера данных заранее, чтобы иметь возможность правильно распределить эти участки для постановки задач потокам. В случае AV1 разделение буфера оказалось бы крайне сложным, поскольку оно не статично и даже не непрерывно. Для сохранения N-мерных массивов, таких как
ndarray
, которые бы позволили разделить эти буферы, существуют крейты. Но в этом случае для правильной разбивки буферов нам бы потребовалось понимание точных паттернов доступа всех задач во всех буферах. А это, в свою очередь, потребовало бы фундаментального перестраивания планировки задач rav1d
. В итоге мы выбрали другой путь, который больше подходит модели, используемой в
dav1d
. Мы создали тип обёртки буфера, который позволяет осуществлять к нему непересекающийся конкурентный доступ. В отладочных сборках мы отслеживаем каждый заимствованный участок, чтобы каждое такое изменяемое заимствование подразумевало эксклюзивный доступ к соответствующему диапазону элементов. Мы выяснили, что такое отслеживание очень полезно для отладки и исключения пересечения потоков друг с другом при заимствовании данных. Однако отслеживание каждого заимствования для релизных сборок слишком затратно, поэтому в них вся структура
DisjointMut
является бесплатной обёрткой вокруг внутреннего буфера. При доступе к буферу по-прежнему выполняется проверка на выход за границы, так что мы сохраняем пространственную безопасность, потенциально компрометируя безопасность потоков. Все буферы DisjointMut
в rav1d
представляют примитивные данные, поэтому в худшем случае этот паттерн может лишь внести неопределённость, если доступ окажется некорректно разделён. В листинге 2 показан фрагмент структуры, которая совместно используется всеми рабочими потоками. Несколько потоков конкурентно изменяют различные блоки в поле
b
, поэтому мы обернули этот вектор в DisjointMut
, чтобы обеспечить возможность конкурентного доступа.Листинг 2a. Фрагмент из структуры контекста фрейма C:
struct Dav1dFrameContext_frame_thread {
// ...
// Индексируется с помощью t->by * f->b4_stride + t->bx
Av1Block *b;
int16_t *cbi; /* bits 0-4: txtp, bits 5-15: eob */
// ...
} frame_thread;
Листинг 2b. Эквивалент той же структуры с листинга 2а на Rust:
pub struct Rav1dFrameContextFrameThread {
// ...
// Индексируется с помощью `t.b.y * f.b4_stride + t.b.x`.
pub b: DisjointMut<Vec<Av1Block>>,
pub cbi: Vec<RelaxedAtomic<CodedBlockInfo>>,
// ...
}
Там, где это возможно, вместо добавления блокировки мы использовали атомарные типы. Мы опираемся на код, уже избегающий логических состояний data race, и атомарные примитивные типы обеспечивают формальную безопасность потоков. Мы не требовали конкретного атомарного упорядочивания памяти, поскольку предполагаем, что операции записи в общие поля не склонны к состояниям гонки, поэтому использовали слабое (relaxed) упорядочивание.
На интересующих нас платформах естественным образом выровненные операции загрузки и сохранения уже являются атомарными, поэтому relaxed-операции упорядочивания атомиков в Rust сводятся к тем же операциям с памятью в С без дополнительных издержек1. Мы не могли использовать несвободные атомарные операции или методы
fetch
+update
, поскольку они сводятся к сложным и более медленным инструкциям. Мы добавили обёртку RelaxedAtomic
, чтобы упростить использование этих атомарных полей и исключить применение неэффективных паттернов. Мы также использовали крейт atomig
, чтобы сделать простые структуры примитивного размера и перечисления атомарными. В целом мы выяснили, что модель безопасности потоков Rust крайне строга. Если бы мы писали этот декодер с нуля, то спроектировали бы более разделённый и отчётливый обмен данными между потоками. Тем не менее мы смогли, по сути, сохранить производительность без внесения значительных изменений в существующую логику, используя новые структуры данных вроде
DisjointMut
и RelaxedAtomic
, которые по-прежнему дают нам желаемые гарантии безопасности при слабой защите от состояния data race.▍ Самореферентные структуры
Указатели на одну структуру или переключающиеся рекурсивно между разными являются типичным паттерном в С, который сложно воссоздать в безопасном Rust. Проблемные указатели, которые мы встретили при портировании
dav1d
, преимущественно относятся к одной из двух категорий: курсоры, отслеживающие позиции в буфере, и ссылки между структурами контекста.По большей части мы преобразовали указатели курсоров буфера в целочисленные индексы. Тем не менее это не всегда было просто — некоторые указатели могли временно выходить из границ, оказываясь перед началом буфера, поскольку позже добавлялось положительное смещение, или указатель не разыменовывался вовсе. Мы отрефакторили эти случаи, переместив вычисления индексов, чтобы смещение оставалось неотрицательным. И даже для более простых случаев изменение указателей на индексы потребовало внимательного отслеживания и документирования того, на какой буфер указывает каждый индекс, а также обеспечения для каждой операции с использованием индекса возможности доступа к соответствующему буферу.
Нам пришлось распутать структуры контекста
dav1d
, удалив из дочерних структур указатели на их контейнеры и затем передав дополнительные ссылки на структуры в виде параметров функций. Например, мы добавили в decode_tile_sbrow
ссылочные параметры Rav1dContext
и Rav1dFrameData
, поскольку из структуры контекста задачи нам эти указатели пришлось удалить.Листинг 3. Отделение структуры контекста:
// Оригинальная функция C
int dav1d_decode_tile_sbrow(Dav1dTaskContext *const t) {
const Dav1dFrameContext *const f = t->f;
Dav1dTileState *const ts = t->ts;
const Dav1dContext *const c = f->c;
// ...
}
// Её безопасная версия на Rust
pub(crate) fn rav1d_decode_tile_sbrow(
c: &Rav1dContext,
t: &mut Rav1dTaskContext,
f: &Rav1dFrameData,
) -> Result<(), ()> {
let ts = &f.ts[t.ts];
}
▍ Объединения
В
dav1d
используются неразмеченные объединения из языка C. В случаях, где дополнительное поле применяется в качестве тега, мы переписали эти объединения в безопасные размеченные перечисления Rust. Однако в С дискриминант некоторых объединений был неявным. Например, то, какой вариант объединения должен использоваться, определяла стадия задачи, сохранённая совершенно в другой структуре контекста. Для этих случаев вместо того, чтобы добавлять ненужный тег и изменять представление структуры и размер, мы предпочли использовать крейт zerocopy
, чтобы в среде выполнения повторно интерпретировать те же байты как два разных типа. Это было единственное возможное решение, поскольку объединения состояли полностью из примитивных типов без заполнения. Предоставляемые zerocopy
трейты обуславливают использование этого инварианта и делают возможным доступ к содержимому объединения без затрат и не требуя явного тега. И хотя этот паттерн менее идиоматичен, в нескольких случаях мы нашли его применение необходимым для повышения быстродействия и совместимости. Заключение
Оказались ли транспиляция и переписывание оправданы? Мы считаем, что да, по крайней мере, для проекта
rav1d
. Переписывание декодера AV1 с нуля привело бы к внесению всевозможных багов и проблемам совместимости. Мы выяснили, что, несмотря на проблемы с потоками и заимствованием, переписывание существующего кода С на безопасном и производительном Rust возможно. Наша реализация rav1d
на данный момент примерно на 6% медленнее текущей реализации dav1d
на С. Детали оптимизации быстродействия rav1d
мы более подробно разберём в следующей статье. Для приложений, где безопасность является первостепенной,
rav1d
обеспечивает безопасную по памяти реализацию, не вносящую дополнительных издержек в результате таких мер, как изолирование. Мы считаем, что если продолжить оптимизацию и доработку порта на Rust, то он сможет уверенно соперничать с реализацией на С во всех ситуациях, попутно обеспечивая безопасность памяти.Благодарим Amazon Web Services, Sovereign Tech Fund и Alpha-Omega за поддержку этого проекта. Если вы хотите больше узнать о rav1d
или начать его использовать, добро пожаловать на GitHub.
- Единственные издержки связаны с невозможностью совместить, например, 2 последовательных, выровненных загрузки
AtomicU8
в одно хранилищеAtomicU16
, что должно прозрачно реализовываться дляu8s
иu16s
. Для отдельных полей это не является проблемой, но всё же вносит дополнительные издержки при работе с массивами и срезами.
Telegram-канал со скидками, розыгрышами призов и новостями IT ?
Комментарии (72)
JordanCpp
15.09.2024 10:13+31В общей сложности наша команда из 3 разработчиков затратила более 20 человеко-месяцев усилий.
Аминь. Почти два года жизни было затрачено на переписывание, уже готового кода. Что бы он был переписан...
Yuri0128
15.09.2024 10:13Ржавчина все новые площади охватывает.
Почти два года жизни
Да не, - почти стандартные 9 месяцев на 1 чела.
JordanCpp
15.09.2024 10:13+6В общей сложности наша команда из 3 разработчиков затратила более 20 человеко-месяцев усилий.
Аминь. Почти два года жизни было затрачено на переписывание, уже готового кода. Что бы он был переписан...
perfect_genius
15.09.2024 10:13+1Получается, Хабр глючит или сломался, потому что даблпосты уже в нескольких темах.
DBalashov
15.09.2024 10:13+1И это только первый этап, который в результате дал минус 6% перфа. Дальше будет оптимизация (неясно сколько времени займёт) и самое главное - синк кода на Rust с изменениями в оригинальном коде. А это почти бесконечный процесс. Это должно быть очень нужно, если за такое взялись.
RichardMerlock
15.09.2024 10:13Надо было один раз потратить время на C-to-R конвертер и переписывать код автоматически и масшабно, а ошибки пусть нейросеть разгребает, всяко ей проще подкручивать, чем синтезировать. Тестов насыпать и всё, что завалилось снова на вход нейросети! Вкалывают роботы!
Ukrainskiy
15.09.2024 10:13+16Интересно, а можно было переписать с "небезопасного С" на более безопасный С(с соблюдением современных стандартов, рекомендаций, использованием стат. анализаторов и т.д.). Уверен, что прирост в производительности был бы измерим. А вот на сколько одно "безопаснее" другого, тут был бы вопрос. Да и времени наверняка бы меньше ушло.
Serpentine
15.09.2024 10:13+1Интересно, а можно было переписать с "небезопасного С" на более безопасный С(с соблюдением современных стандартов, рекомендаций, использованием стат. анализаторов и т.д.)
Проект dav1d "относительно" свежий по меркам выхода сишных стандартов (2018г.). Да, стандарт там C99 (только без VLA и некоторых примочек), C11 для потоков и современный POSIX. Смысла в тогда вышедшем С18 они при разработке, наверное, не видели.
Учитывая, что целью поставили кроссплатформенность и достижение максимально возможной скорости, чтобы преодолеть временное отсутствие аппаратного декодера AV1 (он уже появился 3-4 года назад если что), думаю, им новый C23 не подойдет, т.к. он до сих пор не опубликован и, соответственно, не так широко поддерживается. Да и насколько быстрее будет код на C23, это другой вопрос.
Ukrainskiy
15.09.2024 10:13+3Я скорее имел ввиду полный рефакторинг, нежели именно переход на новый стандарт С, да это мало что даст. Быстрее библиотека стала работать точно не из-за того, что Rust быстрее С. А так получается мы просто получили копию библиотеки но вместо С тут Rust. Окей, Раст гарантирует нам, что мы не отстрелом себе правую ногу, но по всем остальным конечностям все ещё можно стрелять. Так что если бы это была новая библиотека, без обратной совместимости и т.д. это дело было бы однозначно хорошее, но такая библиотека уже есть. А так, какой смысл в точно такой же либе, но без того комьюнити и поддержки, что делает оригинал?
Serpentine
15.09.2024 10:13+7Я скорее имел ввиду полный рефакторинг, нежели именно переход на новый стандарт С
Видимо, я просто не так понял.
Окей, Раст гарантирует нам, что мы не отстрелом себе правую ногу, но по всем остальным конечностям все ещё можно стрелять.
Учитывая, что в "безопасной" версии объем кода на Расте сопоставим с оставленным сишным кодом, а львиная доля так и осталась на asm'е, речь будет идти скорее про пару пальцев на правой ноге.
KReal
15.09.2024 10:13+8Быстрее библиотека стала работать точно не из-за того, что Rust быстрее С
А она и не стала)
lexxsu
15.09.2024 10:13+1Аппаратный декодер появляется в течении года после финализации стандарта. AV1 завершен гораздо ранее 3-4 лет, 3 года это VVC.
dv0ich
15.09.2024 10:13+1Лично мне интереснее было бы посмотреть на С++-версию, без использования C-style, но с использованием всех плюсовых механизмов безопасности и читаемости. Как велика была бы просадка в производительности (ведь истые сишники любят пугать ею).
alexandertortsev
15.09.2024 10:13Не понятно, в чем проблема. Они написали обработку для видео? Так ведь давно есть klite coded pack и даже vlc, где все это есть. А еще каждый телефон умеет его записывать и проигрывать
Ukrainskiy
15.09.2024 10:13+4Более того, на сколько я понял из статьи они переписали все as is, без погружения в детали алгоритмов и т.д. так что если где-то были логические баги, есть вероятность, что они остались и в новой реализации.
A3st
15.09.2024 10:13+16Какая то бесполезная работа была сделана, взять рабочую си библиотеку и заставить ее работать на расте. Фанатикам раста главное написать о безопасности и производительности и написать при этом кучу unsafe кода. Писать нужно новый код, а старый просто переносить в виде врапперов, либо делать сразу на расте с нуля, если позволяют ресурсы, тогда можно заявлять о повышении безопасности/производительности
pvsur
15.09.2024 10:13Очень не хочется лёжа на операционном столе услышать - блин, опять аппарат заглючил-завис, ребутаем. Может, там фобия у растоманов такая ?
Yuri0128
15.09.2024 10:13+1Так не услышите. Просто по приходу в рай/ад послушайте дежурных ангелов/чертей, которые между собой буду говорить о причине вашего визита...
JerryI
15.09.2024 10:13+10если продолжить оптимизацию и доработку порта на Rust, то он сможет уверенно соперничать с реализацией на С во всех ситуациях, попутно обеспечивая безопасность памяти.
Как оно может соперничать, если не переделывать логику алгоритмов. То что Rust безопаснее не может быть без компенсации этого скоростью. Дополнительные проверки все равно будет подтормаживать. Как надоело слышать «производительнее С». Производительнее Си только машинный код, даже не llvm.
PrinceKorwin
15.09.2024 10:13+4Производительнее Си только машинный код, даже не llvm.
Это как? Си же компилируется в машинный код тем же llvm. Можете раскрыть что вы имели ввиду? Ассемблер? Так код на ASM не дает гарантий, что он будет быстрее оптимизаций C компилятора.
То что Rust безопаснее не может быть без компенсации этого скоростью.
Та же проверка типов. Повышает безопасность, в рантайме никак не влияет на скорость. Влияет только на время компиляции.
blaka
15.09.2024 10:13В rust же все эти многочисленные unwrap/expect/? разворачиваются в дополнительные проверки, которые повышают безопасность за счет производительности.
mayorovp
15.09.2024 10:13+1Вы так пишете, как будто на Си нет никаких проверок ошибок. А если и правда нет - нафиг такой код, даже если он "быстрый".
JerryI
15.09.2024 10:13Вопрос к кодеру. Вылет за пределы это всегда ошибка в алгоритме. Если это декодер видео то там все должно быть детерминировано.
Siemargl
15.09.2024 10:13+1Не только. Ещё бывает ошибка во входных данных - испорченный видео поток напр
JerryI
15.09.2024 10:13Это вероятно ловится на уровне алгоритма. Я имею ввиду проверки видео потока на уровне алгоритма, а не проверка алгоритма на уровне языка. Те супервизия предполагается, но на другом концептуально уровне
emusic
15.09.2024 10:13+2Можете раскрыть что вы имели ввиду?
В виду имелось то, что непосредственно машинный код можно написать любой, а компилятор в состоянии породить только ограниченное подмножество вариантов.
код на ASM не дает гарантий, что он будет быстрее оптимизаций C компилятора
Гарантий не дает, но возможность оставляет, иначе бы упомянутые процедуры декодирования тоже писались бы на C.
JerryI
15.09.2024 10:13+2Это как? Си же компилируется в машинный код тем же llvm.
Да, почему же. Это только с clang-ом пришло.
ак код на ASM не дает гарантий, что он будет быстрее оптимизаций C компилятора.
В принципе внизу ответили :) Я имею ввиду, что вручную написанный код на ASM точно не будет ограничен сверху ничем, связанным с кроссплатформиерностью и ограниченностью знаний/алгоритмов компилятора.
Действительно, возможно с некоторой вероятностью комплиятор Rust что-то и применит из ряда оптимизирующего, может даже лучше, чем С. Однако, в самих доках написано про boundary checks, borrow checks именно в райнтайме. С учетом их количества в типичном коде, это будет бить, и с наибольшей вероятностью либо не изменит производительность, либо ухудшит.
Ну и в целом Rust выразительнее Си, можно более высокоуровневые конструкции городить. Я сомневаюсь, что люди будут заниматься аскетизмом и не пользоваться этим преимуществом, что опять ударит по производительности, но увеличит продолжительность жизни программиста :)
Ничего плохого в этом не вижу, станет меньше seg fault, может ошибку найдут где-то, может улучшат что-то. Однако буду бить по столу и брюзжать слюной когда снова увижу "Этот язык быстрее С", пока не докажут, что новый компилятор устраняет фатальный недостаток (или делает мега-оптимизацию с учетом AXV и SSE и выравниванием памяти и чего там еще...) внутри сгенерированного кода llvm/asm по сравнению с clang/gcc.
orbion
15.09.2024 10:13+1Возможности оптимизации во многом зависят от того, что знает о коде компилятор. Может оказаться так, что в Rust больше возможностей дать компилятору подсказку (либо же это получается косвенным образом, исходя из строгости языка), тем самым позволив ему создать более шустрый машинный код.
Но зачастую сделать такие подсказки без тщательного изучения алгоритма непросто.
emusic
15.09.2024 10:13+2Строгость языка, как таковая, редко дает компилятору достаточно информации для предельной оптимизации кода. В основном он извлекает эту информацию косвенно. В некоторых языка/компиляторах есть некоторые "внеязыковые" средства подсказки (прагмы, инварианты и т.п.), но еще ни разу не видел, чтоб этих средств хватало хотя бы для большинства случаев. Как правило, даже в предельно оптимизированном коде есть недостатки, для устранения которых компилятору банально не хватает информации о возможных значениях данных, их соотношениях и т.п.
Melirius
15.09.2024 10:13+1Ага, потому в новый C++ завезли assume.
emusic
15.09.2024 10:13+1Угу, и тридцати лет не прошло с тех пор, как его стали завозить в популярные коммерческие компиляторы. Мгновение на фоне вечности. :)
Melirius
15.09.2024 10:13Комитет отбивался, как мог - это ж новый источник UB на ровном месте :)
emusic
15.09.2024 10:13С чего бы вдруг? Если компилятор опасается, что его обманут, он может в отладочном режиме вставлять код для вычисления выражения и проверки результата, сочетая assume и assert. У меня макрос Assert так раскрывается с незапамятных времен.
orbion
15.09.2024 10:13+3Конечно, предельная оптимизация вообще должна подразумевать кучу аспектов:
Заточенность на определённую аппаратную платформу, причём именно платформу целиком, не только процессор — так как, например, разные планки памяти могут иметь разные характеристики, соответственно, оптимальный код может отличаться (пусть это и редкий случай). Ещё надо учитывать микроархитектуру ядра, на котором выполняется программа — особенно актуально на ARM и x86, начиная с Alder Lake
Заточенность на входные данные — если они не совсем случайные (а это большинство кейсов), то как правило, здесь тоже можно разгуляться — branch prediction, префетчи, оптимальная глубина разворачивания циклов, разные ветви кода для разных кейсов и так далее
Входные данные могут меняться со временем, и это тоже надо учитывать (нужен JIT?)
Как-то раз удалось, почти случайно, написать на Ассемблере код, который работал на E-ядрах (тех самых, которые так не любят) i7-13700H как минимум на 70% быстрее, чем любая вариация машинного кода, сгенерированная GCC / Clang / oneAPI DPC++ с различными сочетаниями оптимизирующих флагов и PGO (как компиляторным, так и с LLVM BOLT), причём работало на те же 70% быстрее даже чем при запуске на P-ядре при равной частоте.
Немного таблиц из других экспериментов, без оптимизации на Ассемблере
Числа в ячейках — время работы в секундах.
Программа на проверку гипотезы Коллатца до определённого N:
Сортировка пузырьком:
Кроме того, при ручном написании кода мы неизбежно играем в перебор, надеясь эмпирически найти оптимальный код (который совсем не факт что будет быстрым на другом железе). Когда речь заходит о брутфорсе, в это непременно должен вовлекаться компьютер, так как у него для этого существенно больше ресурсов. Выше Вы привели очень правильную мысль:
непосредственно машинный код можно написать любой, а компилятор в состоянии породить только ограниченное подмножество вариантов
Так почему бы не дать компиляторам возможность генерировать разные вариации машинного кода и тестировать их самостоятельно? Да, среди этих вариаций может не быть идеальной, но возложив перебор на компьютер, мы сможем подойти к ней существенно ближе, чем перебирая варианты вручную.
emusic
15.09.2024 10:13+1Так я всегда выступаю за то, чтобы иметь возможность дать компилятору любую полезную информацию, которую он не может извлечь непосредственно из кода. Но такой подход все менее популярен - уже давно преобладает стремление писать на C++ только для абстрактной универсальной машины, и нехай каждый конкретный компилятор пытается оптимизировать ее под свою платформу, как умеет.
Slparma
15.09.2024 10:13Правильное название статьи: занимаемся ху....
Не кому ваш раст не нужен это бесполезный не до С язык
DieSlogan
15.09.2024 10:13+10Согласен по поводу спорных результатов команды, впрочем, это их дело.
Но, как человек писавший на Си и изучающий Rust, могу сказать, что новый проект начну на Rust-е.
Более того, в старых проектах, уж коли доведётся модули зарефакторить, то перепишу на Rust.
Yuri0128
15.09.2024 10:13+2Ну почему бесполезный? Вполне нормальный, со своими ньюансами и заморочками. И для быстрой разработки более удобный. Для сильно замороченного остается C/C++ (для особой упертости - assembler language, - сам применял пару лет назад на ARM-е).
Вон выше тоже чел написал свой вердикт. И я вполне его понимаю.
Melirius
15.09.2024 10:13+2А вот ответьте мне, чем Rust удобнее для быстрой разработки? Я вот с его убер-жёсткой структурой постоянно мучился.
Yuri0128
15.09.2024 10:13+1Така я до сих пор (я типа начинающий, ну по крайней мере в сравнении со строкм в C)... Но все-же он несколько быстрее позволяет получить результат на непростых задачах. Да и на простых тоже. И в некоторых случаях его жеская типизация позволяет упростить отладку. Хотя и режет возможности... Ну и никто не говорит что он простой, в общем-то. А там - каждому свое... На вкус и цвет все люди разные..
DieSlogan
15.09.2024 10:13+4Для быстрой разработки, например небольшой консольной утилиты, которая парсит JSON-ы и складывает в Oracle DB.
На Си нам потребуется:
Нужно поискать, где вы подключали похожие либы с помощью
autoconf
,cmake
,meson
,SCons
(мы ведь не будем заново jansson писать).
Ну и пошаманить с ними, открыть документацию, само собой.Если пишите под винду, там есть Visual C++ и вопрос с cmake-ами снимается, но ваша функция
main
выглядит примерно так. Вроде бы на Си пишите, а кажется, что и не на Си:DWORD __cdecl wmain(void) { LPCWSTR str = L"Value"; return 0; }
Если работаем с Oracle, то нужен Pro*C. На известном ресурсе посвященном Ораклу "Ask Tom", в вопросе про Pro*C адепт Oracle пишет:
I use pro*c only when I cannot accomplish the task efficiently in PLSQL or SQL. In 9i, with external tables, merge, pipelined functions -- I'm very very hard pressed to find a reason to use C.
Если вы не знаете что такое Pro*C, то вам очень повезло. Там нет проверки синтаксиса, а для компиляцииcc
вас не выручит, компилить такие файлы следует своей утилитой.
Добро пожаловать в АД
И это для одно поточного приложения, если у нас несколько потоков, то нам нужен контекст, и такой "элегантный код"
В моём опыте на Си, всегда присутствовало вот это: "ну на Си это написать — всё равно, что из пушки по воробьям, там же ерундовая утилита какая-то".
И в результате кодовая база состоит наполовину из Си, наполовину из адского нагромождения тормозных скриптов на bash, awk, python.
ViacheslavNk
15.09.2024 10:13Для быстрой разработки, например небольшой консольной утилиты, которая парсит JSON-ы и складывает в Oracle DB.
Тут разве нужен С, если нет особых требований к перфомансу, то тут всякие python выглядят предпочтительнее.
Yuri0128
15.09.2024 10:13Я крайний раз делал такое на Delphi, который сейчас Embarcadero, вполне шустро парсил и мороки было меньше. Пайтон все-ж сильно медленнее.
ViacheslavNk
15.09.2024 10:13Я согласен, все зависит от задачи, если нужно один небольшой json в минуту парсить это одно, если миллионы совсем другое или если размеры json-файлов гигабайты, а если еще сами файлы нужно получать из разных мест (сеть, шара, облако) и т.д.. Но в общем случае видится что данная задача перегнать json-ы в базу данных это типичная задача для скриптовых языков.
Yuri0128
15.09.2024 10:13Ну, вот как раз в моем случае была сетка с 10 источниками и числом строчек, ну от 100 000 и выше. А разово и на 5 строчек - ну скрип.
DieSlogan
15.09.2024 10:13В другой ветке я ответил https://habr.com/ru/companies/ruvds/articles/842970/#comment_27299666
Yuri0128
15.09.2024 10:13складывает в Oracle DB
О... Когда-то приходилось, слава Богу, в однпоток. Если вам сейчас приходится - примите соболезнования. Про "спроси Тома" - тоже знакомо...
TheCoolKuid
15.09.2024 10:13А зачем тут вообще Rust или тем более C? Один скрипт на Python или JS решит проблему раз в 10 быстрее.
DieSlogan
15.09.2024 10:13Исходя из того, что мы уже определились, что пишем наше большое приложение на Си или Rust, в таких условиях дописывание маленькой сопутствующей утилиты всегда рискует перерасти в ад из кучи скриптов на bash, awk, python, etc.
ViacheslavNk
15.09.2024 10:13Если работаем с Oracle, то нужен Pro*C. На известном ресурсе посвященном Ораклу "Ask Tom", в вопросе про Pro*C адепт Oracle пишет:
Много приходилось работать с ораклом ни когда не использовали Pro*c, если работали локально на сервере, находили oracle home из него загружали OCI библиотеку (dll/so) из нее собственно десяток функций, если удаленно то использовали динамические библиотеки из SDK.
Yuri0128
15.09.2024 10:13Ну так у каждого свои грабли. Помнится, Oracle Call Interface - он только под винду вроде был?
ViacheslavNk
15.09.2024 10:13Под линукс так же работает, те же сигнатуры функций, только название библиотеки другое.
Melirius
15.09.2024 10:13+8То есть люди переписали "потенциально небезопасный" код на C в настолько же небезопасный код на Rust (цитирую: "мы сохраняем пространственную безопасность, потенциально компрометируя безопасность потоков"), но теперь будут говорить, что у них безопаснее?
Faust-Victor
15.09.2024 10:13+4Портируем декодер AV1 с С на Rust для повышения быстродействия и безопасности
Для сохранения быстродействия мы оставили в коде (небезопасные) нативные процедуры ассемблера, которые выполняют низкоуровневые операции декодирования
kovserg
Если со сравнением производительности вроде всё ясно. То как сравнивали безопасность? По количеству найденных багов fazzing тестами?
JordanCpp
Я думаю, скоро начнутся новости типа. В библиотеке x переписанной на rust, найдена ошибка, уязвимость и т.д
KanuTaH
Что значит "скоро начнутся", уже. Служебный крейт для
whome
, RIIR стандартной юниксовой командыwhoami
.