Критика языка Rust и почему C/C++ никогда не умрет

24 апреля 2015

Я не мог не заметить, что читателей сего блога очень заинтересовала тема «нужно ли давать котикам играться с новыми клубочками». Поэтому хотелось бы поделиться еще кое-какими своими мыслями по связанной теме, в отношении языков Си и C++ и убьет ли их Rust. Тема, как вы сами понимаете, очень-очень холиварная, поэтому подумайте еще раз, хотите ли вы читать эту заметку дальше и тем более участвовать в «конструктивном обсуждении» поста при помощи комментариев.

Примечание: Ниже по тексту я исхожу из предположения, что Rust является попыткой сделать быстрый и безопасный язык. В конце концов, ребята из Mozilla делали его, как инструмент разработки браузерного движка. Если же это очередной просто безопасный язык, тогда получаем странное. Самых разных безопасных языков и так уже пруд пруди, каждый найдет себе по вкусу. И если не стоит цели заменить C++, то (1) для чего в языке сделано unsafe подмножество? (2) зачем было удалять из языка легковесные потоки, удобно же? Другими словами, в этом случае происходящее вообще не имеет никакого смысла.

Если вдруг вы почитываете форум linux.org.ru, отмечу, это это не тот список из 10 чисто технических причин не любить Rust, речь о котором шла в этом трэде. Как показало обсуждение в Skype с уважаемым товарищем @sum3rman, есть больше одного мнения касательно того, насколько «техническими» считать эти причины. В общем, фиговый список я составил, но кое-какие пункты из него, наиболее интересные, пожалуй, я все-таки рискну привести. На самом деле, тут и простых, не технических, причин за глаза хватает.

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

Хорошо, а как на счет применения Rust при написании нового кода?

Вспомним, что это уже далеко не первая попытка сделать «более правильный» C/C++. Возьмем хотя бы язык D. Появился в 2001 году, очень хороший язык. Нет ни вакансий, ни нормальных инструментов разработки, ни каких-то особо выдающихся саксесс сторис. Проект OpenMW изначально писали на D, а потом внезапно решили целиком переписать на C++. Как признаются разработчики, им приходило много писем в стиле «отличный проект, мы были бы рады в него контрибьютить, но не знаем и не хотим знать этот дурацкий D». Википедия сообщает, что помимо D была и масса других попыток в той или иной степени убить C++, например, Vala, Cyclone, Limbo, BitC. Многие ли вообще слышали о таких языках?

Думаю, давно пора извлечь уроки из истории. Ни один здравомыслящий человек не потащит в проект новый язык, пока вы хотя бы не покажете ему нормальные инструменты разработки, не расскажете парочку саксесс сторис и не покажете десяток программистов на этом языке, живущих поблизости. Программисты же, пожалуй, кроме самых молодых, никогда не станут тратить свое время и здоровье на изучение очередного самого правильного языка, пока вы не покажете им нормальные инструменты разработки (не поделки типа Racer), пару десятков тысяч готовых библиотек (не «experimental», «unstable» и так далее), не расскажете парочку саксесс сторис и не покажите десяток открытых вакансий в их городе. Проблема курицы и яйца. Очень редко эту проблему удается успешно решить (условно тут можно привести в пример Go и Scala), в основном благодаря вложению времени и денег со стороны некоторой крупной компании (Google, Typesafe), по каким-то своим соображениям заинтересованных в популяризации языка.

Как я уже отмечал, одних только не технических причин более, чем достаточно. Однако чисто из любопытства попробуем представить на секунду, что их нет. Тогда нет причин не писать на Rust? Оказывается, это тоже как минимум под очень большим вопросом.

