LINUX.ORG.RU

Сравнение Rust и C++ на примере трассировщика путей

 ,


7

2

Тут как-то была тема про то, что хочется нормальное сравнение C++ и Rust. Вот эта серия статей, как мне кажется, вполне себе кандидат:

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

Разработка трассироващика путей на Rust`е, часть 7: Заключение

Чтобы изучить Rust, я портировал свой спекртальный трассировщик путей Luculentus на язык Rust. Результат я выложил на Github. В процессе я также немного обновил Luculentus, переведя его на более современный C++. Детали вы можете прочитать в прошлых постах. В этом же посте я хочу подвести итоги и сравнить результаты.

Картинка

Для начала, пример вывода трассировщика путей! Захардкоженная сцена выглядит вот так:

http://ruudvanasseldonk.com/images/robigo-luculenta.png

Если вам интересно что-то в ней изменить, то смотрите set_up_scene в app.rs.

Начало работы с Rust

В настоящий момент, вы можете установить компилятор Rust`а и Cargo за пару минут, даже в Windows. Да и заставить их работать было намного проще, чем, например, Scala и sbt.

Сообщество Rust`а показалось мне очень дружелюбным. Когда я не знал что делать, мне очень помогали IRC канал и /r/rust. Члены основной команды разработки языка есть и там и там, так что часто советы были весьма профессиональными.

На сегодняшний день Rust все еще быстро меняется. Во время процесса портирования порядочное количество функций были объявлены устаревшими или переименованы и даже синтаксис немного изменился. Некоторые люди описывают сегодняшний Rust как совершенно другой язык, сильно отличающийся от того, что было год назад, но я не использовал язык настолько долго, что бы почувствовать это. Все изменения в языке делают его лучше и более последовательным и я уверен, что Rust 1.0 будет отличным языком.

Владение

Если бы мне надо было описать Rust одним словом, это было бы «владение». Для меня, это то, что отличает Rust от остальных языков. В большинстве языков владение неявно и это приводит к нескольким типам ошибок. Когда в Си функция возвращает указатель, кто ответственен за освобождение памяти? Вы можете ответить на этот вопрос без подглядывания в документацию? И даже если вы знаете ответ, то все равно легко забыть освободить память или освободить ее дважды.

Проблема относится не только к указателям, она касается все ресурсов. Может показаться, что сборка мусора это отличное решение, но она работает только для памяти. Тогда вам нужен другой способ для освобождения ресурсов (вроде файловых дескрипторов) и все проблемы возвращаются. Например, сборщик мусора в C# спасает от ошибок «использования после освобождения» (use after free), но ничего не спасает вас от ошибок «использования после удаления» (use after dispose). Разве ObjectDisposedException намного лучше сегфолта? Из-за явного времени жизни и системы владения в Rust нет этих типов ошибок.

прим. ozkriff: поскольку я с C# знаком мало, то пришлось загуглить про этот ObjectDisposedException. Вот пример кода:

using System;
using System.IO;

public class ObjectDisposedExceptionTest 
{
   public static void Main()
   {     
      MemoryStream ms = new MemoryStream(16);
      ms.Close();
      try 
      {
         ms.ReadByte();
      }
      catch (ObjectDisposedException e) 
      {
         Console.WriteLine("Caught: {0}", e.Message);
      }
   }
}

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

Обновление Luculentus

Сами по себе достоинства явного владения не уникальны для Rust`а. Такой же код можно написать на современном C++, который очень сильно отличается от С++ до-2011. Когда я писал Luculentus, C++11 поддерживался только частично. Я использовал много простых указателей, которые сегодня уже не нужны. Я заменил большинство простых указателей в Luculentus на shared_ptr или unique_ptr, а массивы на векторы. Как следствие, все ручные деструкторы пропали. (Раньше их было шесть). Раньше было 11 операторов удаления, теперь их нет. Все управление памятью стало автоматическим. Это сделало код не только короче, но и снизило вероятность появления ошибок.

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

