Попробуем выдвинуть аргументы против Rust

в 12:30, , рубрики: Rust, критика, Программирование, системное программирование, эффект Линди

Недавно я прочитал статью c критикой Rust. Хотя в ней было много правильных вещей, она мне не понравилось — слишком многое там очень спорно. В целом, я вообще не могу рекомендовать к прочтению никакой статьи с критикой Rust. Это нехорошо, ведь важно обсуждать недостатки, а шельмование низкокачественной и неумелой критики, к сожалению, заставляет пропустить мимо внимания действительно хорошие аргументы.

Итак, попробую привести аргументы против Rust.

Не всё программирование является системным

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

Однако в большинстве случаев не требуется абсолютная производительность или контроль над аппаратными ресурсами. Для этих ситуаций современные управляемые языки, такие как Kotlin или Go, обеспечивают приличную скорость, завидную производительность и безопасность памяти благодаря использованию сборщика мусора с динамическим управлением памятью.

Сложность

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

Цена за улучшенный контроль — это проклятие выбора:

struct Foo     { bar: Bar         }
struct Foo<'a> { bar: &'a Bar     }
struct Foo<'a> { bar: &'a mut Bar }
struct Foo     { bar: Box<Bar>    }
struct Foo     { bar: Rc<Bar>     }
struct Foo     { bar: Arc<Bar>    }

В Kotlin вы пишете класс Foo(val bar: Bar) и приступаете к решению задачи. В Rust нужно сделать выбор, иногда достаточно важный, со специальным синтаксисом.

Вся эта сложность не просто так — мы не знаем, как создать более простой и безопасный для памяти язык низкого уровня. Но не для каждой задачи нужен низкоуровневый язык.

См. также презентацию «Почему C++ держится на плаву, когда корабль „Ваза” затонул».

Время компиляции

Время компиляции — это универсальный фактор. Если программа на каком-то языке медленно запускается, но этот язык позволяет быструю компиляцию, то у программиста будет больше времени для оптимизации, чтобы ускорить запуск программы!

В дилемме дженериков Rust намеренно выбрал медленные компиляторы. В этом есть определённый смысл (рантайм действительно ускоряется), но вам придётся всеми силами бороться за разумное время сборки в более крупных проектах.

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

В отличие от C++, сборка Rust не распараллеливается до предела, количество параллельных процессов ограничено длиной критического пути в графе зависимостей. Разница будет заметна, если у вас больше 40 ядер для компиляции.

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

Зрелость

Пять лет — это определённо малый срок, так что Rust молодой язык. Хотя будущее кажется блестящим, но всё-таки более вероятно, что через десять лет мы будем программировать на С, а не на Rust (см. эффект Линди). Если вы пишете софт на десятилетия, то должны серьёзно рассмотреть риски, связанные с выбором новых технологий (хотя выбор Java вместо Cobol для банковского программного обеспечения в 90-е годы ретроспективно оказался правильным выбором).

Есть только одна полная реализация Rust — компилятор rustc. Наиболее продвинутая альтернативная реализация mrustc целенаправленно пропускает многие статические проверки безопасности. На данный момент rustc поддерживает только один продакшн-ready бэкенд — LLVM. Следовательно, поддержка процессорных архитектур здесь более узкая, чем у C, у которого есть реализация GCC, а также поддержка ряда проприетарных компиляторов, специфичных для конкретных вендоров.

Наконец, у Rust нет официальной спецификации. Текущая спецификация не завершена и не документирует некоторые мелкие детали реализации.

Альтернативы

Кроме Rust, для системного программирования есть и другие языки, в том числе C, C++ и Ada.

Современный C++ предоставляет инструменты и рекомендации для повышения безопасности. Есть даже предложение о безопасности времени жизни объектов в стиле Rust! В отличие от Rust, использование этих инструментов не гарантирует отсутствие проблем с безопасностью памяти. Но если вы уже поддерживаете большой объём кода C++, имеет смысл проверить, возможно, следование рекомендациям и использование санитайзеров поможет в решении проблем безопасности. Это трудно, но явно легче, чем переписывать весь код на другом языке!

Если вы используете C, то можете применить формальные методы, чтобы доказать отсутствие неопределённого поведения, или просто тщательно всё протестировать.

Ada безопасна для памяти, если не использовать динамическую память (никогда не вызывайте free).