C/C++ критикуют за разное. Критикуют, кстати, очень часто те, кто в продакшене даже издали не видел кода на С++. Коротко и ясно проблему можно описать так: С++ очень быстрый (а также не требовательный к памяти, заряду батареи и тд), но не безопасный в том смысле, что позволяет выходить за границы массивов, по ошибке обращаться к освобожденным кускам памяти и так далее. В свое время эта проблема привела к появлению массы безопасных языков, таких, как Java, C#, Python и других. Но оказалось, что эти языки по сравнению с C++ слишком требовательны к ресурсам и обладают прочими недостатками, вспомним хотя бы неизбежный stop the world при сборке мусора. Поэтому люди бьются над задачей сделать язык такой же быстрый, как C++, но еще и безопасный. Одним из таких языков и является Rust.

Rust действительно безопасный, но, к сожалению, далеко не быстрый. По скорости на момент написания этих строк Rust сравним с Java, Go и Haskell:

Бенчмарк языка Rust

Я искренне надеюсь, что со временем его как-то разгонят, но до тех пор в плане компромисса скорости и безопасности он не намного интереснее Scala или Go. До сих пор остается открытым вопрос, можно ли вообще сделать язык быстрым и безопасным, или постоянные проверки на выход за границы массива, безопасные обвязки вокруг биндингов к сишным библиотекам и так далее автоматически делают любой язык в 2 раза медленнее С/C++.

А за счет чего, собственно, Rust безопасен? Если говорить простыми словами, то это язык со встроенным статическим анализатором кода. Действительно очень крутым статическим анализатором, который ловит все типичные для С++ ошибки, притом не только связанные с управлением памятью, но и многопоточностью. Передал по каналу ссылку на изменяемый объект другому потоку, а потом попробовал воспользоваться этой ссылкой сам — все, не скомпилится. Это действительно здорово.

Но C++ последние 30 лет тоже не стоял на месте, для него существует великое множество как статических, так и динамических анализаторов. Посмотрите хотя бы короткое видео про гугловые санитайзеры, они действительно очень круты. В любом серьезном проекте вы все равно используете систему непрерывной интеграции и при сборке билда прогоняете кучу тестов. Если это не так, то у вас куда более серьезные проблемы, чем небезопасность языка, потому что статическая типизация не гарантирует вам правильную логику работы приложения! Так вот, а раз вы все равно прогоняете тесты, почему бы не воспользоваться еще и санитайзерами? Да, они находят не все баги. Но с другой стороны, если где-то глубоко в коде вы, условно говоря, не делаете проверку на выход за границу массива, и санитайзер не ловит баг, может быть это просто потому что выше уровнем уже есть все нужные проверки и дополнительная приведет только к лишним тормозам? И даже без санитайзеров сборка проекта разными компиляторами на разных платформах с assert’ами, проверяющими инварианты вашего кода в стиле assert(obj->isValid), и хорошим fuzzing’ом найдет вам очень много чего. Грубо говоря, вопрос сводится к старому-доброму холивару на тему те-еретического и колхозного подхода к разработке.

Часто приводится аргумент, что 90% времени выполняется только 10% кода (что, насколько я понимаю, чисто эмпирическое правило — быстро найти строгих исследований на эту тему не удалось). Следовательно, бОльшую часть программы можно написать на безопасном Rust, а 10% «горячего» кода — на его unsafe подмножестве, и медленность текущей реализации Rust на самом деле не представляет собой проблемы. Ок, но тогда получается, что Rust вообще не нужен, потому что я могу написать 90% кода на Go, а 10% на Си. Только искатели серебряных пуль и оторванные от реальности те-еретики будут использовать Rust исключительно из соображений, что 100% программы можно написать как бы на одном языке. Хотя в действительности это два диалекта одного языка, что не так уж сильно отличается от связки Java плюс Си или Go плюс Си.