Но все же, обновление демонстрирует, что возможно писать относительно безопасный код и на С++. Вы получаете безопасность и автоматическое управление памятью практически без накладных расходов. Единственной проблемой является то, что вы должны очень настойчиво стремиться к этому. Вы можете использовать unique_ptr, но так же можете и простой указатель. Все опасные инструменты «старого» С++ все еще доступны и вы можете смешивать их с новым С++. Конечно, есть определенная ценность в возможности собирать старый код (Бьерн называет это достоинством), но я бы предпочел не смешивать неявно эти две парадигмы и не поддерживать старые ошибочные решения. Требуется некоторое время, что бы разучиться использовать new и delete, но даже тогда старые API останутся с нами на очень долгое время.

Новое начало

Хорошим моментом в Rust является то, что он может начать все практически с чистого листа и учиться на ошибках старых языков. C++11 немного лучше своего предшественника, но он только добавляет новое и обязан поддерживать обратную совместимость. Одна из вещей, на которой это хорошо видно - синтаксис. В Rust, типы идут после имени и возвращаемый функцией тип идет после списка аргументов, что очень разумно. Синтаксис анонимных функций в Rust короток и в нем меньше повторения. Но я так и не могу привыкнуть к египетским скобкам, они до сих пор выглядят как-то не так.

Другим моментов, в котором Rust сделал правильный выбор, является мутабельность. В Rust`е все по-умолчанию неизменяемо, когда как в C++ наоборот. В коде Luculentus 535 раз появляется «const» (на момент написания). В Robigo Luculenta всего 97 «mut». Конечно, в C++ больше дублирования, но это все равно показывает, что неизменяемость по-умолчанию - лучше. Так же, компилятор Rust выдает предупреждение о переменных, которым не нужно быть изменяемыми, это тоже хорошо.

Несмотря на то, что синтаксис является делом вкуса, есть и измеряемые величины. Если я сравню количество непробельных символов в коде, то у С++ будет примерно 109 тысяч символов (не считая файлы, которые я не портировал на Rust), а у Rust - 74 тысячи. Почти на треть меньше.

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

error: binary operation `/` cannot be applied to type `core::iter::Map<'_,f32,f32,core::iter::Map<'_,&[f32],f32,core::slice::Chunks<'_,f32>>>`

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

Я добавил базовые счетчики производительности в Luculentus и Robigo Luculenta. Они считают количество завершившихся задач трассировки (trace tasks) в секунду. Вот результаты:

Компилятор              платформа           производительность

GCC 4.9.1*              Arch Linux x64      0.35 ± 0.04
GCC 4.9.1               Arch Linux x64      0.33 ± 0.06
rustc 0.12 2014-09-25   Arch Linux x64      0.32 ± 0.01
Clang 3.5.0             Arch Linux x64      0.30 ± 0.05
msvc 110                Windows 7 x64       0.23 ± 0.03
msvc 110*               Windows 7 x64       0.23 ± 0.02
rustc 0.12 2014-09-23   Windows 7 x64       0.23 ± 0.01