Rust — интересный язык по соотношению затрат к безопасности, но далеко не единственный!

Набор инструментов

Инструменты Rust не назовёшь идеальными. Базовый инструментарий, компилятор и система сборки (cargo) часто называют лучшими в своём классе.

Но, например, некоторые инструменты, связанные со средой выполнения (в первую очередь, для профилирования кучи), просто отсутствуют — трудно размышлять о рантайме, если инструмента просто нет! Кроме того, поддержка IDE тоже далеко не соответствует уровню надёжности Java. На Rust просто невозможен автоматизированный сложный рефакторинг программы с миллионами строк.

Интеграция

Что бы ни обещал Rust, но сегодняшний мир системного программирования говорит на C и C++. Rust намеренно не пытается имитировать эти языки — он не использует классы в стиле C++ или C ABI.

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

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

Производительность

«Использование LLVM» не является универсальным решением всех проблем производительности. Хотя я не знаю бенчмарков, сравнивающих производительность C++ и Rust в целом, но нетрудно придумать задачи, где Rust уступает C++.

Вероятно, самая большая проблема в том, что семантика перемещения Rust основана на значениях (memcpy на уровне машинного кода). С другой стороны, семантика C++ использует специальные ссылки, из которых можно взять данные (указатели на уровне машинного кода). Теоретически, компилятор должен видеть цепочку копий, на практике это часто не так: #57077. Связанная с этим проблема заключается в отсутствии размещения новых данных — Rust иногда нужно копировать байты в/из стека, в то время как C++ может создать объект на месте.

Несколько забавно, что в дефолтный Rust ABI (в котором пожертвовали стабильностью ради эффективности) иногда работает хуже, чем у C: #26494.

Наконец, хотя теоретически код Rust должен быть более эффективным из-за значительно более богатой информации об алиасах, включение оптимизации, связанной с алиасами, вызывает ошибки LLVM и некорректную компиляцию: #54878.

Но, повторюсь, это редкие примеры, иногда сравнение происходит в другую сторону. Например, в Box у Rust нет проблемы производительности, какая есть у std::unique_ptr.

Потенциально более серьёзная проблема заключается в том, что Rust с его определениями дженериков менее выразителен, чем C++. Таким образом, некоторые шаблонные трюки C++ для высокой производительности не могут быть выражены в Rust с помощью хорошего синтаксиса.

Значение unsafe

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

  1. надёжная (не отмеченный unsafe код не может вызвать неопределённое поведение),
  2. модульная (различные небезопасные блоки можно проверить отдельно).

Вполне очевидно, что так оно и есть: фаззинг кода Rust находит панику, но не переполнения буфера.

Но теоретические перспективы не столь радужны.

Во-первых, нет определения модели памяти Rust, поэтому невозможно формально проверить, является ли данный небезопасный блок допустимым или нет. Существует неофициальное определение «вещей, которые rustc делает или на которые может полагаться» и продолжается работа над верификатором рантайма, но фактическая модель не ясна. Таким образом, где-то может быть какой-то небезопасный код, который сегодня работает нормально, а завтра будет объявлен недействительным и сломается в новой оптимизации компилятора через год.

Во-вторых, есть мнение, что блоки unsafe на самом деле не являются модульными. Достаточно мощные блоки unsafe могут, по сути, расширить язык. Два таких расширения не делают ничего плохого в изоляции друг от друга, но приводят к неопределённому поведению при одновременном использовании: см. статью «Наблюдаемая эквивалентность и небезопасный код».

Наконец, в компиляторе есть явные ошибки.

Вот некоторые темы, которые я намеренно опустил:

  • Экономика («труднее найти программистов на Rust») — мне кажется, раздел «Зрелость» отражает суть этого вопроса, который не сводится к проблеме курицы и яйца.
  • Зависимости («stdlib слишком мал / везде слишком много зависимостей») — учитывая, насколько хорош Cargo и соответствующие части языка, я лично не вижу в этом проблемы.
  • Динамическое связывание («в Rust должен быть стабильный ABI») — не думаю, что это сильный аргумент. Мономорфизация фундаментально несовместима с динамическим связыванием, и если вам действительно нужно, то есть C ABI. Я действительно думаю, что здесь можно улучшить положение вещей, но вряд ли речь идёт о специфичных изменениях именно в Rust.

Обсуждение темы в /r/rust.

Автор: m1rko

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js