На самом деле, правило 10:90 — это все равно вранье. По этой логике можно переписать 90% WebKit, 90% VirtualBox или 90% GCC на Java и получить такой же результат. Очевидно, это не так. Даже если дело не в том, что в ряде программ это отношение очень другое, то следите за руками. Допустим, вся программа написана на небезопасном C/C++ и время ее выполнения, условно говоря, равно 0.9*1 (малая часть горячего кода) + 0.1*1 (много холодного кода) = 1. Теперь сравним с программой на безопасном языке со вставками на Си: 0.9*1 + 0.1*2 = 1.1, условно 10% разницы. Это много или мало? Зависит от ваших масштабов. В случае с Google даже несколько процентов могут сэкономить миллионы долларов (см пункт 5 в пейпере, «Utilization»). Или представьте, что со следующим обновлением JVM внезапно начнет требовать на 10% больше ресурсов! Я боюсь даже гадать, сколько нулей будет в цифре, полученной после перевода процентов на американские деньги! 10% — это дофига в задачах, где используются Си и C++.

Мы повторяем «преждевременная оптимизация — корень всех зол», как мантру. Но если следовать ей буквально, то давайте повсюду использовать пузырьковую сортировку вместо quicksort. Мы же не знаем точно, что программа будет именно в этом месте тормозить! Какой смысл оборачивать обыкновенные счетчики каких-то действий в акторы или транзакционную память, если можно сразу воспользоваться более эффективным atomic? И вообще, в тривиальных случаях нет смысла принудительно инициализировать все-все-все переменные, делать кучу дополнительных проверок и так далее. Пусть в итоге мы получим не 10% ускорения, а 2-5%. Это ведь тоже совсем неплохо, если потребовало всего лишь пары лишних минут размышлений. И как мы уже выяснили, в задачах, решаемых на С/C++, это может быть большой разницей! Потом, кто сказал, что найти горячее место, переписать код (возможно, очень много кода) и доказать, что он стал действительно быстрее — это проще, чем подумать о производительности заранее?

Если отвлечься от вопроса компромисса скорости и безопасности, то по дизайну самого языка у меня тоже есть вопросы. В частности, касательно пяти типов указателей. С одной стороны, это неплохо, когда программист задумывается о том, где лежат переменные, в стеке или куче, и могут или не могут с ними одновременно работать несколько потоков. Но с другой, представьте, что вы пишите программу, и оказалось, что переменная должна жить не в стеке, а в куче. Вы переписываете все, чтобы использовался Box. Потому вы понимаете, что на самом деле нужен Rc или Arc. Снова переписываете. А потом еще раз переписываете на обычную переменную в стеке. Все это — без нормальной IDE под рукой. И регулярки не помогут. Ну или же просто получаете ужасы в стиле Vec<Rc<RefCell<Box<Trait>>>>, привет, Java! Но что самое печальное, компилятор уже знает о времени жизни всех переменных, он мог бы выводить все эти Box, Arc и так далее автоматически. Но почему-то эта часть работы переложена на программиста. Намного удобнее было бы просто писать val (в третьем-то тысячелетии!), а там, где надо, явно указывать Box или Rc. Разработчики Rust в этом смысле запороли всю идею.

Из-за этого, в частности, сильно сужается область применения Rust. Никто в здравом уме не станет писать на таком языке веб и серверсайд. Особенно учитывая, что он не дает существенных преимуществ перед теми же языками под JVM. Да и Go с нормальными легковесными потоками (не футурами) для этих задач выглядит куда более привлекательным. С футурами, чтобы не прострелить себе ногу, нужно еще научиться работать, а вы говорите «безопасный язык». Да, у этих языков свои особенности, взять все тот же stop the world, но эта проблема решаемая, как распиливанием на микросервисы, так и другими приемами. И да, никто не будет транслировать Rust в JavaScript, писать на нем скрипты для раскладки в AWS, или использовать в качестве языка запросов к MongoDB. Под Android тоже вряд ли писать будут, но по другой причине — там сильно больше одной архитектуры, с JVM намного проще. Если вы вдруг думали, что Rust «подходит для всех задач», вынужден вас огорчить.

