Опровергаем четыре стереотипа о языке программирования Rust

iz6j9tu-6qcgp4u3d9j9zdzajsm.png

Язык программирования Rust, созданный и поддерживаемый корпорацией Mozilla, позволяет обычным программистам писать одновременно и безопасные и быстрые системы: от калькуляторов до высоконагруженных серверов.

За своё относительно короткое время существования данный язык уже успел обрасти стереотипами, четыре из которых я попытаюсь опровергнуть ниже. Я могу пропустить некоторые моменты, дискуссии в комментариях приветствуются.


  1. Rust — сложный язык программирования
  2. Rust — ещё один «убийца C/C++»
  3. Unsafe губит все гарантии, предоставляемые Rust
  4. Rust никогда не обгонит C/C++ по скорости

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

Данный стереотип восходит своими корнями к концепции времён жизни ссылок (lifetimes), позволяющей на уровне семантики языка описывать гарантии действительности используемых ссылок. Синтаксис лайфтаймов выглядит сперва странным:

struct R<'a>(&'a i32);
unsafe fn extend_lifetime<'b>(r: R<'b>) -> R<'static> {
    std::mem::transmute::, R<'static>>(r)
}

unsafe fn shorten_invariant_lifetime<'b, 'c>(r: &'b mut R<'static>)
                                             -> &'b mut R<'c> {
    std::mem::transmute::<&'b mut R<'static>, &'b mut R<'c>>(r)
}

Но на деле синтаксис объявления лайфтайма довольно прост — это всего-лишь идентификатор, за которым следует апостроф. Лайфтайм 'static означает, что ссылка является действительной на протяжении всего времени исполнения программы.

Что такое «действительность ссылки»? Если ссылка действительна, то она поддаётся разыменованию без паники, ошибки сегментации и прочих прелестей. Например, в функции main() указатель something становится недействительным т.к. все автоматические переменные функции produce_something() очищаются после её вызова:

int *produce_something(void) {
    int something = 483;
    return &something;
}

int main(void) {
    int *something = produce_something();
    int dereferenced = *something; // Segmentation fault (core dumped)
}

Семантически лайфтайм может быть задан посредством других лайфтаймов. Например, эта функция требует того, чтобы ссылка foo не стала недействительной до недействительности ссылки bar:

fn sum<'a, 'b: 'a>(foo: &'b i32, bar: &'a i32) -> i32 {
    return foo + bar;
}

Конструкция выше применяется крайне редко, чаще всего одного лишь указания лайфтайма достаточно. Лайфтаймы — это элегантная абстракция, дающая возможность программистам быть уверенными в действительности собственных ссылок как в контексте однопоточных, так и многопоточных систем. Сложность лайфтаймов сильно переоценена.

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

Рассмотрим на примере. В приведённом ниже коде переменная x владеет экземпляром структуры Foo, а переменная y заимствует значение, которым владеет переменная x:

struct Foo {
    data: Vec,
}

fn main() {
    let x = Foo { data: Vec::new() }; // Владение (owning)
    let y = &x; // Заимствование (borrowing)
}

Если один поток имеет иммутабельную ссылку на переменную, а другой поток имеет мутабельную, то второй может изменить значение, вызвав состояние гонки. Данная ситуация недопустима в безопасном языке, вследствие чего команда Rust определила два простых правила:


  • Значение может быть заимствовано иммутабельными переменными множество раз и при этом не заимствовано мутабельной;
  • Значение может быть заимствовано мутабельной переменной лишь один раз и при этом не быть заимствованным иммутабельными.

На данный момент Rust — единственный язык программирования, обладающий одновременно активным сообществом и характеристиками, позволяющими ему решать задачи, решаемые языками C/C++. Синтаксис и семантика позволяют с лёгкостью изъясняться на разных уровнях абстракции — от инструкций SIMD до управления веб-серверами.

Данный стереотип возник вследствие языков Vala, Zig, Golang и подобных. Как я сказал выше, у этих языков либо слишком маленькое сообщество, либо они теоретически и практически не смогут работать на всех системах, на которых способны работать C/C++. У Vala и Zig маленькое сообщество, а Golang берёт курс на вытеснение интерпретируемых языков и не может работать на системах с критической нехваткой ресурсов т.к. поставляется с дополнительной средой выполнения (например, сборщик мусора).

Очевидно, что языки C/C++ будут жить ещё очень много лет из-за накопленного за десятилетия кода и программистов, пишущих на них, но Rust имеет все шансы потеснить их как это когда-то сделала Java.

Unsafe — это конструкция языка, позволяющая совершать операции, способные привести к неопределённому поведению (UB). В действительности, unsafe позволяет делать лишь четыре операции, запрещённые в «безопасном» Rust:


  • Вызов небезопасной функции;
  • Реализация небезопасного трейта;
  • Разыменование глобальной статической мутабельной переменной;
  • Разыменование сырого указателя.

Во-вторых, потенциально небезопасный блок кода может быть инкапсулирован в безопасный блок т.к. компилятор предполагает, что программист не сомневается в его правильности. Смысл потенциально небезопасного блока кода в том, что при его неправильном использовании поведение программы не определено.

fn safe_display() {
    unsafe {
        let x = 385;
        let x_ref: *const i32 = &x;
        println!("{}", *x_ref);
    }
}

Функция safe_display() полностью безопасна т.к. правильность потенциально небезопасного блока формально доказуема. Пользователь может использовать эту функцию без боязни UB.

Данный стереотип можно встретить в несколько иной трактовке: «Rust станет популярным лишь тогда, когда его сделают полностью небезопасным». И снова неверно т.к. не все гарантии, предоставляемые Rust, могут работать в полностью небезопасном коде. Концепция Rust теряется при отсутствии гарантий безопасности.

Утверждение безосновательное. В теории, программа, написанная на языке Rust, может быть оптимизирована столь же хорошо, как и аналогичная программа на C/C++. В некоторых синтетических тестах производительности Rust даже обгоняет GCC C:

9o8t5df6bc-scwykpwhsdvi3bmu.png
l5krsswtuqmdt-pw393o7ncjhv4.png
rccho4wali6k5lonxrrwrw7kxv0.png

Что касается тестов производительности на реальных задачах, то можно отметить замеры производительности RapidJSON и serde_json. serde_json парсит DOM медленнее, чем это делает RapidJSON, но при сериализации/десериализации структур serde_json обогнал RapidJSON как на GCC, так и на CLANG:

yqc4stv2w0xzctqtoqdnzb2gaow.png

Также можно отметить библиотеку Rustls, обогнавшую знаменитую OpenSSL практически во всех тестах (на 10% быстрее при установке соединения на сервере и на 20%-40% быстрее на клиенте, на 10%-20% быстрее при восстановлении соединения на сервере и на 30%-70% быстрее на клиенте).

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


© Habrahabr.ru