Везде выставлены самые высокие уровни оптимизации. Компиляторы со звездочкой использовали PGO (Profile-guided optimization - оптимизация, управляемая профилированием https://ru.wikipedia.org/wiki/Profile-guided_optimization). Единственный вывод, который я могу сделать из этого, что вам, наверное, не стоит использовать Windows для сильно нагружающих процессор приложений.

Во второй статье из этой серии я отметил, что код на Rust`е собирается очень быстро, но тогда было не много кода. Сейчас время сборки вот такое (в секундах):

Компилятор             Время

rustc 0.12 2014-09-26  7.31 ± 0.05
Clang 3.5.0            13.39 ± 0.03
GCC 4.9.1              17.3 ± 0.5
msvc 110               20.4 ± 0.3

Сборка теперь не так быстра, но все равно быстрее С++.

Заключение

Изучать Rust было интересно. Мне понравился язык и портирование привело к нескольким озарениям, которые могут улучшить и оригинальный код. Владение часто неявно в других языках, что увеличивает чувствительность кода к человеческим ошибкам. Rust делает владение явным, убирая возможность допущения подобных ошибок. Все безопасно по умолчанию. Все это сдвигает Rust намного ближе к краю «стабильность» на спектре, чем к краю «быстрая разработка». Я не написал на Rust`е достаточно кода, что бы быть на 100% уверенным, но пока что достоинства Rust`а перевешивали его недостатки. Если бы я выбирал между C++ и Rust`ом для своего следующего проекта, то выбрал бы Rust.

Ну как, это тянет на «нормальное» сравнение? По-моему, в любом случае интересно почитать.


Ответ на: комментарий от hateyoufeel

но ICC действительно генерирует более тормозной код (без SSE, например),

Да я в курсе. Не здорово, но и не диверсия, как некоторые любят заявлять.

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

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

Можно подумать, что в С++ не так же. Компилятор ведь умеет очень многое оптимизировать - те же лямбды и т.д.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Издеваюсь. Имхо, проблемы с потерянными указателями и проч. были решены первыми же версиями С++, но существуют благодаря лени программистов. Почему-то все новые языки преподносятся как решение этой проблемы, хотя административных мер вполне достаточно. А может просто я старый и ленивый, а новые языки уравнивают с молодью в борьбе за место под настольной лампой, поэтому язвлю, фик знает.

ilovewindows ★★★★★
()
Ответ на: комментарий от ilovewindows

административных мер вполне достаточно

Действительно, за каждый потерянный указатель - штраф к зарплате. За каждый сегфолт - удар плетью.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Ну если уж выбор изобретать новый язык или плетью, то дешевле плетью. Хотя выделять место например для строчки специально заточенной функцией и другой специально заточенной удалять избавляет на сто процентов от повторного удаления, неудаления и позволяет найти виновника торжества с точностью до строчки кода. Но да, славы такая рутина не приносит.

ilovewindows ★★★★★
()
Ответ на: комментарий от ilovewindows

Если бы все руководствовались твоей логикой, мы бы до сих пор писали на фортране.

hateyoufeel ★★★★★
()
Ответ на: комментарий от ozkriff

Почему? Вот код на плюсах, если что: https://github.com/ruud-v-a/luculentus.

Потому что открываю первый попавшийся файл Camera.cpp и наблюдаю там такое:

Ray Camera::GetScreenRay(const float x, const float y,
const float chromaticAberrationFactor,
const float dofAngle,
const float dofRadius) const
{
float screenDistance = 1.0f / std::tan(fieldOfView * 0.5f);

причем fieldOfView поле класса Camera и лежит в Camera.h

 /// Horizontal field of view in radians.
float fieldOfView;
Мне лень разбираться в тамошнем дизайне, моэет эта функция вызывается редко, но все равно есть некие правила хорошего тона при написании подобных шняг. Аффтор видно не в курсе что деление дорогая операция, а тангенс совсем дорогая операция, и можно в данном случае их вынести в сеттер.

Или такое

 // field at all, so it is a hack anyway).
Vector3 lensPoint =
{
std::cos(dofAngle) * dofRadius,
0.0f,
std::sin(dofAngle) * dofRadius
};
про sincos аффтор тоже не слышал.

Или скажем MontecaloUnit.cpp:

Vector3 MonteCarloUnit::GetHemisphereVector()
{
// First, generate polar coordinates in a hemisphere.
float phi = GetLongitude();
float theta = GetLatitude();
// Calculate the direction based on the polar coordinates.
Vector3 v =
{
std::cos(phi) * std::sin(theta),
std::sin(phi) * std::sin(theta),
std::cos(theta),
};
return v;
}
Эта функция вызывается ОЧЕНЬ часто (если я хоть что то понимаю в трассировке), и ее можно ускорить примерно в 2.5 раза. Вывод - аффтор понятия не имеет о том, как оптимизируют код с т.з. производительности. Вообще не имеет понятия, от слова совсем.

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

AIv ★★★★★
()

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

intelfx ★★★★★
()
Ответ на: комментарий от DarkEld3r

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

Можно подумать, что в С++ не так же

В первой версии Си++ так не было :)

tailgunner ★★★★★
()
Ответ на: комментарий от AIv

О боже, кресты...пытаюсь въехать в последний стандарт после всяких шарпов, питонов и яв и от каждой мелочи умираю внутри.

Автор жалуется на 100500 констов

Ray Camera::GetScreenRay(const float x, const float y,
const float chromaticAberrationFactor,
const float dofAngle,
const float dofRadius) const
{
float screenDistance = 1.0f / std::tan(fieldOfView * 0.5f);

Тут вообще есть ПРАКТИЧЕСКИЙ смысл в таком количестве const keyword'ов кроме как для документирования кода для сторонних читателей? Причем я подозреваю, что даже для документирования это слабо нужно, т.к. в коде вроде бы однозначно видно, что меняется а что нет

Midael ★★★★★
()
Ответ на: комментарий от AIv

Потому что открываю первый попавшийся файл Camera.cpp и наблюдаю там такое:

А открыть мой файл и прочитать 10строчек?

В такого рода вещах файлов «*.cpp» быть не должно.

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

Там проблема далеко не в этом, а в с++. Даже гцц не умеет нормальной инлайнить всю эту классовонаследовательную лапшу.

про sincos аффтор тоже не слышал.

Гцц слышал - он это умеет.

Эта функция вызывается ОЧЕНЬ часто (если я хоть что то понимаю в трассировке)

Относительно всего остального - капля в море.

и ее можно ускорить примерно в 2.5 раза

Вероятность крайне мала.

Вывод - аффтор понятия не имеет о том, как оптимизируют код с т.з. производительности.

Аффтор невероятный клоун, который обошел меня на пару порядков. Достаточно посмотреть как он собирает свою программу, при этом сравнивая скорость конпеляции.

Ну собственно такая ЦА и нужна русту.

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

Ну все этим грешат. Откуда он может знать, что он не оптимальный?

В таких задачах правильно написанный код на gcc может подходить довольно близко к теоретическому пределу производительности.

На С++ с дефолтным стилем написания? Маловероятно.

А кто посчитает эту теоретическую производительность, при этом правильно?

Я говорил про 15-20гигов, которые изи снимаются на 2-х каналах на одном-двух вёдрах. Но почему-то это не особо интересно пацанам. Когда как пацан чисто теоретически на одном ведре может снять 3-7 гигафлопс на ведро при том, что на точку надо флопсов 6-8 минимум - т.е. 4гига отсилы.

По поводу вины С++ - я даже не поленился, пошел и помог гцц.

#!/bin/bash

for file in *.h; do
base_name=`basename  $file .h`
header_file=$file
cpp_file=$base_name".cpp"
if [ -a $cpp_file  ]; then
  sed -e s/\#include\ *\"$header_file\"// -i $cpp_file
  echo "#include \"$cpp_file\"" >> $header_file
fi
done 

$ g++ Main.cpp -pthread `pkg-config --cflags gtkmm-3.0` `pkg-config --libs gtkmm-3.0` -std=c++11 -Ofast -Wall -Wextra -march=native -finline -finline-functions -funroll-loops -funroll-all-loops -fwhole-program

//.45 .55 - до
//.77 .87 - после.

Можно попробовать выпилить виртуальй вызов, который гцц не осилил.

Собственно тут проявляются все основные проблемы крестов. Дробление всего и вся на классики и рассовывание этого добра по файликам. Ради мифической «скорости конпеляции» ну и адепты дедовской методы. Ах да, этот файл на 250тысяч строк собирается 2секунды, когда как раздробленный на файлы 10(ну если собирать это нормально будет 2-4).

Когда как это наповал убивает возможно нормальной оптимизации и главное инлайна.

Всякие наследования с верой в оптимизатор - не работает.

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

Это противоречит 2-м основным столпам производительности - чистый поток независимых полезных инструкций и векторизация(т.е. полностью подход, а не мистическая вера в атовекторизациию и деревенскую векторизацию).

Т.е. на рваная взаимозависимая пронизанная калами лапша упирается в латенси, а это 10-30% от трупута. А собственно отсутствие векторизации - это те же отсилы 10-20% производительности. Т.е. пацан снимает 10-30% от 10-20%.

Ничего не могу сказать за Rust в этом плане, но меня терзают смутные сомнения - все таки над ним работают гораздо меньше чем над gcc...

Там llvm, т.е. производительность рядовой лапши на уровне шланга.

anonymous
()
Ответ на: комментарий от Midael

Тут вообще есть ПРАКТИЧЕСКИЙ смысл в таком количестве const keyword'ов кроме как для документирования кода для сторонних читателей?

В конст нет никакого смысла. Крестовики их ставят из-за синергии 2-х причин. Веры в то, что это из защитит от какой-то мистической ошибки и заодно «читемей» ну и главное - вера в то, что это помогает какой-то там оптимизации. Потамучто какой-то вася так сказал.

Собственно помощи в нормальном коде от неё ровно 0, а рядовом говне в районе нуля.

Причем я подозреваю, что даже для документирования это слабо нужно, т.к. в коде вроде бы однозначно видно, что меняется а что нет

А зачем там нужны классы? Наследование? Управление памятью и прочая мура о которой он там балаболил. Незачем, просто пацан апогей бездарности и как и любая личность такого типа хочет быть в тренде.

Т.е. ничего, кроме подражания за этим не стоит.

anonymous
()
Ответ на: комментарий от ilovewindows

хотя административных мер вполне достаточно.

Это каких? Опять же, не автоматизируемые меры не катят. Да и дело не только в borrow checker, хотя он и очень полезен. Но в расте, на мой взгляд, очень многое сделано грамотно. Скажем по результату, который аналог плюсового optional мы должны матчить, следовательно не можем игнорировать ошибки и т.д. И тут борьба не только с нубами, банальную невнимательность никто не отменял. И даже ревью и тесты не всегда спасают.

Ну и такой момент - мне нравится С++. Но очень многое в нём - это дань совместимости. В итоге многое коряво и не может быть исправлено без добавления новых сущностей. Да и сами «сущности» вынуждены быть более громоздкими. Если добавят модули, то «старые» инклюды никуда не денутся. Макросы, если доживём, всё равно будут соседствовать с дефайнами. И чем дальше, тем такого больше. Язык будет сложнее изучить, сложнее разобраться в комбинировании кучи сущностей и т.д.

DarkEld3r ★★★★★
()
Ответ на: комментарий от tailgunner

В первой версии Си++ так не было :)

Ну так первая версия уже никому и не интересна. (:

DarkEld3r ★★★★★
()
Ответ на: комментарий от AIv

Ничего не могу сказать за Rust в этом плане, но меня терзают смутные сомнения - все таки над ним работают гораздо меньше чем над gcc...

Дык, ты сравниваешь компилятор и язык. Над LLVM работают не меньше, чем над GCC.

А расту для начала надо дизайн утрясти, а потом уже оптимизировать.

DarkEld3r ★★★★★
()
Ответ на: комментарий от Midael

Тут вообще есть ПРАКТИЧЕСКИЙ смысл в таком количестве const keyword'ов

Смысл примерно такой же, как в растовской иммутабельности по умолчанию. Это удобно. Случайно не изменишь. А кода может быть много. Решать это именованием, как в некоторых языках, убого.

Кстати, это вполне себе пример неудачного момента С++, который никогда не «исправят».

DarkEld3r ★★★★★
()
Последнее исправление: DarkEld3r (всего исправлений: 1)
Ответ на: комментарий от unt1tled

Фу ... выглядят как

поставь розовый фон, тебе должно выглядеть лучше

anonymous
()
Ответ на: комментарий от AIv

все равно есть некие правила хорошего тона при написании подобных шняг. Аффтор видно не в курсе что деление дорогая операция

«This is not meaningful if you do not say which CPU are you using. Division is not that expensive anymore, and the extra code for implementing division with bitwise operators could very well be MUCH slower.» (с)

Автор в курсе, просто ты слоу.

anonymous
()
Ответ на: комментарий от slackwarrior

Это не так. Программисты не гентушники — нам крайне редко приходится пересобирать весь проект. Даже самые больше правки редко охватывают больше десятка модулей, что редко превышает нескольких минут при компиляции.

Stahl ★★☆
()
Ответ на: комментарий от Stahl

Программисты не гентушники — нам крайне редко приходится пересобирать весь проект.

Крайне редко программисты в больших проектах пересобирают руками. А теперь глядим на билдсервера, и удивляемся... Как б-ть долго все конпеляется под N-систем и архитектур, если тому, кто их настраивал, пофиг на время конпеляции - и сука работа остальных простаивает, потому что где-то в середине сборки что-то упало, этот дятел забыл, что у него на билд-серверах N-систем, а не уютненькая, на которой он лучше всего надрочился дрочить на оптимизации :)

slackwarrior ★★★★★
()
Последнее исправление: slackwarrior (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.