Ну и до кучи:

  • Макросы, как подпорка к излишней многословности, вызванной отсутствием нормальных исключений. Я уже писал о проблемах метапрограммирования, в частности, нормальную IDE для Rust мы вряд ли увидим из-за него. И я не уверен, но похоже, что у макросов в Rust даже неймспейсов нет.
  • Люди идиоты, а cargo очень поощряет стягивание пакетов напрямую из git-репозиториев, в обход Crates.io. В итоге велика вероятность получить такой же бардак с пакетами, как и в мире Erlang с его Rebar’ом. Другая проблема заключается в том, что когда GitHub лежит, а лежит он довольно часто, работа у программистов встает. К слову, в мире Go, похоже, такая же ситуация.
  • Как многие новые языки, Rust идет по пути упрощения. Я в целом понимаю, почему в нем нет нормального наследования и исключений, а также перегрузки операторов и прочего, но сам факт, что кто-то за меня решает такие вещи, оставляет неприятный осадок. C++ не ограничивает программиста в вопросах чем пользоваться, а чем нет.
  • Если уж идти по пути упрощения, то выкинуть бы уж все эти расширения языка. А то получается, как в мире Haskell, каждый программист пишет на своем диалекте.
  • Смарт поинтеры, если что, далеко не бесплатны и не приводят к предсказуемому времени сборки мусора. Какому-то потоку внезапно выпадает честь освободить очень глубокую структуру данных. Пока он ходит по лабиринту из мертвых ссылок, зависящие от него потоки терпеливо тупят. Та же проблема есть и в Erlang с его маленькими кучками, сам не раз наблюдал. Смарт поинтеры имеют и свои проблемы, ту же фрагменатцию памяти и утечки. Забыл викпоинтер в цеклической структуре, и все. И это в языке, претендующем на безопасность. Если хотите предсказуемого времени GC, либо изучайте поведение вашего приложения под нагрузкой, и предпринимайте меры (вспомним хотя бы те же пулы объектов), если время GC вас не устраивает, либо управляйте памятью вручную.
  • Кто-нибудь видел строгое описание семантики Rust? У него хотя бы memory model есть? Тоже мне «безопасный» язык, «доказывающий корректность» программ, который вообще-то может трактовать исходный код десятью разными способами, ха!
  • Не могу в очередной раз не напомнить, что проблема почти всегда в людях, а не в технологиях. Если у вас получается плохой код на C++ или Java вдруг тормозит, это не потому что технология плоха, а потому что вы не научились правильно ею пользоваться. Rust вы тоже будете недовольны, но уже по другим причинам. Не проще ли научиться пользоваться более популярными инструментами и начать их любить?

В общем и целом, ближайшие лет 5 я лучше будут инвестировать свое время в изучение C/C++, чем Rust. С++ — это промышленный стандарт. На этом языке успешно решают самые разнообразные задачи уже более 30 лет. А Rust и иже с ним — непонятные игрушки с туманным будущем. Про скорую смерть С++ разговоры идут как минимум с 2000-х, но писать на C/C++ за это время стали не меньше. Скорее наоборот. И мы видим, что язык развивается (C++11, C++14), для него появляются новые инструменты (вспомним хотя бы Clang, CLion и Biicode), и соответствующих вакансий просто куча.

Программист на C++ всегда без труда найдет себе работу с более чем достойной зарплатой, а при необходимости быстро переучится на Rust. Обратное очень и очень сомнительно. Кстати, язык, если что — далеко не единственный и не решающий фактор при выборе нового места работы. Кроме того, опытный программист на C/C++ без труда вонзается в исходники PostgreSQL, виртуальной машины Java или ядра Linux, использует мощные современные инструменты разработки, а также имеет в своем распоряжении множество книг и статей (скажем, по OpenGL).

Берегите свое время и здоровье, их у вас не так много, как кажется!

Дополнение: Схожие мысли по затронутой теме вы найдете в статьях раз, два, три, четыре и пять, а также Радио-Т 442.

Дополнение: Эта статья была переведена на английский и опубликована на viva64.com, а также blog.biicode.com. Кроме того, есть перевод на китайский.

Метки: , , .

Подпишись через RSS, E-Mail, Google+, Facebook, Vk или Twitter!

Понравился пост? Поделись с другими: