Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Archive for the ‘Programming’ Category

Не нравится ООП? Делайте свой язык программирования!

25 комментариев

В интернете нынче модно говорить что «ООП это шлак», и многие мечтают сделать свой собственный язык программирования но чего-то боятся. А на самом деле, если подойти с умом, тут все просто. Серьезно! Я знаю, вы хотите мне возразить, что дескать…

  • Писать свой компилятор в native code/IL/bytecode слишком сложно — а и не надо!! Вы понимаете, что существующие компиляторы вроде С или Java оттачивались годами, сотнями людей? Почему бы не воспользоваться всем этим богатством, компилируя ваш язык в тот же С/C++, и потом получая от С/C++ компилятора все оптимизации и плюшки?

  • Меня не прет идея возиться с кастомными форматами файлов для лексинга и парсинга, это депрессивно — а и не надо!! Просто возьмите фреймворк, который поддерживает парсеростроение прямо в коде.

  • У меня сейчас весь код на Java/C#/C++ написан, как я сделаю interop? — да очень просто, ведь с подходом транскомпиляции, вы можете транслировать свой язык в любой из вышеперечисленных, генеря и потом потребляя любой интерфейс, причем в обе стороны.

  • Язык-то я сделаю, а как насчет поддержки языка в моей любимой IDE? — о, ну это уже высший полет. Для начала, постарайтесь сделать лаконичный маленький язык, для которого инструменты не критичны. А потом можно научиться и тулы поддерживать (или писать свои).

Ну что, убедил?

Чтобы показать насколько это все просто, вот небольшой пример: представим, что вы хотите расширить определение С-образной глобальной функции, добавив следующие фичи:

  • Определить свой набор «коротких» типов переменных, вроде i32 или f64 на замену int и double.

  • Передавать аргументы в формате x,y : i32, то есть переиспользуя определение типа для нескольких сразу.

  • Добавлять в тело функции определения переменных вроде x = 5 так чтобы, при условии что x не имя параметра, это преврашалось в полноценную декларацию переменной, а иначе просто присваивалось значиние.

Для начала такой фичесет подойдет? Я знаю что мало, но я тут и не пытаюсь целый язык сделать. Вот как это будет выглядеть:

Наш язык… …станет вот этим
void foo(x,y:i32, z:f64)
{
  x = 5;
  w = 123;
}
void foo(int32_t x, int32_t y, double z)
{
  x = 5;
  int w = 123;
}

Структуры для языка

Во всех языках есть механизмы построения парсеров. Я возьму C++ и Boost.Spirit, для примера, но вообще язык тут особого значения не имеет. Для начала давайте сделаем новые типы вроде f32 вместо float:

struct numeric_types_ : qi::symbols<wchar_t, wstring>
{
  numeric_types_()
  {
    add(L"i32", L"int32_t");
    add(L"f32", L"float");
    add(L"f64", L"double");
  }
} numeric_types;

Теперь определяем функцию:

struct function
{
  wstring name;
  vector<parameter> params;
  vector<assignment_statement> assignments;
 
  // поиск параметра по имени; реализация банальна
  boost::optional<const parameter&> find_parameter(const wstring& name) const;
};

У любой функции есть имя, она берет сколько-то там параметров (ну, деклараций параметров, но краткость сестра таланта), и у нее есть тело, которое в нашем случае будет состоять исключительно из присвоений значений переменным (не очень практично, I know).

Поскольку мы умеем определять аргументы в стиле x,y:i32, т.е. несколько с одним типом, собственно структур parameter мы определим вот так:

struct parameter
{
  vector<wstring> names;
  wstring type;
};

Ну и наконец присвоение значений переменным можно сделать вот так:

struct assignment_statement
{
  wstring variable_being_assigned;
  wstring value;
  wstring infer_type() const
  {
    if (value.find(L'.') == wstring::npos)
      return L"int"s;
    return L"float"s;
  }
};

Выше показан г~код для определения типа, думаю вы понимаете что в последствии это можно пофиксить.

Всё, структуры готовы, можно строить парсер. (На самом деле есть еще этап их адаптации через Boost.Fusion, но это implementation-specific деталь, если что гляньте в сорцы.)

Парсер

Парсер для нашего языка написать настолько легко что я просто приведу весь код целиком, а потом мы его обсудим, ок?

template<typename Iterator>
struct function_parser : qi::grammar<Iterator, function(), space_type>
{
  function_parser() : function_parser::base_type(start)
  {
    using qi::lit;
    param %= 
      +alnum % ','
      >> ':'
      >> numeric_types
      >> -char_(',');
    assignment %=
      +alnum
      >> '='
      >> +alnum
      >> ';';
    start %= lit("void ")
      >> +(char_ - '(')
      >> '('
      >> *param
      >> ')'
      >> '{'
      >> *assignment
      >> '}';
  }

  qi::rule<Iterator, parameter(), space_type> param;
  qi::rule<Iterator, assignment_statement(), space_type> assignment;
  qi::rule<Iterator, function(), space_type> start;
};

Для начала, вот эти qi::rule просто говорят парсеру как то что он распарсит ложится на структуры что мы определили ранее. Например, вот хочется распарсить присваивание вроде x = 3, что это? Это идентификатор (то есть, 1 и более alphanumeric символов), потом =, потом еще раз набор символов и в конце ;.

Конкретно в Boost.Sprit, в отличии от регулярок, «один и более» записывается как + до типа символа, т.е. +alnum. То есть + означает «один и более», * — «сколько угодно», и так далее. Вот и получается что присваивание мы распарсили, а поскольку наш qi::rule мэпит его на assignment_statement, поля этой структуры будут присвоены автоматически. Это гениально, или как?

То же и с другими частями языка. Хочешь распарсить несколько переменных через запятую и запихнуть их в вектор? Пишем +alnum % ',' где оператор % – это как сказать *(+alnum >> ','), только короче. Что тоже удобно.

Так вот, парсер у нас готов, можно парсить. На Spirit это делается вот так:

template<typename Iterator>
wstring parse(Iterator first, Iterator last)
{
  using boost::spirit::qi::phrase_parse;
  function f;
  function_parser<wstring::const_iterator> fp{};
  auto b = phrase_parse(first, last, fp, space, f);
  if (b)
  {
    return render(f);
  }
  return wstring(L"FAIL");
}

…где render() – это функция которая обходит то что мы напарсили и генерит из этого чистейший, готовый к компиляции С (никто не мешает вам выводить сразу в N разных языков).

Pretty print

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

inline wstring render(const function& f)
{
  wostringstream s;
  // name of the function (assume void-returning)
  s << "void " << f.name << "(";

Потом выводим параметры, не забывая что у нас может быть несколько имен для одного и того же типа аргумента:

// each of the parameters
const auto param_count = f.params.size();
for (auto i = 0u; i < param_count; ++i)
{
  auto& p = f.params[i];
  for (int j = 0; j < p.names.size(); ++j)
  {
    s << p.type << " " << p.names[j];
    if (j + 1 < p.names.size()) s << ", ";
  }
  if (i + 1 < param_count) s << ", ";
}
s << ")\r\n{\r\n";

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

// each of the assignments
const auto assign_count = f.assignments.size();
for (auto i = 0u; i < assign_count; ++i)
{
  s << "  ";
  auto& a = f.assignments[i];
  auto type = a.infer_type();
  bool is_param = f.find_parameter(a.variable_being_assigned) != boost::none;
  if (!is_param)
    s << type << " ";
  s << a.variable_being_assigned << " = " << a.value << ";\r\n";
}
s << "}";
return s.str();

Вот собственно и всё. Тут в принципе можно реализовать «полноценный» Visitor, если хочется.

Заключение

Как вы поняли, тут остался шаг компиляции полученного кода — думаю всем итак очевидно как это делать, это зависит от языка который вы нагенерили. Вообще я ратую за «портативный» C/C++, но решать в конечном счете вам.

Если хотите сорцы проекта, они тут. Мой пример на С++, но вы можете реализовать свой язык на чем угодно. Мораль в том что создать сейчас свой кросс-компилируемый язык легко, поэтому вместо того чтобы ныть про ООП и воевать с ветряными мельницами, проще сесть и запилить что-то своё. Так что садитесь и пишите спеку вашего чудо-юдо языка. Удачи!

Written by Dmitri

2 сентября 2016 at 16:10

Опубликовано в Programming

Tagged with , ,

Плохие и хорошие идеи в языках программирования

12 комментариев

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

На самом деле нет. Как 5 лет назад было нормой 2-4 ядра, так и сейчас. И да, я знаю что есть процы подороже и посерьезнее, только вот косяк: подавляющее большинство людей нынче пользуют скорее макбуки нежели десктопные РС, а на них сколько ядер? 12? 16? Древнее индейское поселение фигвам.

Поэтому в этом посте немного троллинга “современных” языков. При этом я не жалуюсь, у меня все работает. Просто мысли в слух.

Дорогая, мы продолбали операторы

Даже не операторы. Вы на раскладку клавиатуры смотрели? Что это за !@#$%^&*()_+ мля? Я вообще не понимаю почему у нас на главном органе компьютера такой вагон легаси. Ладно, я вас не прошу с QWERTY следать, но хоть мусор-то можно было убрать?

Я сотственно вот о чем:

  • Единственный вменяемый оператор в ЯП это +. Всё. Кроме плюса все остальное – бред.

  • Минуса на клавиатуре нет вообще. Есть тире - и я тут не пытаюсь быть педантом или типографилом, я просто говорю что использовать тире как минус это вообще неправильно.

  • Деление и умножение тоже продолбано. С какого перепугу “звездочка” это умножение? Умножение это ×. Насчет деления конечно можно поспорить, может / без полноценного фронт-енда a la Mathematica — это приемлимо, но я не уверен.

  • Оператор = никаким образом не означает присваивание. Коротко, Delphi, Mathematica и прочие – правы, нужно разделять присвоение и определение, если вы уж решили использовать = для записи значения в переменную. Я конечно склонен думать что x := 2 это первичное определение переменной (даже auto/var не нужен) а для записи значения подойдет x <- 3 или 3 -> x (это кстати подход в F#, VHDL).

  • Использование = для сравнения — абсолютно нормальная практика (т.е. if (x = 3)) если бы не одно но — числа с плавующей точкой так сравнивать нельзя т.к. нужно указывать погрешность. Я бы, может, и не отказался вместо abs(x-y) < tol писать что-то вроде x =1e-10= y хоть это и выносит порядочно мозг.

  • Вот эта галочка ^ это либо оператор возведения в степень (ну или **), либо ничего. XOR из нее сделали безумцы.

  • Скобки продолбали все и полностью. Вы понимаете, что в математике разные скобки ([], {}, ()) все имеют право на жизнь и используются чтобы не запутаться в сложных выражениях? Так тогда зачем квадратные скобки узурпировали для массовов а фигурные — для scope? А представьте ЯП где можно использовать любые для выражений, плюс компилятор проверяет согласованность.

  • Вообще в математике оператор × можно опускать, и я склонен думать что выражение 3x в том же С++ или C#, при желании, нельзя мизинтерпритировать как ничто кроме 3×x. Также сюда можно приплести units of measure, чтобы можно было писать 52m вместо 52 mm. (И да, я понимаю что непонятно xy это переменная или x*y, но можно же из контекста выводить? Или опасно?)

  • F# узурпировал prime mark ' как символ, но я бы его взял как транспозицию, как делает MATLAB.

  • А еще я бы разрешил произвольные названия переменных, только не как в F# (ВОТ ТАК) а что-то вроде:

    struct Values { #1, #2:i32 }
    Values v; print(v.2)
    

    Кстати, писать v.2 можно в Rust, тоже неплохая возможность.

Косяки с именами типов в C++ и не только

С++ ставит рекорд по полному факапу системы типов. Что такое unsigned long long? Никто не знает, поэтому мы и используем <cstdint>, но можно лучше (спасибо Rust):

  • Целочисленные типы = знаковость (i/u) + размер (сколько вешать в битах), например i32, u64 итд.

  • Для чисел с плавающей в луже точкой просто делаем f32/f64 (спасибо за вынос мозга, F#). Эй SG14, где там 16-bit float?

  • Если вам вдруг нужны пакованые SIMD типы, их можно добавить как p128/256/512. Но это экзотика. Придется вводить векторные операторы (.+, .*, и так далее) как это сделал MATLAB.

  • Использование этих коротеньких типов как постфиксы (var z = 3.0f32) — гениально, спасибо Rust.

  • Целочисленные переменные isize/usize с размером, равным слову проца — это очень умно, опять же спасибо, Rust. И что еще более гениально, так это то, что в Rust индекс массива — unsigned. А в С++ вполне можно написать x[-1] т.к. это всего лишь смещение указателя.

Поддержка векторно-матричных операций

Если все так пекутся о многопоточности и векторизации, почему нигде кроме MATLAB никто не напрягся впаять вектора/матрицы в язык? Более того, я бы предложил:

  • Улучшенную инициализацию, то есть

    X = [1 2; 3 4]; // instead of {{1,2},{3,4}
    X = [ 1 2
          3 4 ]; // also works, kind of
    

  • Вменяемые скалярные и векторные произведения, как сделал MATLAB — X*Y и X.*Y как векторное и Адамарово произведение соответственно.

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

  • Статические и динамические размеры. Очевидно но все же, хочется хороший API. То, что сейчас в С++ это адъ.

И ещё…

Ладно C# запилил observer pattern как event (немного неуклюже, конечно), но было бы неплохо получить binding operator =:= который жестко связывает два значения. А там и до резолва сложных выражений недалеко. Вообщем в современном мире это актуально.

Да, и неплохо было бы оставить операторы < и > в покое и не использовать их для шаблонов. Хотите шаблоны? Используйте « и » и мне плевать что этих символов нет на клавиатуре, т.к. клавиатуры ущербны по определению. И нет, я не предлагаю адъ вроде APL, я скорее предлагаю просто делать уже языкам графические front-end’ы как Mathematica.

Но об этом в следующем посте. ■

Written by Dmitri

4 июля 2016 at 20:40

Опубликовано в Programming

Tagged with

Попытки отражения garbage-collected структур на ужасный неуправляемый С++

Если вы вдруг решили сконвертировать C# в C++, то большинство задач конверсии – тривиальны. int меняется на int32_t, string на std::wstring (хотя на самом деле просто string для большинства людей которым unicode не нужен). Ну и так далее. Различия в большинстве случаев несущественны.

Основной задачей, как ни странно, является конверсия reference типов, т.к. в C# можно создавать массивы, дженерики и просто ref-типы и пускать все на самотек дескать “когда-нибудь освободится”, а вот на C++ так делать нельзя. Напомню, что в С++ есть вот такие варианты:

  • Аллоцировать вещи на стеке. К сожалению, за исключением примитивных типов, в C# никто так не делает. Чтобы C#ный объект который кто-то аллоцировал через new аллоцировать в С++ коде на стеке, нужно быть увереным что объект маленький и что он действительно только вот тут, в локальном scope, что его никто не потребляет. Зато стек почистится по мере выхода их скоупа, что как бы неплохо.

  • Аллоцировать вещи вручную через new/delete: это нереально в принципе т.к. если идти по этому пути, то у каждого типа может быть свой набор ссылок на весь мир, ну и… вы поняли. Получится ад. Не вариант.

  • Использовать умные указатели так, как их нужно использовать. То есть например если у вас есть фабрика которая порождает объекты и выдает их наружу, то вы можете возвращать unique_ptr. И так далее. Одна лишь проблема: как понять какой указатель нужен? Опять нужен глубинный анализ, а это сложно.

  • Использовать везде shared_ptr, т.к. это как раз тот указатель, который можно либерально копировать повсюду и при этом не получить ситуацию когда вы пытаетесь вызвать что-то у объекта, который уже был перемещен куда-то.

Инициализация и присваивание

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

class Person
{
  string name;
public:
  Person(const string name) : name(name) {}
};

Опаньки! А мы-то думали что все не-примитивные объекты будут shared_ptr. В результате получается, что

  • Все типы которые мы считаем “маленькими” (а это включает всякие int, float и так далее) передаются by value

  • Строки и прочие типы которые нормально себя ведут в нынешней системе типов мы передаем как const ref

  • А вот все остальное – да, проблуем использовать shared_ptr

Полноценный пример

Давайте типичный пример с человеком у которого есть адрес:

struct Address
{
  int HouseNumber;
  string StreetName;
  Address(int houseNumber, const string streetName)
    : HouseNumber(houseNumber), StreetName(streetName) {}
};

Я намеренно использую struct и C#-ное именование а также не использую properties т.к. автосвойство C# можно смело конвертировать в поле C++ without loss of generality. Так вот, у нас есть адрес который берет const string во втором аргументе и это как бы правильно.

Теперь посмотрим на человека, который обладает адресом:

struct Person
{
  int Age;
  string Name;
  shared_ptr&lt;Address&gt; HomeAddress;
  Person(int age, const string name, shared_ptr&lt;Address&gt; homeAddress)
    : Age(age), Name(name), HomeAddress(homeAddress) {}
};

Вроде тоже неплохо. Сразу понятно что подсовывать Address нам не нужно, и что нам нужна “управляемая” версия. Теперь все это можно потреблять:

auto address = make_shared&lt;Address&gt;(221, &quot;Baker St&quot;);
auto person = make_shared&lt;Person&gt;(40, &quot;Sherlock&quot;, address);
cout &lt;&lt; person-&gt;Name &lt;&lt; endl;

Более того, мы можем не нервничать насчет удаления address если его “хозяин” надоел и его грохнули:

shared_ptr&lt;Address&gt; addr;
{
  auto address = make_shared&lt;Address&gt;(221, &quot;Baker St&quot;);
  auto person = make_shared&lt;Person&gt;(40, &quot;Sherlock&quot;, address);
  addr = person-&gt;HomeAddress;
}
cout &lt;&lt; addr-&gt;StreetName &lt;&lt; endl;

Ну ок, вроде оно как-то работает. Хотя тут мне подумалось… а ведь по идее можно и адрес было передавать как const shared_ptr<Address>, не так ли?

Ладно, что там следующее?

Массивы

Массивы – это вообще больное место. В C# есть два разных типа – прямоугольные [,] и угловатые [][], в С++ существует только 2й вариант. Но проблема даже не в этом. Проблема в том, как вообще представить N-мерный массив чего либо через умный указатель.

Начнем с простого: у человека несколько имен и это как-то нужно хранить. Тут, самое безопасное – это vector<string>. В принципе, со строками мы можем себе позволить такой вот расклад когда мы копируем by value. Но как насчет набора адресов?

Одномерный набор адресов в C# подразумевает конверсию как List<T> так и T[] в vector<shared_ptr<T>> – конечно, для тех типов где shared_ptr нужен, т.е. не для для примитивов или string:

struct Person
{
  int Age;
  vector&lt;string&gt; Names;
  typedef vector&lt;shared_ptr&lt;Address&gt;&gt; AddressCollection;
  AddressCollection Addresses;
  Person(int age, const vector&lt;string&gt; names, const AddressCollection addresses)
    : Age(age),
      Names(names),
      Addresses(addresses)  {  }
};

Ну а тут можно воспользоваться любезно предоставленым typedef’ом и проинициализировать все это счастье в стиле С++11:

auto person = make_shared&lt;Person&gt;(23, 
  vector&lt;string&gt;{&quot;Janes&quot;, &quot;Jimmy&quot;},
  Person::AddressCollection{ 
    make_shared&lt;Address&gt;(123, &quot;Hull Rd&quot;), 
    make_shared&lt;Address&gt;(23, &quot;Sesame St.&quot;)});
for (auto addr  : person-&gt;Addresses)
{
  cout &lt;&lt; addr-&gt;StreetName &lt;&lt; endl;
}

А что же насчет std::array или например boost::shared_array? Ну, std::array на самом деле требует (сразу!) известную длину. То есть если у вас в C# написано например что int i = new int[3] то тогда да, вам повезло:

struct Person
{
  array&lt;shared_ptr&lt;Address&gt;, 2&gt; HomeAndWorkAddresses;
  explicit Person(const array&lt;shared_ptr&lt;Address&gt;, 2&gt; home_and_work_addresses)
    : HomeAndWorkAddresses(home_and_work_addresses)
  {
  }
};

От кода выше в принципе уже должно тошнить, но для тех кто не понял в чем соль, вот то, как выглядит инициализация сего счастья:

array&lt;shared_ptr&lt;Address&gt;, 2&gt; addresses = { {
  make_shared&lt;Address&gt;(123, &quot;London Rd&quot;),
  make_shared&lt;Address&gt;(23, &quot;Shirley Ave&quot;)
} };
auto person = make_shared&lt;Person&gt;(addresses);
cout &lt;&lt; &quot;Work: &quot; &lt;&lt; person-&gt;HomeAndWorkAddresses[1]-&gt;StreetName &lt;&lt; endl;

Нда, мне определенно не нравится std::array. В большинстве случаев, мне кажется, лучше всего использовать vector<T>, vector<vector<T>> и так далее.

Полузаключение

На самом деле, то, что я описал – это верхушка айсберга. Например, допустим что мы допускаем хранения vector<string> но ведь по сути дела для передачи этой коллекции куда либо (а мало ли зачем?) мы не можем передавать ее by value (это расточительно!), и с другой стороны, она не shared_ptr<T>, то есть единственный наш вариант – это передавать ее как const vector<T>. Хмм, а вообще это не такая уж и плохая идея.

Вообщем, в этом посте я для себя вывел набор (вполне вероятно ошибочных) правил по тому, как конвертировать reference-конструкты C# в нечно С++ное чем можно хоть как-то управлять. Возможно в последствии построить поверх всего этого какие-то дополнительные оптимизации. Посмотрим…

Written by Dmitri

30 января 2015 at 1:20

Опубликовано в Programming

Tagged with , ,

Воскрешение языка С++

23 комментария


Сдается мне, что на всей нашей круглой планете вряд ли можно найти человека, который мог бы с уверенностью сказать, в чем заключается стратегия Microsoft относительно разработки в целом. Жив ли WPF, что будет с Silverlight для веба, откуда такой ажиотаж вокруг HTML 5 и причем тут, наконец, С++? Иногда мне кажется что ни у кого нет ответов на эти вопросы, и что в погоне за добычей (добычей тут может быть, например, рынок планшетов), не возбраняется полностью смешать все карты и начать заново.

Тем не менее, мы должны признать, что по крайней мере в свете анонсов касательно WinRT, язык программирования С++ снова становится более актуальным чем ранее. Да и не только сам С++. Ведь фактически то, что нас ждет, это некая реинкарнация COM, правда с другими терминами и, возможно, меньшим количеством проблем. (Ведь неслучайно PIA – Primary Interop Assembly – расшифровывается также как Pain In the Ass;)

Если быть честно-откровенными, то тот С++ который предлагает нам Microsoft – это совсем не «стандартные» плюсы. Хотя, точно так же как и в случае с C++/CLI, остается возможность использования «труъ» кода, который был написан ранее. Но вот уже в 3й раз нам предлагают некую магию.

Почему в 3й? Ну, 1м разом я считаю все те изменения языка, которые не были стандартизованы. С Java/Visual J++ такое не прошло т.к. там правила ныне отошедшая на покой компания Sun, в случае же с С++ не нашлось стороны, которая единолично планировала бы получать доходы с этого языка. Поэтому Microsoft могла сделать все, что угодно – добавить поддержку свойств (да-да, свойств в стиле C#) с помощью __declspec(property), замутить поддержку COM с помощью #import, ну и так далее. Вторым вмешательством в С++ можно считать MC++ и с последствии С++/CLI, который добавил массу ненужных ключевых слов, а также эти ненавистные шляпки (^) для классов. Ну и так далее.

Впрочем, создание C++/CLI было вынужденным. Я это начал понимать только тогда, когда начал встречать кросс-платформенные (!) библиотеки на С++ которые компилировались, помимо обычного С++, еще и с «управляемой прослойкой» для .Net языков. Идея в принципе оказалась правильной – дать .Net разработчикам всю мощь С++ библиотек без особого напряжения в виде P/Invoke или надобности обращать С++ библиотеки в COM.

Ну и наконец, шаг 3й – это полностью узаконить любые новшества в С++ сделав его одним из официальных языков разработки для Windows. Это на самом деле лицемерие – ведь для Windows итак можно писать на «плюсах» используя MFC или даже WTL. Но уверен что мало кто этим занимается. Потери от этого больше, чем потери от требования наличия .Net Framework Client Profile на компьютере. Кстати – недавно в процессе тестирования лицезрел что происходит если поставить ClickOnce-программу которая использует .Net 4 на Windows XP. Инсталлятор сам скачивает и устанавливает framework. Это конечно нехилый download, но большинство людей 100-мегабайтные файлы уже практически не пугают.

Так вот, хотите вы этого или нет, С++ будет использоваться Microsoft в своих целях, связанных с разработкой под Windows. Но это конечно не значит что вы не сможете получить от этого бенефиты, даже если пишете на .Net.

Приемущества С++

Давайте определимся с тем, что же дает нам С++ чего не может дать тот же C#.

Ручное управление памятью. У меня в голове иногда случаются страшные баталии, связанные с тем, нужно ли делать объект структурой или классом. В принципе, в .Net нам пытаются навязать ограничение, связанное с размером объекта – дескать, если маленький то можно и структуру, а иначе ни-ни. Но это подразумевает что мы, разработчики, наивно делаем pass by value и тем самым поедаем память. Что не всегда так.

Доступ к высокопроизводительным инструкциям процессора. Возьмем например SIMD. Думаю можно с большой степенью уверенности сказать, что JIT-компилятор не оптимизирует и никогда не будет оптимизировать код под SIMD. Соответственно, мы с Microsoft.Net начинаем проигрывать даже Mono, где есть соответствующий пакет.

Прямой доступ к намного более зрелым и продуманным библиотекам. Это более сумеречный бенефит, т.к. API таких библиотек порой ужасно. Но надо смотреть не только на API но и на производительность.

О да, я же забыл упомянуть о самом важном – Microsoft ныне разрабатывает свои библиотеки для С++. Примером такой библиотеки может быть вышеупомянутая Parallel Patterns Library (PPL), которая с одной стороны может считаться «неуправляемым» аналогом .Net’ного TPL, а с другой – конкурентом Intel Threading Building Blocks (TBB). Но на этом все не останавливается – Microsoft работает еще на одной библиотекой под названием C++ Accelerated Massive Parallelism (AMP), которая будет заниматься поддержкой вычислений как на CPU так и на GPU.

Программирование для GPU заслуживает отдельную заметку. На данный момент, графические процессоры можно программировать с помощью OpenCL (вариации языка С++ для различных устройств), CUDA C (подобия С++ для чипов NVidia, кстати с прекрасным тулсетом для Visual Studio). Существуют также решения на .Net, такие как исследовательский проект Microsoft Accelerator (который, насколько я знаю, также работает с FPGA) или коммерческое решение GPU.NET.

Недостатки С++

С++ архаичен. Использовать его для ООП – делать себе плохо. Даже со всеми новшествами C++ 2011 является динозавром, пусть и динозавром который пока не думает вымирать. С другой стороны, С++ прекрасно подходит для производительных алгоритмов.

Приведу пример. Мне в TypograFix нужно обрабатывать Bitmap’ы. Если использовать .Net, то единственная возможность получить более-менее вменяемую скорость по обработке картинок – это использовать unsafe. Что как бы само по себе не очень эффективно, но даже если у вас получится, вы не сможете эффективно настроить параллелизацию unsafe кода. (Автор пробовал использовать TPL и unsafe, с весьма мутными результатами.)

Еще одна проблема – это нехватка инструментария. Отладчики конечно есть, но вот анализа кода в стиле Решарпера мне очень нехватает. А ведь это как раз тот «небезопасный» язык, в котором так нужны всяческие проверки, причем чем их больше, тем лучше.

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

AMP

У инициативы AMP есть одно существенное приемущество – над этой технологией работают AMD и NVidia, две компании которые держат рынок графических карт (лично я тяготею к AMD потому что у них есть Eyefinity). Работают они над ней, конечно, с Microsoft, т.к. у всех трех компаний есть уклон в сторону различных процессоров. Кстати, в случае с Microsoft, это конечно же ARM, о котором вы все уже наверняка наслышаны. Соответственно идея, как мне кажется, в том, чтобы иметь код (возможно даже код низкого уровня – драйвера, кодеки, и т.п.) который мог бы выжать максимум возможностей из любого процессора.

С другой стороны, по крайней мере для GPU, поддержка идет по наименьшему знаменателю, и этот знаменатель называется DirectCompute. Поэтому неизвестно, будет ли AMP настолько эффективен как например CUDA C или кросскомпиляция, скажем, C# в NVidia PTX (именно это делает GPU.NET). Также непонятно, придется ли писать fallback-код для CPU в случае отсутствия девайса с поддержкой DirectCompute – если это придется делать, то часть шарма данного подхода пропадет, т.к. это точно не «write once, run everywhere».

Intel C++

Microsoft не является единственной компанией, которая выпускает компилятор С++. Intel тоже делает компилятор, в большей степени заточенный на свои процессоры, с неплохой интеграцией в Visual Studio. Важен не только сам компилятор (а он хорош), но и тулсет – отладчик, профилировщик, библиотеки. Все это на высоте. Библиотеки, например, работают более резво на процессорах AMD чем библиотеки самой AMD.

На сегодняшний день я готов работать только с компилятором Intel. Я не использую C++/CLI, мне хватает P/Invoke. На самом деле, технология P/Invoke – настолько простая и понятная, что я не чувствую особого стеснения когда пишу часть проекта на C# а часть на С++. Что P/Invoke не поддерживает так это, конечно же, ООП и возможность передавать туда-сюда классы, но тут я скажу вот что: если вы используете С++ именно для ООП (или для взаимодействия с .Net) – вы делаете что-то не так. На С++ нужно писать производительные алгоритмы, или использовать уже существующие библиотеки (например, Intel TBB, MKL или IPP). Потреблять ООП-структуры там бесполезно, но с другой стороны, не возбраняется потреблять оттуда COM. Например, когда только вышли библиотеки Direct2D/DirectWrite, я захотел ими воспользоваться в WPF-проекте и просто написал C++ DLL’ку, которая рисовала в «залоченый» System.Drawing.Bitmap. Никаких проблем.

OpenMP

OpenMP – это поддержка декларативной параллелизации на С++. Это может показаться шуткой, но на самом деле это старая, проверенная временем технология, которая работает. Для большинства случаев data-level параллелизации в стиле parallel_for() подходит именно OpenMP. Я много где использую именно OpenMP, оставляя Intel TBB для более сложных задач, где автоматический подход не сработает.

Конечно, в .Net есть некий аналог – использование TPL и надежда на то, что JIT-компилятор магическим способом оптимизирует и «векторизует» сгенерированный код. Но это достаточно наивно. Пока вы проверяете индекс элемента доступа к массиву, вы теряете в производительности. Как только вы это отключите, у вас начнутся уже другие проблемы.

Если честно, я не большой фанат TPL. Для простеньких случаев в которые не хочется вникать, я просто пишу Parallel.Invoke или вставляю AsParallel() в цепочку LINQ-запроса и надеюсь на лучшее. Но если мне нужно почитать дистанцию Левенштейна на огромном наборе данных, то я даже напрягаться не буду – отошлю все данные в неуправляемый код, а там уж сделаю все «правильно».

Заключение

Я надеюсь, что повышенное внимание к С++ приведет к улучшению редактора. Правильный IntelliSense уже будет достижением. А что еще надо – ах да, возможно какой-нибудь генератор P/Invoke оберток и соответствующей документации для того чтобы упростить процесс.

К слову скажу, что недавно пытался найти нормальный блог по С++ путем вбивания “с++ блог” в гугле. Гугл выдал какой-то мусор. Если кто-то знает интересные блоги на русском, дайте ссылочку в комментариях. Спасибо.

Written by Dmitri

30 октября 2011 at 0:13

Опубликовано в Programming

Tagged with

Microsoft, инновации и откровенный флейм

54 комментария

Учитывая то количество флейма которое летит в направлении Microsoft/.Net разработчиков, тот пост что я пишу сейчас надо было написать уже давно, дабы попробовать объяснить всем кому интересно чем же .Net стек так привлекателен и что в нем не так. Следуя традициям spbalt.net, попробую осветить проблему с обеих сторон. Если что – комментируйте, буду рад.

Фанатизм Microsoft

Для начала, лучше всего сразу отсекать фанатиков. Предлагаю сделать это первым же абзацем дабы потом не фокусировать на них внимание. Да, существуют фанатики, причем как анти-Microsoft так и pro-Microsoft. Обе группы не несут в себе какого-то адеквата и трезвой аргументации. Я не знаю какая демографика этих групп, но подозреваю что не слишком-то зрелая там аудитория. Зрелая аудитория по крайней мере пытается преставлять хоть какие-то аргументы, и даже если они иногда выглядят как сравнение яблок с апельсинами, это лучше чем ничего.

Интересно было посмотреть на весьма смелый (зная Хабр) пост Александра Ложечкина. Не знаю, ожидал ли он такой флейм или нет, но мне кажется все равно его пост достиг нужной аудитории, и донес до нее главную информацию. Кричали, конечно, различные критики в силу возможности. Первый комментарий (с рейтингом -51) показывает относительную адекватность Хабрапользователя:

Позовите, когда мы сможем попрощаться с Вами вообще.

Комментариев к такому у меня, увы, нет. Интересно что его оставил далеко не самый последний (по рейтингу) пользователь.

Языки – Java vs. C# и аналогичные флеймы

Окей, у меня блог посвященный C# поэтому тут можно обвинить меня в предвзятости. Нет, я не буду пропагандировать C# или сравнивать его, например, с Java потому что подобные действия имеют свойство очень быстро вырождаться в откровенный флейм со стороны MS- и Java- сообществ. Единственное, что я могу сказать насчет языков так это то, что (имхо) C# является фундаментальным мотиватором изучения платформы .Net начинающими разработчиками. Этот язык сложно критиковать, в то время как к любому другому языку можно предъявить море претензий на тему “современности” и т.п. Это не значит что C# кардинально лучше, т.к. понятие “лучше” само по себе ничего не значит – “интереснее”, “прибыльнее” – это уже более адекватные эпитеты. В этом же ключе можно говорить и про распостраненность, цитируемость, и т.п. Де факто, любой язык – даже COBOL – в правильных руках позволит решать задачи, и метрики показывают что за исключением Ассемблера, производительность разработчиков с разными языками для аналогичных тасков примерно одинакова. Но если учитывать что мы не сферических коней в вакууме строим а решаем бизнес-задачи, более корректны (имхо) сравнения на уровне платформ. И то, каждое сравнение автоматически вырождается в флейм. Может не стоит?

К слову хочу заметить что, несмотря на то что Microsoft поспособствовала выпуску ряда альтернативных языков для платформы .Net, их прием в качестве рабочих лошадок заказчиками был и остается минимален. Причины? А вы представьте себя на стороне заказчика – какой проект лучше потом поддерживать, тот что на C# или тот что на IronRuby? Более того, использование не-C# языков очень сложно аргументировать. Иначе говоря, если аргументировать использование Scala вместо Java достаточно просто (в силу “мощности” языка), то аргументировать использование F# вместо C# – значит напирать на “функциональность”, немутабельность, async workflows и т.п. Иначе говоря, на те вещи которые реализуются на C# без каких-то громадных трудозатрат. Например, на C# проще простого сделать immutable структуру, да еще и тест написать который проверяет ее “немутабельность”.

В заключение о языках хочу добавить что единственная фича, которая нужна в C# – это статическое метапрограммирование. И оно у нас будет. А все кто считает что Ruby лучше как раз из-за возможностей метапрограммирования которые “я получу уже сегодня” занимаются сравнением яблок и апельсинов, т.к. метапрограммирование в Ruby динамическое. Нечестное, как мне кажется, сравнение.

Незрелость библиотек и MVC vs. Rails

Окей. Да, в каком-то смысле .Net играет в догонялки с той же Java в тех областях где эти платформы имеют общие требования. Люди очень любят приводить пример с “рельсами” которые якобы копируют авторы Asp.Net MVC. Меня всегда итересовало – что мотивирует людей делать такие обзоры и сравнения когда мы живем, собственно, в эпоху свободы выбора, когда вместо того чтобы тупо читать что А лучше B я скорее скачаю и сам попробую и А и В.

Вообще сравнивать библиотеки – это дело неблагодарное. Сравнивать фреймворки – это да, это уже как-то актуальнее. Но кто хочет, например, сравнить Swing и WinForms/WPF? Нет желающих? Опять же, очень глупо катить бочку против Microsoft когда в плане презентационных фреймворков для десктоп-приложений не так уж и многа выбора. Я конечно все понимаю, и как сказал Ринат Абдуллин “Россия это страна где еще на Delphi пишут”, но серьезно… какие варианты быстрого (да-да, быстрого и качественного) построения интерфейсов thick-client знаете вы?

Вот другое дело web – тут как бы технология общая (HTML), и бороться за нее не надо, т.к. она не проприетарная. Тем самым можно сказать что используете вы Rails или MVC или PHP – это не так критично т.к. и там и там можно получить идентичный результат. Мне кажется что сам спор веб-фреймворков также бессмысленнен в связи с тем, что вне зависимости от ваших предпочтений уже есть хорошие платформы для построения решений, ничего не надо писать “с нуля”, можно просто кастомизировать. Сказать что А лучше В тут сложно – да, есть разница в зрелости фреймворков, но посмотрите вокруг – люди поставляют решения даже на сырых платформах. И что – работает!

Про стоимость лицензий

Вообще меня удручает подход разработчиков которые считают что все должно быть FOSS (бесплатно и open-source). Это абсолютно ущербный подход, т.к. если мне, например, перестанут платить за производство решений, то… я тупо перестану что-либо производить и уйду в другую область. Идея о том что можно получать бесплатно все что нужно для разработки породило такие решения как NHibernate (действительно the linux way – если что нужно, берите лобзик и допиливайте) или, например, тот же SharpDevelop. Я не имею ничего против, но когда у вас есть IDE без вменяемой отладки – это не IDE. Поэтому коммерческие решения нам нужны как воздух.

Очень много разговоров идет про стоимость лицензирования, про то что BizSpark это маркетинговая уловка, про то что сложно оплатить N лицензий, там, Windows или SQL Server. Если честно, эта дискуссия тоже уже устарела. Например, корпоративный сектор, который очень хорошо умеет считать деньги, с удовольствием использует ПО от Microsoft, и оно входит в его бюджеты. То есть сказать, что Windows – дорого, значит ничего не сказать.

Да, конечно, хорошо когда все бесплатно. И с моей стороны достаточно лицемерно говорить про стоимость лицензий когда я использую ПО по программам MVP и BizSpark. Но несмотря на это, те заказчики с которыми мы работаем с удовольствием приобретают ПО от Microsoft – с другой стороны, у многих из них стоит и Linux/Java. И вот что я вам скажу – они довольны и тем и тем. Поэтому говорить что что-то кардинально лучше – некорректно. И я не буду тут писать про то, насколько стоимость ПО ничтожна по сравнению со стоимостью разработки кастомных решений, ибо вы все итак уже знаете.

Инновации, смерть SQL, и т.п.

Для многих, Microsoft, ‘enterprise и консерватизм – это слова из одной ипостаси (люблю это слово). Поэтому многим видится консервативная компания которая по-тихоньку удовлетворяет и пьет кровь большого бизнеса, не очень-то обращая внимание на малый. Тем самым, сравнивая деательность Apple и Microsoft, например, обыватель может достаточно справедливо сказать что у Apple все как-то поинтересней – по крайней мере с точки зрения продуктов.

Увы, в плане разработки под различные платформы ситуация действительно не очень внушающая – посудите сами, что сейчас выгодней – разработывать для iOS или WinMobile? (или Android?) Ну а desktop-приложения для Windows сейчас создавать бессмысленно т.к. рынок перенасыщен уже давно, да и нет какого-то “уютного магазинчика” для всего этого добра.

Впрочем, в плане веба инноваций тоже как-то маловато. Да, Asp.Net MVC это небольшой шажок вперед, но если посмотреть на другие новые проекты (например Node.js), начинаешь понимать что речь идет о разных уровнях инноваций. В одном случае, это действительно свежии, новаторские идеи. В другом, эволюция существующих идей (например WebFormsViewEngine --> Razor), которая является чем-то ожидаемо-предсказуемым. Совершенно разные вещи. Так что в этом плане я с критиками согласен – инновационность, по крайней мере для разработки в стеке .Net определенно страдает. Есть конечно интересные вещи (Rx, StreamInsight) но их мало и кардинально они мозг не выносят.

Насчет NoSQL и SQL Server

В принципе, таким заголовком можно открыть флейм на тему SQL Server vs. NoSQL, где одни будут предрекать падение платформы SQL Server а другие указывать на то, что понадобятся годы чтобы реализовать SSxS на MongoDB или аналогичных системах.

Очень сложно спорить с тем, что при всех наработках платформы, SQL Server капитально проигрывает в производительности, масштабируемости, а также платформной зависимости. И еще, вы уж извините, но в стоимости он проигрывает тоже – да, я знаю что ранее я написал что стоимость некритична в общем случае, но так уж получилось, что суть хранилищ данных – это большие объемы а следовательно большое количество ядер. Лицензирование SQL Server в этом плане оставляет желать лучшего. Не знаю, может есть скидки когда инстансов много?

Тема NoSQL и то как она повлияет на стек .Net очень интересна. В принципе, уже понятно что binding’и на NoSQL базы есть и будут, поэтому существенные причины уходить с .Net неактуальны пока вы не решите уйти с Windows. Могу по себе сказать, что мои личные проекты связанные с web mining медленно перекачивают на MongoDB. Что касаеться “продакшн”, то тут я как и все буду осторожничать. Так или иначе, лично мне занятие MongoDB кажется актуальной стратегией для развития – даже более актуальной чем наращивание компетенций в Windows Azure.

Comments welcome.

Written by Dmitri

21 июля 2010 at 22:42

Опубликовано в .NET, Development, Engineering, Internet, Programming