Русский
Русский
English
Статистика
Реклама

C++

Перевод Высокопроизводительная сборка мусора для C

20.06.2020 20:07:24 | Автор: admin
Мы уже писали о сборке мусора для JavaScript, о DOM, и о том, как всё это реализовано и оптимизировано в JS-движке V8. Правда, Chromium это не только JavaScript. Большая часть браузера и движок рендеринга Blink, куда встроен V8, написаны на C++. JavaScript можно использовать для работы с DOM, а на экран изменения выводятся с использованием конвейера рендеринга.

Так как граф C++-объектов, имеющих отношение к DOM, тесно связан с JavaScript-объектами, команда разработчиков Chromium пару лет назад начала использовать для управления памятью, в которой хранятся эти объекты, сборщик мусора, названный Oilpan. Oilpan это сборщик мусора, написанный на C++ и предназначенный для управления C++-памятью, которая может быть подключена к V8. Управление памятью осуществляется с использованием технологии кросс-компонентной сборки мусора. В рамках этой технологии граф связанных C++/JavaScript-объектов рассматривается как единая куча.



Этот материал является первой публикацией, посвящённой Oilpan. Здесь будет сделан обзор основных принципов, лежащих в основе данного сборщика мусора, а также C++-API Oilpan. Мы рассмотрим некоторые возможности, поддерживаемые Oilpan, расскажем о том, как устроена работа различных подсистемам сборщика мусора. Тут же мы разберём процесс конкурентного освобождения памяти, занятой объектами.

Самое интересное здесь то, что система Oilpan является частью Blink, но сейчас осуществляется её перевод в V8, где она будет представлена в форме библиотеки для сборки мусора. Цель этого всего заключается в том, чтобы облегчить доступ к C++-механизмам сборки мусора всем тем, кто встраивает в свои платформы движок V8. Кроме того, то, что Oilpan станет библиотекой, позволит пользоваться этой системой абсолютно всем заинтересованным в ней C++-программистам.

Общие сведения


В Oilpan применяется система сборки мусора, в которой используется алгоритм пометок (Mark and Sweep). Этот алгоритм предусматривает разделение процесса сборки мусора на две фазы. Первая фаза заключается в исследовании кучи, и в пометке (mark) живых объектов, которые нельзя удалять из памяти. Вторая фаза это очистка (sweep) памяти кучи, которую занимают ненужные (мёртвые) объекты.

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

В этом плане C++ от JavaScript не отличается. Правда, в отличие от JavaScript-объектов, C++-объекты статически типизированы. Они, в результате, не могут менять собственное представление во время выполнения программы. При работе с C++-объектами с применением Oilpan этот факт учитывается и предоставляется описание указателей на другие объекты (рёбра графа) с использованием паттерна Посетитель (Visitor). Базовый паттерн используемый для описания Oilpan-объектов, выглядит так:

class LinkedNode final : public GarbageCollected<LinkedNode> {public:LinkedNode(LinkedNode* next, int value) : next_(next), value_(value) {}void Trace(Visitor* visitor) const {visitor->Trace(next_);}private:Member<LinkedNode> next_;int value_;};LinkedNode* CreateNodes() {LinkedNode* first_node = MakeGarbageCollected<LinkedNode>(nullptr, 1);LinkedNode* second_node = MakeGarbageCollected<LinkedNode>(first_node, 2);return second_node;}

В этом примере Oilpan управляет LinkedNode, на что указывает то, что класс LinkedNode является наследником GarbageCollected<LinkedNode>. Когда сборщик мусора обрабатывает объект, он находит указатели на другие объекты, вызывая метод объекта Trace. Тип Member это интеллектуальный указатель, который, с синтаксической точки зрения, похож, например, на std::shared_ptr, который предоставляется Oilpan и используется для поддержания единообразного состояния при обходе графа объектов во время выполнения маркировки объектов. Всё это позволяет Oilpan точно знать о том, где именно находятся указатели, с которыми работает эта система.

Тот, кто внимательно прочитал вышеприведённый код, возможно, заметил (и, может быть, его это испугало) то, что first_node и second_node хранятся в стеке в виде обычных C++-указателей. Oilpan не задействует дополнительные абстракции для работы со стеком. Сборщик мусора, обрабатывая корневые объекты в куче, которой управляет, полагается исключительно на консервативное сканирование стека при поиске указателей. Всё это работает путём пословного перебора стека и благодаря интерпретации слов в виде указателей на сущности, находящиеся в управляемой куче. Это означает, что использование Oilpan не приводит к ухудшению производительности при доступе к объектам, размещаемым в стеке. Вместо этого нагрузка переносится на этап сборки мусора, когда осуществляется консервативное сканирование стека. Oilpan интегрирован в подсистему рендеринга и пытается откладывать запуск процедуры сборки мусора до тех пор, пока система не достигнет состояния, когда в стеке, точно, не будет ничего интересного. Так как работа веб основана на событиях, а выполнение кода производится путём обработки задач в циклах событий, в распоряжении Oilpan оказывается достаточно удобных моментов для запуска сборки мусора.

Oilpan используется в Blink, а это большая кодовая база, написанная на C++, в которой содержатся значительные объёмы зрелого кода. Благодаря этому Oilpan, кроме прочего, отличается следующими возможностями:

  • Множественное наследование с помощью миксинов и ссылок на подобные миксины (внутренние указатели).
  • Поддержка вызова сборки мусора при выполнении конструкторов.
  • Поддержание объектов из неуправляемой памяти в живом состоянии с помощью интеллектуальных указателей Persistent, которые рассматриваются как корневые сущности.
  • Коллекции, представляющие собой последовательные (например vector) и ассоциативные (например set и map) контейнеры. Возможность уплотнения данных, лежащих в основе коллекций.
  • Слабые ссылки, слабые функции и эфемерные структур данных.
  • Финализаторы методы, выполняемые перед удалением из памяти отдельных объектов.

Очистка памяти для C++


О том, как осуществляется пометка объектов в ходе работы Oilpan, мы поговорим в другой раз. Сейчас же будем исходить из предположения о том, что пометка объектов уже выполнена и Oilpan уже обнаружил все достижимые объекты, используя метод объектов Trace. После того, как фаза пометки завершена, у достижимых объектов оказывается установленным соответствующий бит.

Очистка это та фаза работы Oilpan, когда уничтожаются мёртвые объекты (те, которых не удалось достичь на фазе пометки объектов). Память, которую они занимают, либо возвращается операционной системе, либо становится доступной для выделения под другие объекты. Ниже мы поговорим о том, как Oilpan выполняет очистку памяти, рассмотрим особенности выполнения этого процесса и его ограничения. Мы, кроме того, поговорим о том, как механизму очистки памяти Oilpan удаётся достигать высоких скоростей при уничтожении мёртвых объектов.

Система очистки памяти находит мёртвые объекты, перебирая память кучи и проверяя соответствующие биты объектов. Для того чтобы сохранить семантику C++, системе очистки памяти нужно вызывать деструктор каждого из мёртвых объектов перед освобождением занимаемой им памяти. Нетривиальные деструкторы реализуются в виде финализаторов.

Здесь, с точки зрения программиста, нет заранее заданного порядка, в котором вызываются деструкторы, так как механизм перебора, используемый системой очистки памяти, не принимает во внимание порядок создания уничтожаемых объектов. Это накладывает на финализаторы ограничения, в соответствии с которым они не могут обращаться к другим объектам, находящимся в куче. Перед нами встаёт задача, встречающаяся достаточно часто. Она заключается в следующем: есть платформа, которая не поддерживает указание порядка финализации объектов (вроде Java), но при этом для данной платформы нужно писать код, требующий определённого порядка вызова финализаторов. Oilpan использует плагин Clang, который, в статическом режиме, обеспечивает запрет доступа к объектам кучи в процессе уничтожения объектов (это лишь одна из многих возможностей Clang):

class GCed : public GarbageCollected<GCed> {public:void DoSomething();void Trace(Visitor* visitor) {visitor->Trace(other_);}~GCed() {other_->DoSomething(); // error: Finalizer '~GCed' accesses// potentially finalized field 'other_'.}private:Member<GCed> other_;};

В этом коде происходит ошибка при попытке обращения к объекту из кучи в ходе уничтожения другого объекта.

Если вас интересует вопрос уничтожения объектов, то знайте, что Oilpan даёт в распоряжение программиста коллбэки, вызываемые перед финализатором. Это позволяет реализовывать сложные схемы уничтожения объектов, когда объекту, перед уничтожением, нужен доступ к другим объектам. Подобные коллбэки создают немалую дополнительную нагрузку на систему в ходе сборки мусора. Эта нагрузка больше той, которую способны создать деструкторы. Поэтому такие коллбэки используются в Blink относительно редко.

Инкрементальная и конкурентная очистка памяти


Теперь, когда мы поговорили об ограничениях деструкторов в управляемом C++-окружении, пришло время более подробно остановиться на том, как в Oilpan реализована и оптимизирована очистка памяти.

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

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


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

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


Инкрементальная очистка памяти

При использовании инкрементального подхода фазы пометки и уничтожения объектов отделены друг от друга. При этом процедура очистки памяти разбита на несколько частей, представленных отдельными задачами, выполняющимися в главном потоке. В лучшем случае подобные задачи выполняются только во время простоя системы, что позволяет избежать их конфликта с задачами, представляющими обычные механизмы приложения. Внутренние механизмы сборщика мусора разделяют одну большую задачу по очистке памяти на небольшие задачи, это разделение основано на понятии страница. Страницы могут пребывать в двух состояниях. Одни страницы находятся в состоянии ожидания очистки (to-be-swept), а другие уже являются очищенными (already-swept). Механизмы выделения памяти учитывают только страницы, которые уже очищены, и пополняют локальные буферы выделения памяти (Local Allocation Buffer, LAB) из списка свободной памяти, в котором хранится список доступных фрагментов памяти. Для того чтобы получить память, сведения о которой есть в списке свободной памяти, приложение сначала попытается найти память, которая относится к уже очищенным страницам. Затем приложение попытается помочь системе в обработке страниц, которые ожидают очистки, воспользовавшись там, где оно пытается выделить память, системой очистки памяти. Новая память у операционной системы будет запрошена лишь в том случае, если вышеприведённые действия не привели к тому, что приложение получило нужную ему память.

В Oilpan механизм инкрементальной очистки памяти использовался несколько лет. Но, по мере того, как приложения, а значит и графы их объектов, становились всё больше и больше, очистка памяти начала влиять на производительность приложений. Ради улучшения механизмов сборки мусора, мы, для реализации конкурентной очистки памяти, начали использовать фоновые задачи. Для того чтобы исключить гонку данных между фоновыми задачами, выполняющими очистку памяти, и приложением, выделяющим память под новые объекты, существуют два правила:

  • Система очистки памяти обрабатывает только память мёртвых объектов, которые, по определению, недостижимы из приложения.
  • Приложение выделяет память, используя только ту память, которая принадлежит очищенным страницам. Такие страницы уже не интересуют систему очистки памяти.

Оба правила позволяют обеспечить то, что несколько сущностей не станут бороться друг с другом за один и тот же объект и его память. К сожалению, C++ сильно полагается на деструкторы, которые реализованы как финализаторы. Oilpan запускает финализаторы в главном потоке для того чтобы помочь разработчикам и исключить гонку данных в самом коде приложения. Для решения этой проблемы Oilpan планирует отложенное выполнение финализатора объекта в главном потоке. А если точнее, то всякий раз, когда конкурентная система очистки памяти сталкивается с объектом, у которого есть финализатор (деструктор), она помещает его в очередь финализации, которая будет обработана на особом этапе финализации. Код на этом этапе всегда выполняется в главном потоке, в котором, кроме того, выполняется и код приложения. Вот как, в общих чертах, выглядит схема конкурентной очистки памяти.


Конкурентная очистка памяти с использованием фоновых задач

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

Результаты


Механизм фоновой очистки памяти был выпущен в сборке Chrome M78. Наш фреймворк для тестирования Chrome в условиях, приближенных к реальным, показал уменьшение времени, уходящего в главном потоке на операции, связанные с очисткой памяти, на 25-50% (в среднем на 42%). Ниже показаны результаты испытаний некоторых популярных веб-проектов.


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

То время, которое всё ещё уходит в главном потоке на очистку памяти, требуется для выполнения финализаторов. В настоящее время идёт работа по оптимизации финализаторов для типов объектов, которые часто используются в Blink. Примечательно здесь то, что все такие оптимизации проводятся за пределами кода системы очистки памяти, так как эта система, в её текущем виде, автоматически подстраивается под ситуацию при отсутствии финализаторов.

Сталкивались ли вы с проблемами производительности веб-проектов, которые вызваны системой сборки мусора Chrome?



Подробнее..

Маленькое удобство, способное прекратить вселенские споры

22.06.2020 00:17:16 | Автор: admin
Все те, кто пишет на Си-подобных языках, знакомы с двумя немного отличающимися стилями кода. Выглядят они вот так:

for (int i=0;i<10;i++) {    printf("Hello world!");    printf("Hello world again!");}


for (int i=0;i<10;i++){    printf("Hello world!");    printf("Hello world again!");}


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

Компромиссное решение могло бы выглядеть вот так:

image

Что, собственно, здесь произошло? Всего лишь небольшая магия или фокус. Потому что настоящие фигурные скобки никуда не делись. Они были заменены редактором на символы верхнего и нижнего подчёркивания. В исходном файле все скобки на месте, но при чтении исходного файла ваша любимая IDE заменяет скобки на подчёркивания по предлагаемому алгоритму.

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

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

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

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

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

Алгоритм замены скобок.



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

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

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

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

Но остаётся небольшая сложность в виде бесшовного совмещения процесса редактирования с отображением замен в виде подчёркиваний.

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

Теперь изменения помельче. Если что-то правится внутри блока, то опять делать ничего не нужно. Но если к блоку добавляется новая строка, то самым логичным было бы ожидать, что разработчик установит курсор в конец последней строки и нажмёт enter. После этого мы ждём ввода первого символа и удаляем нижнее подчёркивание из начала последней строки и вставляем его в начало следующей (пока ещё пустой). Далее разработчик пишет текст после подчёркивания и некоторого набора пробелов (или табуляции), обозначающего отступ блока.

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

Выведем общие правила, соблюдение которых позволит достаточно просто производить замены.

Пока мы в блоке текста, включая крайние строки, делать ничего не нужно. Когда мы нажимаем enter в любом месте последней строки кроме конца нужно перенести подчёркивание. Если enter нажимается в конце строки ждём ввода символа. Когда мы нажимаем backspace в последней строке в позиции подчёркивания нужно переместить текущее подчёркивание на одну линию выше. И разумеется, при всех операциях нужно не забывать вставлять и удалять скобки в тот текст, который будет сохраняться на диск.

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

И напоследок о символе overscore, который я здесь условно назвал верхним подчёркиванием. Его, к сожалению, нет на стандартной клавиатуре, а потому нет возможности вводить его напрямую без помощи редактора. Именно поэтому важна помощь IDE. Хотя теоретически можно вводить последовательность юникода (\u00af = ), которую некоторые редакторы автоматически преобразуют в символ overscore, но всё же делать так каждый раз, когда нам нужна скобка, было бы просто издевательством над разработчиками. Поэтому и нужны плагины, ну и изменения в спецификациях языков.

Всё, ждём срочных обновлений спецификаций языков и массу удобных плагинов для всех возможных IDE :)
Подробнее..
Категории: Javascript , C++ , C , Java , Код , Си , Стиль кода

Работаем в IntelliJ IDEA на слабом железе

09.07.2020 14:06:35 | Автор: admin

Обнаружил секретный репозиторий на гитхабе JetBrains под названием Projector. Благодаря нему написал кусок кода в IntelliJ IDEA, запущенной на Android-планшете. Рассказываю, как это повторить.



Проблема


Все мы любим IntelliJ IDEA, но есть с ней неувязочка она жрёт ресурсы компьютера. Может, крипту майнит, никто не знает.


У всех нас есть что-то вроде старого ноутбука, который ты очень любишь, но работать на нём уже не получается уж слишком он слабый. На некоторых девайсах Идеи и не было никогда. Например, на Android-планшетах. Зайдите на сайт нет там сборки под Arm.


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


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


Решением было бы разделить фронтенд и бэкенд IDE. Запускаем тяжелый вычислительный бэкенд в дата-центре, или просто на своём домашнем Threadripper 3990X. Соединяемся с бэкендом из локального приложения, написанного на Java.


К сожалению, IntelliJ IDEA так сдизайнена, что до последнего времени сделать этого было нельзя. Ну, почти. Не надо быть гением, чтобы догадаться, что в Rider как-то общаются между собой Java-фронт и .NET-бэк, но использовать это в своих корыстных целях никак нельзя.


Удалённый рабочий стол отстой


Конечно, многие пытались запускать Идею через TeamViewer, Microsoft Remote Desktop, VNC, и так далее. Существуют компании, которые только так и работают сотрудники сидят на удалёнке и кодят через Remote Desktop.


Видите в этом проблему? Вот, посмотрите:



Теперь я должен вам новые глаза!


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


Из этого можно вынести какой-то опыт. Например, TeamViewer тормозит меньше, но изображение больше артефачит. RDP выдает лучше картинку, но безбожно тормозит. И там и там есть вопросы к качеству мобильных приложений если хочется запускать на планшете.


Что же делать? Сколько надо полоскать рот, чтобы извести оттуда привкус мыла?


Ваше слово, товарищ Projector!


Вот как выглядит картинка на моем планшете Huawei MediaPad M5:



Вначале я хотел сфотографировать шрифты с близкой дистанции, но неспособность камеры Sony RX100 v5 сфокусироваться на таком расстоянии похоронила эту идею. Вместо этого держите нотариально заверенный скриншот:



Видите косяки в шрифтах, покорёженных джипегом?


Не трудитесь, их там нет. Это настоящие векторные шрифты, и Идея тоже настоящая. Ну, почти.


Магия заключается в том, что в репозитории проекта Projector на GitHub лежит запускатор IntelliJ IDEA в серверном режиме.


Почему это работает?


Судя по всему, Projector работает на переписанном изнутри рендерере AWT из OpenJDK. Теперь AWT рисует всё не на обычных поверхностях из операционной системы, а прямо в браузере. Как именно эта магия работает я сейчас быстро описать затрудняюсь это тема для отдельной статьи.


Но эффект потрясающий любое приложение, написанное на Java и Swing, автоматически начинает работать в браузере без переписывания кода!


Хочу! Что нужно делать?


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


Расскажу о том, как всё это запустить и начать разбираться. Дальше вы уже сами.


Общие инструкции есть вот в официальном репозитории.


Алгоритм действий:


  • Подготовить сервер (только для использования в облаке)
  • Сбилдить и запустить докерные образы
  • Открыть IDEA в браузере
  • PROFIT

Подготовка сервера


Вам понадобится компьютер с Docker.


Завести всё это можно и без Docker, но конкретно эта статья подразумевает его наличие, ибо без Docker всё становится куда сложнее.


"Сервер" может быть как машиной в облаке, так и вашим обычным компьютером неважно.


Я всё тестировал в двух конфигурациях: Linux на десктопе и Linux на удалённой виртуалке с четырьмя ядрами и 4 гигабайтами оперативной памяти. Для других операционных систем могут потребоваться корректировки.


В Ubuntu 16.04 установка докера делается вот по этой инструкции. Если у вас другая операционная система придется погуглить самостоятельно.


Краткое содержание установки Docker на Ubuntu 16.04:


curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"sudo apt-get updatesudo apt-get install -y docker-cesudo usermod -aG docker ${USER}sudo reboot

Дальше нужно немного поднастроить саму операционку. Если вы используете Linux-десктоп как рабочую машину в графическом режиме, то ничего делать не нужно.


Если же вы хотите развернуть что-нибудь вроде тестового сервера в облаке (благо сейчас это делается двумя щелчками мыши в Amazon и других облачных сервисах), нужно будет поставить пару пакетов.


Суть в том, чтобы у вас в командной строке появились xvfb (виртуальный фреймбуфер) и dbus-launch. Зачем нужен фреймбуфер я сказать затрудняюсь, но без этого не работают скрипты сборки. Вероятно, тяжелое наследие AWT.


Вот что нужно установить для Ubuntu 16.04:


apt install xvfb dbus dbus-x11 gnome-keyring

Устанавливать gnome-keyring приходится по внутренним убунтовым причинам. Иначе окажется, что у вас проблемы с секретами для десктопа. Если у вас не Ubuntu, то скорей всего, это не нужно.


Дальше нужно создать фреймбуфер:


Xvfb :99export DISPLAY=:99

Скрипт этот стоит засунуть куда-нибудь в автозагрузку. Например, в юнит systemd. Ну или просто руками каждый раз набирать заново.


Собираем и запускаем образ Projector


Скачиваем репозиторий со сборочными скриптами:


git clone https://github.com/JetBrains/projector-docker.gitcd ./projector-docker

Собираем и запускаем образ:


./clone-projector-core.sh./build-container.sh./run-container.sh

Заходим в IntelliJ IDEA из браузера


Если вы всё это время работали на своём (локальном) компьютере, то ссылка выглядит так: http://localhost:8080/projector/.


Если же вы запускаете всё это на удалённой машине (например, в облаке), то ссылка выглядит так: http://hostname:8080/projector/?host=hostname&port=8887.


Вместо hostname нужно ввести IP-адрес вашего сервера или доменное имя. Обратите внимание, что hostname в URL встречается два раза. Без этого не заработает.


Проблемы:


  • Если вы используете прокси (именно прокси, а не VPN), временно отключите. Проблемы с пробросом вебсокетов через прокси всё ещё существуют в 21 веке.
  • Если вы используете Google Chrome в качестве браузера, он может начать перебрасывать вас с HTTP на HTTPS. Попробуйте вот такую ссылку: http://host:8080/projector/?host=//hostname&port=8887. Заметьте, что слева от hostname появилось два слеша (//). В Firefox все должно работать без этого хака. Предупреждая вопрос, частично включить шифрование можно (для вебсокета), но это настолько муторно, что я не стал бы этим заморачиваться прямо сейчас.

Как работать в Android


Стандартный браузер Google Chrome в Android тратит слишком много места на всякие ненужные вещи вроде адресной строки. Для решения этой проблемы поможет бепсплатное приложение Fully Kiosk Browser.


Если вам не мешает адресная строка, но мешают органы управления Android, то можно использовать бесплатное приложение Fullscreen Immersive.


Одновременно и то и другое использовать не имеет смысла, т.к. FUlly Kiosk Browser уже умеет отключать органы управления Android-оболочки и делает это по-умолчанию.


Как работать в iOS


Понятия не имею! Из всех эппловских девайсов у меня есть только служебный макбук и личный айфон. На айфоне проверять всё это бессмысленно (экран слишком маленький), а на макбуке никаких проблем не существует.


Как запаролить соединение?


Идём в файлы проекта, которые мы скачали ранее, открываем файл run-container.sh и ищем строчку:


docker run --rm -p 8080:8080 -p 8887:8887 -it "$containerName" bash -c "nginx && ./run.sh"

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


docker run --rm \    --env ORG_JETBRAINS_PROJECTOR_SERVER_HANDSHAKE_TOKEN=mypassword \    -p 8080:8080 -p 8887:8887 -it "$containerName" bash -c "nginx && ./run.sh"

Теперь контейнер можно запускать!


./run-container.sh

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


Для локальной машины: https://localhost:8080/projector/?token=mypassword


Для облачного сервера: https://hostname:8080/projector/?host=//hostname&port=8887&token=mypassword


Как сделать безопасное соединение?


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


Перспективы


Можно мгновенно придумать множество областей, где поможет этот проект:


  • Удалённая разработка;
  • Коллаборативная разработка;
  • Комфортная удалённая отладка;
  • Ускорение раундтрипа в приложениях с большими данными;
  • Работа в защищенном контуре;
  • Мгновенное разворачивание рабочего места;
  • Работа с Очень Большими Монорепозиториями;
  • Интеграция в инфраструктуру облачных компаний;

Кто знает, что ещё нас ждёт! Перспективы безграничные.


Проблемы


Надо сказать, что MediaPad M5 двухлетней давности на чипсете Kirin 960 не самая мощная машина в истории. (Зато это толстый надежный кирпич металла, которым можно копать землю в огороде!) И конечно, при редактировании большого количества текста появляются тормоза перерисовки. Браузеру сложно рисовать столько графики быстро.


Тем не менее, чтобы что-то отлаживать этого уже вполне достаточно. Отладчик будет красивый, четкий и приятный, а не как через Remote Desktop.


Если же запускать всё это на ноутбуке, тормозов почти нет. Особенно если там есть видеокарта, а не как у планшета MediaPad M5, где вместо видеокарты работает Mali-G71.


Выводы


Найдено чудесное решение для запуска IntelliJ IDEA (и всех IDE от JetBrains) на удалённом сервере.


Если честно, это чуть ли не лучшая новость про IDEA за последние годы. Новый хоткей это приятно, но в целом ничего не меняет. Projector же меняет всё. Удивительно, что об этом никто ничего не пишет, что из каждого утюга не доносится победная песнь проекта Projector.


Сам я сейчас занимаюсь тем, что пытаюсь упаковать запускатор IDEA в качестве нативного приложения для Windows, Android и iOS с помощью нативных для платформы средств (Electron и WebView). Некоторое время еще нужно писать код, а потом публикация на сторы может занять длительное время. Как чего получится напишу статью на Хабр.


Где-то в богом забытом репозитории на GitHub обнаружилось настоящая гора золота. К сожалению, чудес не бывает, и наверняка, это золото охряняют драконы. Посмотрим, кто выйдет на свет!

Подробнее..

Device Manager. Обновление и мониторинг

16.06.2020 00:11:35 | Автор: admin

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


Автоматизация процесса обновления


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


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


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


Мы решили применить подход с использованием дополнительного легковесного программного обеспечения Launcher.


Launcher должен отвечать нескольким требованиям:


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

Под гибкостью подразумевается умение обновлять не только DeviceManager или другие программы, а в первую очередь самого себя. Тем самым нам необходимо выполнить только первичную установку программы на рабочее место, дальше обновления и работа будут происходить автономно.


Организация обмена данных


Но гибкости не добиться без управляющего звена, которое будет руководить обновлениями, обеспечивать мониторинг и необходимую информацию. В качестве этого звена будет выступать МИС (Медицинская информационная система).



Для поддержания этой концепции со стороны МИС было проделано много работы и это заслуживает отдельной статьи. Я лишь сделаю краткий обзор:


Обновление начинается с загрузки в МИС нового дистрибутива в специальный справочник с указанием номера версии. Есть возможность пометить версию как стабильную. Тогда она автоматически обновится на всех рабочих местах.



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


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



Для мониторинга текущего состояния предусмотрен запрос в Launcher на основе ответа из которого в графическом виде отображается индикатор:


  • Зелёный всё в норме;
  • Желтый требуется обновление;
  • Красный не запущено;
  • Серый не установлено.


Если требуется дополнительная информация предусмотрена команда из МИС на загрузку логов. В ответ Launcher собирает все лог-файлы по сетевым сервисам с рабочего узла, архивирует и высылает в МИС. Launcher при работе использует ротацию логов, что позволяет контролировать количество лог-файлов и занимаемый ими объём.



Обновление происходит по расписанию, в строго установленное время, которое задается в МИС. Но в случае необходимости при помощи соответствующей кнопки можно выполнить экстренное обновление выбранного рабочего места.



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


Процесс разработки


За счет использования framework Qt удалось сократить количество внешних зависимостей до одной библиотеки libarchive, необходимой для работы с архивами различных расширений.


Это обусловлено тем, что дистрибутивы загружаются в МИС в виде zip архива и в процессе обновления этот архив нужно распаковать. А при формировании логов для отправки на запрос МИС необходимо производить сжатие для сокращения объема данных.


Для использования представленных ниже примеров кода необходимо подключить следующие заголовочные файлы: #include <archive.h> и #include <archive_entry.h>.


Пример кода для разархивации:
bool unpackingArchive(const QString &nameArchive, const QString &pathToUnpacking){  archive *a;  archive_entry *entry;  int r;  a = archive_read_new();  archive_read_support_compression_all(a);  archive_read_support_format_all(a);  r = archive_read_open_filename(a, nameArchive.toStdString().c_str(),1024);  if (r != ARCHIVE_OK) {   QFile::remove(nameArchive);   return false; } QDir().mkpath(pathToUnpacking); while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {   __LA_MODE_T filetype = archive_entry_filetype(entry);   if (filetype == AE_IFREG)   {     int64_t entry_size;     const QString currentFile = archive_entry_pathname(entry);     if (currentFile.contains("/"))       QDir().mkpath(pathToUnpacking + "/" + currentFile.left(currentFile.lastIndexOf("/")));     entry_size = archive_entry_size(entry);     QByteArray fileContents;     fileContents.resize(static_cast<int>(entry_size));     archive_read_data(a, fileContents.data(), static_cast<size_t>(entry_size));     const QString nameFile = pathToUnpacking + "/" + currentFile;     QFile file(nameFile);     if (!file.open(QFile::WriteOnly))     {       QString errorMessage = "Error open file " +           QFileInfo(file).absoluteFilePath() + "; error string - " + file.errorString();       qCritical() << errorMessage;       return false;     }     file.write(fileContents,entry_size);     file.close();   } } archive_read_close(a); return true;}


Пример кода для архивации:
bool packingZipArchive(const QString &pathToFolder, const QString &nameArchive){ QDir dir(pathToFolder); QFileInfoList zipedList = dir.entryInfoList(QDir::Files); zipedList += dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); archive *zip = archive_write_new(); archive_write_set_format_zip(zip); if (archive_write_open_filename(zip, nameArchive.toStdString().c_str()) != ARCHIVE_OK) {   qCritical() << "Open zip file error: " << archive_error_string(zip);   return false; } archive_entry *entry = archive_entry_new(); for (const QFileInfo &i : zipedList) {   writeFile(entry, i, zip, dir.absolutePath()); } archive_entry_free(entry); archive_write_close(zip); archive_write_free(zip); return true;}void writeFile(archive_entry *entry, const QFileInfo &info, archive *arch, const QString &archiveRootDir){ if (info.isDir()) {   QFileInfoList condition;   condition << QDir(info.absoluteFilePath()).entryInfoList(QDir::Files) << QDir(info.absoluteFilePath()).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);   for (const QFileInfo &i :condition)     writeFile(entry, i, arch, archiveRootDir); } else {   struct stat st;   QFile file(info.absoluteFilePath());   if (!file.open(QFile::ReadOnly))   {     qCritical() << "File " << info.absoluteFilePath() << " not open: " << file.errorString();     return;   }   qDebug() << "Add file " << info.absoluteFilePath();   if (fstat(file.handle(), &st) == -1)   {     qWarning() << "Error read metadata from file " << info.absoluteFilePath();   }   else     archive_entry_copy_stat(entry,&st);   QString subPath = info.absoluteFilePath().remove(archiveRootDir);   QRegExp regex("^(./|/)");   if (subPath.indexOf(regex) > -1)     subPath = subPath.remove(regex);   archive_entry_set_pathname(entry, subPath.toStdString().c_str());   archive_entry_set_size(entry, info.size());   archive_entry_set_filetype(entry, AE_IFREG);   if (archive_write_header(arch, entry) != ARCHIVE_OK)   {     qCritical() << "Open zip file error: " << archive_error_string(arch);   }   else   {     QByteArray data = file.readAll();     archive_write_data(arch, data.data(), static_cast<size_t>(data.size()));   }    archive_entry_clear(entry);  }}


Процесс обновления Launcher


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


Чтобы этого избежать было решено проводить обновление следующим образом:



Запущенная версия Launcher проверяет доступность новых версий в МИС. При наличии обновления происходит скачивание нового дистрибутива, его распаковка и настройка. Затем текущая версия запускает обновлённый экземпляр программы, не завершая свою работу. После запуска обновлённая версия производит поиск запущенного ранее экземпляра и, если находит, то закрывает его. В окончание процесса обновления удаляются все старые файлы запущенной в начале версии Launcher. Тем самым достигается бесшовное обновление, риск сбоя при котором крайне мал.


Итог


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


P.S. В рамках данной статьи не удалось полностью раскрыть организацию работы по обновлению со стороны МИС, но это мы исправим в следующих публикациях. До новых встреч!

Подробнее..

Tango Controls

19.06.2020 16:07:35 | Автор: admin
main

Что такое TANGO?


Это система для управления различным оборудованием и программным обеспечением.
TANGO поддерживает 4 платформы на данный момент: Linux, Windows NT, Solaris и HP-UX.
Здесь будет описана работа с Linux(Ubuntu 18.04)


Где взять?



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


Из чего она состоит?


  • JIVE служит для просмотра и редактирования базы данных TANGO.
  • POGO генератор кода для серверов устройств TANGO.
  • Astor программный менеджер для системы TANGO.

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


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


  • C
  • C++
  • Java
  • JavaScript
  • Python
  • Matlab
  • LabVIEW

Я работал с ней на python & c++. Здесь в качестве примера будет использоваться c++.


Теперь перейдем к описанию как подключить устройство к TANGO и как с ним работать. В качестве примера будет взята плата GPS neo-6m-0-001:




Как видно на картинке плату к ПК подключаем через UART CP2102. При подключении к ПК появляется устройство /dev/ttyUSB[0-N], обычно /dev/ttyUSB0.


POGO


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



У меня уже был создан код, создадим его заново File->New.



Получаем следующее:



Наше устройство(под устройством в дальнейшем будет иметься ввиду программная часть) пустое и имеет две команды управления: State & Status.
Его нужно заполнить необходимыми атрибутами:
Device Property значения по умолчанию которые передаем в устройство для его инициализации, для платы GPS нужно передать имя платы в системе com="/dev/ttyUSB0" и скорость com порта baudrade=9600
Commands команды управления нашим устройством, им можно задать аргументы и возвращаемое значение.


  • STATE возвращает текущее состояние, из States
  • STATUS возвращает текущий статус, это строковое дополнение к STATE
  • GPSArray возвращает gps строку в виде DevVarCharArray
    Далее задаются атрибуты устройства которые можно читать/писать в/из него.
    Scalar Attributes простые атрибуты (char, string, long и т.п.)
    Spectrum Attributes одномерные массивы
    Image Attributes двумерные массивы

States состояния в котором находится наше устройство.


  • OPEN устройство открыто.
  • CLOSE устройство закрыто.
  • FAILT ошибка.
  • ON принимаем данные с устройства.
  • OFF нет данных с устройства.

Пример добавления атрибута gps_string:



Polling period время в мс, как часто будет обновляться значение gps_string. Если время обновления не задать, то атрибут будет обновляться только по запросу.


Получилось:



Теперь нужно с генерировать код File->Generate



По умолчанию Makefile не генерируется, в 1-ый раз нужно поставить галочку что бы его создать. Это сделано для того что бы внесенные в него правки не удалялись при новой генерации. Создав его единожды и настроив под свой проект(прописать ключи компиляции, доп. файлы) можно забыть про него.


Теперь переходим непосредственно к программированию. pogo с генерировал нам следующее:



Нас будут интересовать NEO6M.cpp & NEO6M.h. Рассмотрим для примера конструктор класса:


NEO6M::NEO6M(Tango::DeviceClass *cl, string &s) : TANGO_BASE_CLASS(cl, s.c_str()){    /*----- PROTECTED REGION ID(NEO6M::constructor_1) ENABLED START -----*/    init_device();    /*----- PROTECTED REGION END -----*/    //  NEO6M::constructor_1}

Что здесь есть и что здесь главное? В функции init_device() происходит выделение памяти для наших атрибутов: gps_string & gps_array, но это не важно. Самое важное здесь, это комментарии:


/*----- PROTECTED REGION ID(NEO6M::constructor_1) ENABLED START -----*/    ......./*----- PROTECTED REGION END -----*/    //  NEO6M::constructor_1

Все что находится внутри этого блока комментария при последующих перегенерациях кода в pogo не будет удаляться!. Все что в не блоках будет! Это те места где мы можем программировать и вносить свои правки.


Теперь какие главные функции содержит класс NEO6M:


void always_executed_hook();void read_attr_hardware(vector<long> &attr_list);void read_gps_string(Tango::Attribute &attr);void read_gps_array(Tango::Attribute &attr);

Когда мы захотим прочитать значение атрибута gps_string, будут вызваны функции в следующем порядке: always_executed_hook, read_attr_hardware и read_gps_string. В read_gps_string произойдет заполнение gps_string значением.


void NEO6M::read_gps_string(Tango::Attribute &attr){    DEBUG_STREAM << "NEO6M::read_gps_string(Tango::Attribute &attr) entering... " << endl;    /*----- PROTECTED REGION ID(NEO6M::read_gps_string) ENABLED START -----*/    //  Set the attribute value        *this->attr_gps_string_read = Tango::string_dup(this->gps.c_str());    attr.set_value(attr_gps_string_read);    /*----- PROTECTED REGION END -----*/    //  NEO6M::read_gps_string}

Компиляция


Заходим в папку с исходниками и:


make

Программа скомпилируется в папку ~/DeviceServers.


tango-cs@tangobox:~/DeviceServers$ lsNEO6M

JIVE



В БД уже есть какие-то устройства, создадим теперь наше Edit->Create Server



Теперь попробуем подключиться к нему:



Ни чего не выйдет, сначала надо запустить нашу программу:


sudo ./NEO6M neo6m -v2

Подключиться к com порту у меня можно только с правами root-а. v уровень логирования.


Теперь можем подключиться:



Клиент


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


#include <tango.h>using namespace Tango;int main(int argc, char **argv) {    try {        //        // create a connection to a TANGO device        //        DeviceProxy *device = new DeviceProxy("NEO6M/neo6m/1");        //        // Ping the device        //        device->ping();        //        // Execute a command on the device and extract the reply as a string        //        vector<Tango::DevUChar> gps_array;        DeviceData cmd_reply;        cmd_reply = device->command_inout("GPSArray");        cmd_reply >> gps_array;        for (int i = 0; i < gps_array.size(); i++) {                        printf("%c", gps_array[i]);        }        puts("");        //        // Read a device attribute (string data type)        //        string spr;        DeviceAttribute att_reply;        att_reply = device->read_attribute("gps_string");        att_reply >> spr;        cout << spr << endl;        vector<Tango::DevUChar> spr2;        DeviceAttribute att_reply2;        att_reply2 = device->read_attribute("gps_array");        att_reply2.extract_read(spr2);        for (int i = 0; i < spr2.size(); i++) {            printf("%c", spr2[i]);        }        puts("");    } catch (DevFailed &e) {        Except::print_exception(e);        exit(-1);    }}

Как компилировать:


g++ gps.cpp -I/usr/local/include/tango -I/usr/local/include -I/usr/local/include -std=c++0x -Dlinux -L/usr/local/lib -ltango -lomniDynamic4 -lCOS4 -lomniORB4 -lomnithread -llog4tango -lzmq -ldl -lpthread -lstdc++

Результат:


tango-cs@tangobox:~/workspace/c$ ./a.out $GPRMC,,V,,,,,,,,,,N*53$GPRMC,,V,,,,,,,,,,N*53$GPRMC,,V,,,,,,,,,,N*53

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


Ссылки



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

Подробнее..

Перевод Microsoft Rust является лучшим шансом в отрасли программирования безопасных систем

14.06.2020 12:18:17 | Автор: admin

Независимо от того, сколько вложений компании-разработчики могут потратить на инструментарий и обучение своих разработчиков, C++, по своей сути, не является безопасным языком, сказал Райан Левик (Ryan Levick) 'cloud developer advocate' из Microsoft на виртуальной конференции AllThingsOpen в прошлом месяце, объясняя в виртуальной беседе почему Microsoft постепенно переходит с C/C++ на Rust для создания своего инфраструктурного программного обеспечения. И вдохновляет других гигантов индустрии программного обеспечения задуматься о том же.



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


Фактически, Microsoft сочла C++ более неприемлемым для написания критически важных программ. Отрасль крайне нуждается в переходе на производительный, безопасный для памяти язык для низкоуровневой работы систем. А лучший выбор на рынке сегодня это Rust, сказал Левик.


C/C++ не могут быть исправлены


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


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


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


Конечно, есть ряд попыток повысить безопасность C++, но несмотря на то, что каждая из них эффективна, они не решает проблему полностью.


Один из подходов, который давно используется, состоит в том, чтобы больше обучать программистов написанию более безопасного кода. Но нет никаких доказательств того, что комплексное обучение разработчиков на C и C++ действительно решит эту проблему любым существенным способом, сказал Левик, сославшись на собственные наработки в Microsoft, предназначенные для внутреннего обучения разработчиков.


Статический анализ упоминается, как еще одно возможное решение. Но статический анализ требует слишком много времени: его необходимо подключить к системе сборки. Так что есть большой стимул не использовать статический анализ сказал Левик. Если он не включен по умолчанию, он не поможет.


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


Лучший шанс для отрасли


В ответ на проблему ошибок связанных с использованием памяти Центр реагирования Microsoft Security запустил инициативу Язык программирования безопасных систем. В ней некоторая работа была посвящена укреплению C/C++. Язык Верона новый язык программирования, создаваемый для безопасного низкоуровневого программирования, также был создан здесь. Но третий аспект стратегии этого проекта, в которую они верят больше всего, заключается в том, чтобы поддержать наилучшие шансы отрасли для непосредственного решения этой проблемы.


И мы считаем, что это Rust, сказал он.


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


Но главная причина, по которой Microsoft так увлечена Rust, заключается в том, что это язык безопасный для памяти, с минимальными проверками во время выполнения. Rust выделяется в создании корректных программ. Корректность означает примерно то, что программа проверяется компилятором на небезопасные операции, что приводит к меньшему количеству ошибок во время выполнения. Ключевое слово 'unsafe' является опцией, но не используется по умолчанию. Небезопасный код в Rust почти всегда является подмножеством большего объема безопасного кода. Небезопасный 'unsafe' режим необходим для задач по выделению памяти, например для написания драйверов устройств. Но даже здесь небезопасные части заключены в безопасный API.


По словам Левика, эту способность безопасно программировать не следует воспринимать легкомысленно. Фактически, Rust может обеспечить более чем 10-кратное улучшение, что делает его полезным для инвестиций. Во многом это связано с тем, что практически весь код C/C++ нуждается в аудите на предмет небезопасного поведения, в то время как небезопасный код написанный на Rust, который необходимо будет проверить, является лишь небольшим подмножеством основной кодовой базы.


Хотя Microsoft оптимистично настроена на Rust, Левик признает, что разработчики ядра Microsoft не прекратят использовать C/C++ в ближайшее время.


У нас в Microsoft много C++ и этот код никуда не денется сказал он. Фактически, Microsoft продолжает писать c++ и будет писать некоторое время.


Много инструментария построено на C/C ++. В частности, двоичные файлы Microsoft сейчас почти полностью собраны компилятором Microsoft Visual C++, который создает двоичные файлы MSVC, тогда как Rust использует LLVM.


Возможно, самая большая проблема, тем не менее, является культурной. Есть некоторые люди, которые хотят выполнить свою работу на языке, который они уже знают, признался Левик.


Тем не менее, индустрия, похоже, движется в сторону Rust. Amazon Web Services использует его, в частности для развертывания безсерверной среды выполнения (Lambda serverless runtime), а также для некоторых частей EC2. Facebook начал использовать Rust как и Apple, Google, Dropbox и Cloudflare.


YouTube: Ryan Levick - Rust at Microsoft

Объявлены даты All Things Open 2020: 20-22 октября.


Ресурс "The New Stack" не позволяет оставлять комментарии непосредственно на сайте. Мы приглашаем всех читателей, которые хотят обсудить историю или оставить комментарий, посетить нас в Twitter или Facebook. Мы также приветствуем ваши новостные советы и отзывы по электронной почте: **feedback@thenewstack.io.


Amazon Web Services является спонсором The New Stack.


2020 The New Stack. All rights reserved.

10 июня 2020 10:10 автор Joab Jackson
Подробнее..

Долой циклы, или Неленивая композиция алгоритмов в C

15.06.2020 16:10:07 | Автор: admin
"Кто ни разу не ошибался в индексировании цикла, пусть первый бросит в деструкторе исключение."

Древняя мудрость

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


В конце концов, это просто некрасиво.


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


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


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



Содержание


  1. Существующие модели
  2. Базовые понятия
    1. Определение 1: свёртка
    2. Определение 2: ядро свёртки
  3. Идеология
    1. Факт 1: каждый цикл можно представить в виде свёртки
    2. Факт 2: большинство циклов расладываются на простые составляющие
    3. Факт 3: каждую свёртку можно представить в виде автомата
    4. Факт 4: автоматы комбинируются
    5. Снова к свёртке
  4. Я птичка, мне такое сложно, можно я сразу код посмотрю?
    1. Простой пример
    2. constexpr
  5. Многопоточность
  6. Сравнительная таблица
  7. Ссылки


Существующие модели


Основные на текущий момент способы избавления от циклов это алгоритмы из стандартной библиотеки и ленивые итераторы и диапазоны из библиотек Boost.Iterator, Boost.Range и range-v3.


range-v3 частично попали в стандартную библиотеку C++20, но, во-первых, попали они туда в достаточно усечённом виде, а во-вторых, соответствующих реализаций на текущий момент пока нет.

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


Именно из-за этого появились ленивые итераторы и диапазоны в сторонних библиотеках, а в C++17 появились гибриды семейства std::transform_reduce.


Ленивые итераторы и диапазоны решают многие проблемы. Но они сами не лишены своих собственных проблем. В частности, поскольку они отделены от схемы вычислений (они определяют только операции над отдельными элементами последовательности), их сложно параллелить. А стандартные алгоритмы уже с C++17 имеют параллельные версии, способные более эффективно использовать многоядерные архитектуры.


Возникает вопрос: можно ли объединить преимущества обоих подходов одновременно? Оказывается, можно. Об этом и пойдёт речь далее.



Базовые понятия


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



Определение 1: свёртка


Свёртка это вычислительный процесс, производимый над некоторой последовательностью значений по правилу, задаваемому ядром свёртки.


Результат свёртки значение, полученное последовательным применением ядра свёртки к текущему значению и очередному элементу последовательности.



Определение 2: ядро свёртки


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


Свёртка


На этом рисунке изображена свёртка последовательности $\{x_0, x_1, x_2\}$ с ядром $f$ и начальным значением $v_0$. $v_3$ результат свёртки.


В стандартной библиотеке свёртка представлена алгоритмами std::accumulate и std::reduce.



Идеология


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



Факт 1: каждый цикл можно представить в виде свёртки


И действительно:


  1. Контекст программы перед началом цикла начальное значение;
  2. Набор индексов, контейнер, диапазон и т.п. последовательность элементов;
  3. Итерация цикла применение двуместной операции (ядра свёртки) к текущему значению и очередному элементу последовательности, в результате чего текущее значение изменяется.

auto v = 0;                   // Начальное значение: v_0for (auto i = 0; i < 10; ++i) // Последовательность: [x_0, x_1, ...]{    v = f(v, i);              // Двуместная операция, изменяющая                              // значение: v_{i + 1} = f(v_i, x_i)}

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


Пример 1: отображение через свёртку


template <ForwardIterator I, OutputIterator J, UnaryFunction F>J transform (I begin, I end, J result, F f){    // Начальное значение  это выходной итератор.    auto initial_value = result;    // Ядро свёртки.    auto binary_op =        [] (auto iterator, auto next_element)        {            // Записываем в текущий итератор результат отображения...            *iterator = f(next_element);            // ... и возвращаем продвинутый итератор.            return ++iterator;        };    // Свёртка.    return accumulate(begin, end, initial_value, binary_op);}

Пример 2: фильтрация через свёртку


template <ForwardIterator I, OutputIterator J, UnaryPredicate P>J copy_if (I begin, I end, J result, P p){    // Начальное значение.    auto initial_value = result;    // Ядро свёртки.    auto binary_op =        [p] (auto iterator, auto next_element)        {            if (p(next_element))            {                *iterator = next_element;                ++iterator;            }            return iterator;        };    // Свёртка.    return accumulate(begin, end, initial_value, binary_op);}

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



Факт 2: большинство циклов расладываются на простые составляющие


Если присмотреться, то станет понятно, что большинство циклов типовые. Они раскладываются на простые составляющие:


  • Преобразование;
  • Фильтрация;
  • Группировка;
  • Подсчёт;
  • Суммирование;
  • Запись в массив;
  • ...
  • и т.д.

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



Факт 3: каждую свёртку можно представить в виде автомата


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


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


Важно:

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

Кроме того, наш автомат может обладать памятью.

Автомат


Пример 1: автомат для отображения


Например, так будет выглядеть автомат для отображения (transform, или map в функциональном программировании).


Автомат для отображения


Здесь $a$ входной символ, $f$ функция преобразования.


Данный автомат имеет одно состояние и один переход. Каждый входной символ $a$ он преобразует с помощью функции $f$, и результат этого преобразования подаёт на выход. После этого возвращается в исходное состояние.


Пример 2: автомат для фильтрации


Автомат для фильтрации


Здесь $a$ входной символ, $p$ предикат, $\epsilon$ обозначение пустого символа.


Данный автомат имеет одно состояние и два перехода. Один переход реализуется тогда, когда входной символ $a$ удовлетворяет предикату $p$. В этом случае на выход подаётся сам символ $a$. В случае, если символ $a$ не удовлетворяет предикату, на выход подаётся пустой символ $\epsilon$ (то есть ничего не подаётся). В обоих случаях автомат возвращается в исходное состояние.



Факт 4: автоматы комбинируются


Если у автомата есть выход, то, очевидно, этот выход можно подать на вход другому автомату.


Композиция автоматов


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



Снова к свёртке


Чтобы получить нужную нам свёртку, в конец цепочки мы поставим автомат, который представляет собой ядро свёртки.


Цепочка с ядром в конце


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


Схлопнули


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



Код


На основе изложенных выше идей разработана библиотека Проксима.



Простой пример


#include <proxima/compose.hpp>#include <proxima/kernel/sum.hpp>#include <proxima/reduce.hpp>#include <proxima/transducer/stride.hpp>#include <proxima/transducer/take_while.hpp>#include <proxima/transducer/transform.hpp>#include <cassert>int main (){    const int items[] = {1, 2, 3, 4, 5};    const auto kernel =        proxima::compose        (            proxima::transform([] (auto x) {return x * x;}),   // 1. Каждый элемент возведён в квадрат;            proxima::stride(2),                                // 2. Берутся только элементы с номерами,                                                               //    кратными двойке (нумерация с нуля);            proxima::take_while([] (auto x) {return x < 10;}), // 3. Элементы берутся до тех пор, пока                                                               //    они меньше десяти;            proxima::sum                                       // 4. Результат суммируется.        );    const auto x = proxima::reduce(items, kernel);    assert(x == 10); // 1 * 1 + 3 * 3}


constexpr


Можно отметить, что код из примера может быть выполнен на этапе компиляции:


#include <proxima/compose.hpp>#include <proxima/kernel/sum.hpp>#include <proxima/reduce.hpp>#include <proxima/transducer/stride.hpp>#include <proxima/transducer/take_while.hpp>#include <proxima/transducer/transform.hpp>int main (){    constexpr int items[] = {1, 2, 3, 4, 5};    constexpr auto kernel =        proxima::compose        (            proxima::transform([] (auto x) {return x * x;}),   // 1. Каждый элемент возведён в квадрат;            proxima::stride(2),                                // 2. Берутся только элементы с номерами,                                                               //    кратными двойке (нумерация с нуля);            proxima::take_while([] (auto x) {return x < 10;}), // 3. Элементы берутся до тех пор, пока                                                               //    они меньше десяти;            proxima::sum                                       // 4. Результат суммируется.        );    constexpr auto x = proxima::reduce(items, kernel);    static_assert(x == 10); // 1 * 1 + 3 * 3}

Большая часть Проксимы может быть выполнена на этапе компиляции.



Многопоточность


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


В Проксиме существует механизм, с помощью которого очень легко распараллеливать вычисления. Это делается с помощью фиктивного преобразователя pipe, который выполняет роль "разделителя потоков":


proxima::reduce(values,    proxima::compose    (        proxima::for_each(hard_work), // | Поток 1                                      // ----------        proxima::pipe,                //            Разделитель потоков                                      // ----------        proxima::for_each(hard_work), // | Поток 2                                      // ----------        proxima::pipe,                //            Разделитель потоков                                      // ----------        proxima::for_each(hard_work), // | Поток 3        proxima::sum                  // | Поток 3    ));

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


Чтобы показать эффективность такого разбиения, рассмотрим пример (полный код лежит на Гитлабе).


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


auto hard_work (std::int32_t time_to_sleep){    std::this_thread::sleep_for(std::chrono::microseconds(time_to_sleep));}const auto proxima_crunch_parallel =    [] (auto b, auto e)    {        return            proxima::reduce(b, e,                proxima::compose                (                    proxima::for_each(hard_work),                    proxima::pipe,                    proxima::for_each(hard_work),                    proxima::pipe,                    proxima::for_each(hard_work),                    proxima::sum                ));    };const auto proxima_crunch =    [] (auto b, auto e)    {        return            proxima::reduce(b, e,                proxima::compose                (                    proxima::for_each(hard_work),                    proxima::for_each(hard_work),                    proxima::for_each(hard_work),                    proxima::sum                ));    };const auto loop_crunch =    [] (auto b, auto e)    {        auto sum = typename decltype(b)::value_type{0};        while (b != e)        {            hard_work(*b);            hard_work(*b);            hard_work(*b);            sum += *b;            ++b;        }        return sum;    };

Если сгенерировать 1000 случайных засыпаний в диапазоне от 10 до 20 микросекунд, то получим следующую картину (показано время работы соответствующего обработчика чем меньше, тем лучше):


proxima_crunch_parallel | 0.0403945proxima_crunch          | 0.100419loop_crunch             | 0.103092

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


proxima_crunch_parallel | 0.213352proxima_crunch          | 0.624727loop_crunch             | 0.625393

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



Сравнительная таблица


Библиотека STL (алгоритмы) Boost range-v3 Проксима
Компонуемость Нет Да Да Да
Вывод типов Плохо Средне Средне Хорошо
Параллелизация Почти* Нет Нет Да
Совместимость Boost STL STL Всё
Расширяемость Сложно Нормально Сложно Легко
Самостоятельность Да Да Да Не совсем
constexpr Частично Нет Частично** Да***
Модель Монолитная Ленивая Ленивая Неленивая

*) Параллелизация в STL ещё не везде реализована.

**) constexpr диапазонов, видимо, будет лучше, когда они попадут в STL.

***) constexpr Проксимы зависит от STL. Всё, что своё уже constexpr. Всё, что зависит от STL, будет constexpr как только в STL оно будет таковым.


Ссылки


  1. Проксима
  2. Алгоритмы STL
  3. atria::xform
  4. Boost.Iterator
  5. Boost.Range
  6. range-v3
  7. Абстрактный автомат
Подробнее..

Перевод volatile vs. volatile

19.06.2020 18:10:12 | Автор: admin
Всем привет! Мы подготовили перевод данной статьи в преддверии старта курса Разработчик C++



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

Херб автор бестселлеров и консультант по вопросам разработки программного обеспечения, а также архитектор ПО в Microsoft. Вы можете связаться с ним на www.gotw.ca.



Что означает ключевое слово volatile? Как его следует использовать? К всеобщему замешательству, существует два распространенных ответа, потому что в зависимости от языка, на котором вы пишете код, volatile относится к одной из двух различных техник программирования: lock-free программированию (без блокировок) и работе со необычной памятью. (См. Рисунок 1.)


Рисунок 1: повесть о двух технических требованиях.

Усугубляет путаницу и то, что эти два различных случая использования имеют частично совпадающие предпосылки и накладываемые ограничения, что заставляет их выглядеть более схожими, нежели они являются на самом деле. Давайте же четко определим и поймем их, и разберемся, как их правильно употреблять в C, C++, Java и C# и всегда ли именно как volatile.


Таблица 1: Сравнение накладывающихся, но разных предпосылок.

Случай 1: Упорядоченные атомарные переменные для lock-free программирования


Lock-free программирование связано с налаживанием коммуникации и синхронизации между потоками с помощью инструментов более низкого уровня, нежели взаимоисключающие блокировки. Как в прошлом, так и сегодня существует широкий спектр таких инструментов. В грубом историческом порядке они включают явные барьеры (explicit fences/barriers например, mb() в Linux), специальные упорядочивающие вызовы API (например, InterlockedExchange в Windows) и различные разновидности специальных атомарных типов. Многие из этих инструментов муторны и/или сложны, и их широкое разнообразие означает, что в конечном итоге lock-free код пишется в разных средах по-разному.

Однако в последние несколько лет наблюдается значительная конвергенция между поставщиками аппаратного и программного обеспечения: вычислительная индустрия объединяется вокруг последовательно согласованных упорядоченных атомарных переменных (ordered atomic variables) в качестве стандарта или единственного способа написания lock-free кода с использованием основных языков и платформ ОС. В двух словах, упорядоченные атомарные переменные безопасны для чтения и записи в нескольких потоках одновременно без каких-либо явных блокировок, поскольку они обеспечивают две гарантии: их чтение и запись гарантированно будут выполняться в том порядке, в котором они появляются в исходном коде вашей программы; и каждое чтение или запись гарантированно будут атомарными, все или ничего. У них также есть специальные операции, такие как compareAndSet, которые гарантированно выполняются атомарно. См. [1] для получения дополнительной информации об упорядоченных атомарных переменных и о том, как их правильно использовать.

Упорядоченные атомарные переменные доступны в Java, C# и других языках .NET, а также в готовящемся стандарте ISO C++, но под другими именами:

  • Java предоставляет упорядоченные атомарные переменные под ключевым словом volatile (например, volatile int), полностью поддерживая это с Java 5 (2004). Java дополнительно предоставляет несколько именованных типов в java.util.concurrent.atomic, например, AtomicLongArray, который вы можете использовать для тех же целей.
  • .NET добавил их в Visual Studio 2005, также под ключевым словом volatile (например, volatile int). Они подходят почти для любого варианта использования lock-free кода, за исключением редких примеров, подобных алгоритму Деккера. .NET исправляет оставшиеся ошибки в Visual Studio 2010, которая находится на стадии бета-тестирования на момент написания этой статьи.
  • ISO C++ добавил их в черновик стандарта C++ 0x в 2007 году под шаблонным именем atomic <T> (например, atomic). С 2008 года они стали доступны в Boost и некоторых других реализациях. [2]. Библиотека atomic ISO C++ также предоставляет C-совместимый способ написания этих типов и их операций (например, atomic_int), и они, вероятно, будут приняты ISO C в ближайшем будущем.

Пару слов об оптимизации


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

Фундаментальное правило оптимизации практически во всех языках таково: оптимизации, которые переупорядочивают (трансформируют) выполнение вашего кода, всегда являются легитимными, только если они не меняют смысла программы, так что программа не может определить разницу между выполнением исходного кода и преобразованного. В некоторых языках это также известно как правило as-if, которое получает свое название из-за того факта, что преобразованный код имеет те же наблюдаемые эффекты, как если бы (as if) исходный исходный код был выполнен в том виде, в котором он был изначально написан.

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

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

Упорядоченные атомарные переменные и оптимизация


Использование упорядоченных атомарных переменных ограничивает виды оптимизации, которые может выполнять ваш компилятор, процессор и система кэширования. [3] Стоит отметить два вида оптимизаций:

  • Оптимизации упорядоченных атомарных операций чтения и записи.
  • Оптимизации соседних обычных операций чтения и записи.

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

Например, рассмотрим этот код, где a упорядоченная атомарная переменная:

a = 1;  // Aa = 2;  // B

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

// A ': OK: полностью исключить строку A a = 2;  // B

Ответ: Да. Это легитимно, потому что программа не может определить разницу; это как если бы этот поток всегда работал так быстро, что никакой другой поток, работающий параллельно, в принципе не может чередоваться между строками A и B, чтобы увидеть промежуточное значение. [4]

Аналогично, если a упорядоченная атомарная переменная, а local неразделяемая локальная переменная, допустимо преобразовать

a = 1;  // C: запись в alocal = a;  // D: чтение из a

в

a = 1;  // C: запись в alocal = 1;  // D': OK, применить "подстановку константы"

что исключает чтение из a. Даже если другой поток одновременно пытается выполнить запись в a, это как если бы этот поток всегда работал так быстро, что другому потоку никогда не удавалось чередовать строки C и D, чтобы изменить значение, прежде чем мы успеем записать наше собственное обратно в local.

Во-вторых, близлежащие обычные операции чтения и записи все еще могут быть переупорядочены вокруг упорядоченных атомарных, но с некоторыми ограничениями. В частности, как описано в [3], обычные операции чтения и записи не могут перемещаться вверх по отношению к упорядоченному атомарному чтению (от после к до) и не могут перемещаться вниз по отношению к упорядоченной атомарной записи (от до к после). Короче говоря, это может вывести их из критического раздела кода, и вы сможете писать программы, которые выиграют от этого в производительности. Для получения более подробной информации см. [3].

На этом все касательно lock-free программирования и упорядоченных атомарных переменных. А как насчет другого случая, в котором рассматриваются какие-то волатильные адреса?

Случай 2: Свободные от семантики переменные для памяти с необычной семантикой


  • Вторая необходимость работать с необычной памятью, которая выходит за рамки модели памяти данного языка, где компилятор должен предполагать, что переменная может изменить значение в любое время и/или что чтение и запись могут иметь непознаваемую семантику и следствия. Классические примеры:
  • Аппаратные регистры, часть 1: Асинхронные изменения. Например, рассмотрим ячейку памяти М на пользовательской плате, которая подключена к прибору, который производит запись непосредственно в M. В отличие от обычной памяти, которая изменяется только самой программой, значение, хранящееся в M, может измениться в любое время, даже если ни один программный поток не пишет в нее; следовательно, компилятор не может делать никаких предположений о том, что значение будет стабильным.
  • Аппаратные регистры, часть 2: Семантика. Например, рассмотрим область памяти M на пользовательской плате, где запись в эту позицию всегда автоматически увеличивается на единицу. В отличие от обычного места в RAM памяти, компилятор даже не может предположить, что выполнение записи в M и последующее сразу после нее чтение из M обязательно прочитает то же значение, которое было записано.
  • Память, имеющая более одного адреса. Если данная ячейка памяти доступна с использованием двух разных адресов А1 и А2, компилятор или процессор может не знать, что запись в ячейку А1 может изменить значение в ячейке А2. Любая оптимизация, предполагающая? что запись в A1, не изменяет значение A2, будет ломать программу, и должна быть предотвращена.

Переменные в таких местах памяти являются неоптимизируемыми переменными, потому что компилятор не может безопасно делать какие-либо предположения о них вообще. Иными словами, компилятору нужно сказать, что такая переменная не участвует в обычной системе типов, даже если она имеет конкретный тип. Например, если ячейка памяти M или A1/A2 в вышеупомянутых примерах в программе объявлена как int, то что это в действительности означает? Самое большее, что это может означать, это то, что она имеет размер и расположение int, но это не может означать, что он ведет себя как int в конце концов, int не автоинкрементируют себя, когда вы записываете в него, или таинственным образом не изменяет свое значение, когда вы совершите запись во что-то похожее на другую переменную по другому адресу.

Нам нужен способ отключить все оптимизации для их чтения и записи. ISO C и C++ имеют портативный, стандартный способ сообщить компилятору, что это такая специальная переменная, которую он не должен оптимизировать: volatile.

Java и .NET не имеют сопоставимой концепции. В конце концов, управляемые среды должны знать полную семантику программы, которую они выполняют, поэтому неудивительно, что они не поддерживают память с непознаваемой семантикой. Но и Java, и .NET предоставляют аварийные шлюзы для выхода из управляемой среды и вызова нативного кода: Java предоставляет Java Native Interface (JNI), а .NET предоставляет Platform Invoke (P/Invoke). Однако в спецификации JNI [5] о volatile ничего не говорится и вообще не упоминается ни Java volatile, ни C/C++ volatile; аналогично, в документации P/Invoke не упоминается взаимодействие с .NET volatile или C/C++ volatile. Таким образом, для правильного доступа к неоптимизируемой области памяти в Java или .NET вы должны написать функции C/C++, которые используют C/C++ volatile для выполнения необходимой работы от имени вызывающего их уравляющего кода, чтобы они полностью инкапсулировали и скрывали volatile память (т. е. не принимали и не возвращали ничего volatile) и вызывать эти функции через JNI и P/Invoke.

Неоптимизируемые переменные и (не) оптимизация


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

Рассмотрим снова два преобразования, которые мы рассматривали ранее, но на этот раз заменим упорядоченную атомарную переменную a на неоптимизируемую (C/C++ volatile) переменную v:

v = 1;  // Av = 2;  // B

Легитимно ли это преобразовать следующим образом, чтобы удалить явно лишнюю запись в строке A?

// A ': невалидно, нельзя исключить записьv = 2;  // B

Ответ нет, потому что компилятор не может знать, что исключение записи строки A в v не изменит смысла программы. Например, v может быть местоположением, к которому обращается пользовательское оборудование, которое ожидает увидеть значение 1 перед значением 2 и иначе не будет работать правильно.

Аналогично, если v неоптимизируемая переменная, а local неразделяемая локальная переменная, преобразование недопустимо

v = 1;            // C: запись в vlocal = v;        // C: чтение из v

в

a = 1;         // C: запись в vlocal = l;   // D': невалидно, нельзя совершить// "подстановку константы"

для упразднения чтение из v. Например, v может быть аппаратным адресом, который автоматически увеличивается каждый раз при записи, так что запись 1 даст значение 2 при следующем считывании.

Во-вторых, что насчет соседних обычных операций чтения и записи можно ли их переупорядочить вокруг неоптимизируемых? Сегодня нет практического портативного ответа, потому что реализации компилятора C/C++ сильно различаются и вряд ли в скором времени начнут движение к единообразию. Например, одна интерпретация Стандарта C++ гласит, что обычные операции чтения могут свободно перемещаться в любом направлении относительно чтения или записи volatile C/C++, а вот обычная запись вообще не может перемещаться относительно чтения или записи volatile C/C++ что делает volatile C/C++ в то же время и менее и более ограничительным, чем упорядоченные атомарные операции. Некоторые поставщики компиляторов поддерживают эту интерпретацию; другие вообще не оптимизируют чтение или запись volatile; а третьи имеют свою собственную семантику.

Резюме


Для написания безопасного lock-free кода, который коммуницирует между потоками без использования блокировок, предпочитайте использовать упорядоченные атомарные переменные: Java/.NET volatile, C++0x atomic<T>и C-совместимый atomic_T.

Чтобы безопасно обмениваться данными со специальным оборудованием или другой памятью с необычной семантикой, используйте неоптимизируемые переменные: ISO C/C++ volatile. Помните, что чтение и запись этих переменных не обязательно должны быть атомарными.

И наконец, чтобы объявить переменную, которая имеет необычную семантику и обладает какой-либо из или же сразу всеми гарантиями атомарности и/или упорядочения, необходимыми для написания lock-free кода, только черновик стандарта ISO C++0x предоставляет прямой способ ее реализации: volatile atomic <T>.

Примечания
  1. Г. Саттер. Writing Lock-Free Code: A Corrected Queue (DDJ, октябрь 2008 г.). Доступно online тут.
  2. [2] См. www.boost.org.
  3. [3] Г. Саттер. Apply Critical Sections Consistently (DDJ, ноябрь 2007 г.). Доступно в Интернете тут.
  4. [4] Существует распространенное возражение: В исходном коде другой поток мог видеть промежуточное значение, но это невозможно в преобразованном коде. Разве это не изменение наблюдаемого поведения? ответ: Нет, потому что программе никогда не гарантировалось, что она будет фактически чередоваться как раз вовремя, чтобы увидеть это значение; для этого потока уже был легитимный результат он всегда работал так быстро, что чередование никогда не случалось. Опять же, то, что следует из этой оптимизации, так это уменьшает набор возможных исполнений, что всегда является легитимным.
  5. [5] С. Лянг. Java Native Interface: Руководство программиста и спецификация. (Прентис Холл, 1999). Доступно online тут.



Бесплатный вебинар: Hello, World! на фарси или как использовать Unicode в C++"


Подробнее..

Перевод Учебник по симулятору сети ns-3. Заключительные главы 8, 9

23.06.2020 06:21:42 | Автор: admin
image
[главы 1,2]
[глава 3]
[глава 4]
[глава 5]
[глава 6]
[глава 7]

Глава 8 Сбор информации
8.1 Мотивация
8.2 Пример кода
8.3 GnuplotHelper
8.4 Поддерживаемые типы трасс
8.5 FileHelper
8.6 Итоги
Глава 9 Заключение
9.1 На будущее
9.2 Завершение

Глава 8Сбор информации


В последней главе этого руководства представлены некоторые компоненты, которые были добавлены вns3в версии 3.18 и те, что сейчас находятся в стадии разработки. Этот раздел также находится в стадии разработки.

8.1Мотивация


Одной из основных целей запуска симуляции является генерация выходных данных, либо для исследовательских целей, либо просто для изучения системы. В предыдущей главе мы представили подсистему трассировки и примерsixth.cc, который генерировал файлы PCAP или ASCII трассировок. Эти трассы ценны для анализа данных с использованием различных внешних инструментов и для многих пользователей такие выходные данные предпочтительны с точки зрения сбора данных (для анализа с помощью внешних инструментов).

Тем не менее, существуют варианты использования не только для генерации файла трассировки, они включат следующие:

  • генерация данных, которые плохо отображаются в PCAP или ASCII трассировки, данные которые не представлены в виде пакетов (например, переходы машины состояний протокола);
  • большие симуляции, для которых требования к диску по вводу/выводу во время генерации файлов трассировки являются непомерными или громоздкими;
  • необходимость сокращения или вычисления данных в режиме онлайн во время моделирования. Хороший пример этого определение условия завершения симуляции, чтобы остановить её при получении необходимого количества данных для формирования достаточно узкого доверительного интервала вокруг оценки некоторого параметра.

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

8.2Пример кода


Учебный примерexamples/tutorial/sevenh.ccнапоминает примерsixth.cc, который мы ранее проработали, за исключением нескольких изменений. Во-первых, в него было добавлено переключение на поддержку IPv6 параметром командной строки:
CommandLine cmd;cmd.AddValue ("useIpv6", "Use Ipv6", useV6);cmd.Parse (argc, argv);

Если пользователь указывает опциюuseIpv6, программа будет работать с использованием IPv6 вместо IPv4. Во всех программахns3, которые поддерживают объектCommandLine, доступна опция справки. Как показано выше, она может быть вызвана следующим образом (обратите внимание на использование двойных кавычек):
./waf --run "seventh --help"

который выдаст:
ns3-dev-seventh-debug [Program Arguments] [General Arguments]Program Arguments:--useIpv6: Use Ipv6 [false]General Arguments:--PrintGlobals: Print the list of globals.--PrintGroups: Print the list of groups.--PrintGroup=[group]: Print all TypeIds of group.--PrintTypeIds: Print all TypeIds.--PrintAttributes=[typeid]: Print all attributes of typeid.--PrintHelp: Print this help message.

Режим по умолчанию (использование IPv4, посколькуuseIpv6имеет значениеfalse) можно изменить, переключив логическое значение следующим образом:
./waf --run "seventh --useIpv6=1"

и посмотрите на сгенерированныйpcap, например, с помощьюtcpdump:
tcpdump -r seventh.pcap -nn -tt

Это было короткое отступление в поддержку IPv6 и командной строки, которая также была представлена ранее в этом руководстве. В качестве отдельного примера использования командной строки, пожалуйста, смотритеsrc/core/examples/command-line-example.cc. Теперь вернемся к сбору данных. В директорииexamples/tutorial/введем следующую команду:
diff -u sixth.cc sevenh.cc

и рассмотрим некоторые из новых строк этогоdiff:
+ std::string probeType;+ std::string tracePath;+ if (useV6 == false)+ {...+ probeType = "ns3::Ipv4PacketProbe";+ tracePath = "/NodeList/*/$ns3::Ipv4L3Protocol/Tx";+ }+ else+ {...+ probeType = "ns3::Ipv6PacketProbe";+ tracePath = "/NodeList/*/$ns3::Ipv6L3Protocol/Tx";+ }...+ // Use GnuplotHelper to plot the packet byte count over time+ GnuplotHelper plotHelper;++ // Configure the plot. The first argument is the file name prefix+ // for the output files generated. The second, third, and fourth+ // arguments are, respectively, the plot title, x-axis, and y-axis labels+ plotHelper.ConfigurePlot ("seventh-packet-byte-count",+ "Packet Byte Count vs. Time",+ "Time (Seconds)",+ "Packet Byte Count");++ // Specify the probe type, trace source path (in configuration namespace), and+ // probe output trace source ("OutputBytes") to plot. The fourth argument+ // specifies the name of the data series label on the plot. The last+ // argument formats the plot by specifying where the key should be placed.+ plotHelper.PlotProbe (probeType,+ tracePath,+ "OutputBytes",+ "Packet Byte Count",+ GnuplotAggregator::KEY_BELOW);++ // Use FileHelper to write out the packet byte count over time+ FileHelper fileHelper;++ // Configure the file to be written, and the formatting of output data.+ fileHelper.ConfigureFile ("seventh-packet-byte-count",+ FileAggregator::FORMATTED);++ // Set the labels for this formatted output file.+ fileHelper.Set2dFormat ("Time (Seconds) = %.3e\tPacket Byte Count = %.0f");++ // Specify the probe type, probe path (in configuration namespace), and+ // probe output trace source ("OutputBytes") to write.+ fileHelper.WriteProbe (probeType,+ tracePath,+ "OutputBytes");+Simulator::Stop (Seconds (20));Simulator::Run ();Simulator::Destroy ();

Внимательный читатель заметит, что при тестировании атрибута командной строки IPv6, описанного выше, файлsevenh.ccсоздал ряд новых выходных файлов:
seventh-packet-byte-count-0.txtseventh-packet-byte-count-1.txtseventh-packet-byte-count.datseventh-packet-byte-count.pltseventh-packet-byte-count.pngseventh-packet-byte-count.sh

Они были созданы дополнительными объявлениями, представленными выше. В частности,GnuplotHelperиFileHelper.

Эти данные были получены путем подключения к источникам трассировкиns3компонентов сбора и упорядочения данных для форматированногоgnuplotи форматированного текстового файлов. В следующих разделах мы рассмотрим каждый из них.

8.3GnuplotHelper


GnuplotHelper- это вспомогательный объектns3, предназначенный для создания графиковgnuplotдля общих случаев с минимальным, по возможности, количеством операторов.

Он подключается к источникам трассировкиns3с типами данных, поддерживаемыми системой сбора данных. Некоторые типы данных источников трассировкиns3не поддерживаются, но многие из общих типов трассировки, включаяTracedValuesс простыми старыми типами данных (POD) могут быть обработаны этим помощником.

Давайте посмотрим на вывод этого помощника:
seventh-packet-byte-count.datseventh-packet-byte-count.pltseventh-packet-byte-count.sh

Первый это файл данныхgnuplotс серией меток времени, разделенных пробелами, и счетчиками байтов пакетов. Мы рассмотрим ниже как этот конкретный вывод данных был настроен, но давайте продолжим с выходными файлами. Файлseventh-packet-byte-count.plt- это файл графикаgnuplot, который можно открыть вgnuplot. Читатели, которые понимают синтаксисgnuplot, могут увидеть, что он создаст отформатированный выходной файл PNG с именемseventh-packet-byte-count.png. Наконец, небольшой shell-сценарийsevenh-packet-byte-count.shзапускает этот файл графика черезgnuplotдля получения желаемого PNG (который можно просмотреть в редакторе изображений), после команды:
sh seventh-packet-byte-count.sh

выдастseventh-packet-byte-count.png. Почему этот PNG не был сделан сразу? Ответ заключается в том, чтобы пользователь, если хочет, мог вручную настроить предоставленный файлplt, перед созданием PNG. Название PNG-изображения показывает, что этот график представляет собой Число байтов пакетов в зависимости от времени и что он отображает захваченные данные, соответствующие пути источника трассировки:
/NodeList/*/$ns3::Ipv6L3Protocol/Tx

Обратите внимание на знак подстановки в пути трассировки. Итак, этот график захвата байтов пакетов, наблюдаемых на источнике трассировки объектаIpv6L3Protocol; в основном 596-байтовые сегменты TCP в одном направлении и 60-байтовые TCPackв другом (два источника трассировки узлов были сопоставлены этим источником трассировки).

Как это было настроено? Должны быть предоставлены несколько объявлений. Во-первых, объектGnuplotHelperдолжен быть объявлен и настроен:
+ // Use GnuplotHelper to plot the packet byte count over time+ GnuplotHelper plotHelper;++ // Configure the plot. The first argument is the file name prefix+ // for the output files generated. The second, third, and fourth+ // arguments are, respectively, the plot title, x-axis, and y-axis labels+ plotHelper.ConfigurePlot ("seventh-packet-byte-count",+ "Packet Byte Count vs. Time",+ "Time (Seconds)",+ "Packet Byte Count");

К этому моменту пустой график был настроен. Префикс имени файла первый аргумент, заголовок графика второй, подпись к оси X третий, а название оси Y четвертый аргумент.
Следующим шагом является настройка данных, вот где перехватывается источник трассировки. Во-первых, обратите внимание выше в программе мы объявили для дальнейшего использования несколько переменных:
+ std::string probeType;+ std::string tracePath;+ probeType = "ns3::Ipv6PacketProbe";+ tracePath = "/NodeList/*/$ns3::Ipv6L3Protocol/Tx";

Мы используем их здесь:
+ // Specify the probe type, trace source path (in configuration namespace), and+ // probe output trace source ("OutputBytes") to plot. The fourth argument+ // specifies the name of the data series label on the plot. The last+ // argument formats the plot by specifying where the key should be placed.+ plotHelper.PlotProbe (probeType,+ tracePath,+ "OutputBytes",+ "Packet Byte Count",+ GnuplotAggregator::KEY_BELOW);

Первые два аргумента это имя типа захвата и путь источника трассировки. Эти двое, вероятно, труднее всего определить, когда вы пытаетесь использовать эту структуру для построения других трасс. Здесь трасса захвата это трасса Tx источника классаIpv6L3Protocol. Когда мы исследуем реализацию этого класса (src/internet/model/ipv6-l3-protocol.cc) мы можем видеть:
.AddTraceSource ("Tx", "Send IPv6 packet to outgoing interface.",MakeTraceSourceAccessor (&Ipv6L3Protocol::m_txTrace))

Это говорит о том, что Tx это имя переменнойm_txTrace, которая имеет объявление:
/*** \brief Callback to trace TX (transmission) packets.*/TracedCallback<Ptr<const Packet>, Ptr<Ipv6>, uint32_t> m_txTrace;

Оказывается, что эта специфическая сигнатура источника трассировки поддерживается классомProbe(что нам и нужно) классаIpv6PacketProbe. Смотрите файлыsrc/internet/model/ipv6-packet-probe.{h, cc}.

Итак, в приведенном выше оператореPlotProbeмы видим, что он перехватывает источник трассировки (идентифицируемый строкой пути) с соответствующим типом захватаns3Ipv6PacketProbe. Если бы мы не поддерживали такой тип захвата (соответствие трассировки сигнатуре источника), мы не могли бы использовать это объявление (хотя могли бы быть использованы некоторые более сложные объявления более низкого уровня, как описано в руководстве).

Ipv6PacketProbeсам экспортирует некоторые источники трассировки, которые извлекают данные из исследуемого объектаPacket:
Ipv6PacketProbe::GetTypeId (){  static TypeId tid = TypeId ("ns3::Ipv6PacketProbe")  .SetParent<Probe> ()  .SetGroupName ("Stats")  .AddConstructor<Ipv6PacketProbe> ()  .AddTraceSource ( "Output",  "The packet plus its IPv6 object and interface that serve as the output for this probe",  MakeTraceSourceAccessor (&Ipv6PacketProbe::m_output))  .AddTraceSource ( "OutputBytes",  "The number of bytes in the packet",  MakeTraceSourceAccessor (&Ipv6PacketProbe::m_outputBytes));  return tid;}

Третий аргумент нашего оператораPlotProbeуказывает, что нас интересует количество байтов в этом пакете;

в частности, источник трассировки OutputBytesIpv6PacketProbe. Наконец, два последних аргумента объявления предоставляют условные обозначения для этой серии данных (Число байтов пакета) и необязательный оператор форматированияgnuplot(GnuplotAggregator :: KEY_BELOW), которым мы показываем, что хотим, чтобы легенда графика была вставлена ниже графика. Другие варианты включают NO_KEY, KEY_INSIDE и KEY_ABOVE.

8.4Поддерживаемые
типы трасс


На момент написания этой главы вProbesподдерживаются следующие трассируемые значения:

ТипTracedValue ТипProbe Файл
double DoubleProbe stats/model/double-probe.h
uint8_t Uinteger8Probe stats/model/uinteger-8-probe.h
uint16_t Uinteger16Probe stats/model/uinteger-16-probe.h
uint32_t Uinteger32Probe stats/model/uinteger-32-probe.h
bool BooleanProbe stats/model/uinteger-16-probe.h
ns3::Time TimeProbe stats/model/time-probe.h

и поддерживает следующие типыTraceSource:

Тип TracedSource Тип Probe Вывод Probe Файл
Ptr&ltconst Packet&gt PacketProbe байты network/utils/packet-probe.h
Ptr&lt const Packet&gt, Ptr&lt Ipv4&gt, uint32_t Ipv4PacketProbe байты internet/model/ipv4-packetprobe.h
Ptr&ltconst Packet&gt, Ptr&ltIpv6&gt, uint32_t Ipv6PacketProbe байты internet/model/ipv6-packetprobe.h
Ptr&ltconst Packet&gt, Ptr&ltIpv6&gt, uint32_t Ipv6PacketProbe байты internet/model/ipv6-packetprobe.h
Ptr&ltconst Packet&gt, const Address& ApplicationPacketProbe байты applications/model/applicationpacket-probe.h

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

8.5FileHelper


Класс FileHelper это всего лишь вариант предыдущего примераGnuplotHelper. Пример программы обеспечивает форматированный вывод тех же данных с метками времени, например:
Time (Seconds) = 9.312e+00 Packet Byte Count = 596Time (Seconds) = 9.312e+00 Packet Byte Count = 564

Предоставляются два файла, один для узла 0 и один для узла 1, как видно из имен файлов. Давайте посмотрим на код кусок за куском:
+ // Use FileHelper to write out the packet byte count over time+ FileHelper fileHelper;++ // Configure the file to be written, and the formatting of output data.+ fileHelper.ConfigureFile ("seventh-packet-byte-count",+ FileAggregator::FORMATTED);

Префикс вспомогательного файла является первым аргументом, а спецификатор формата следующим. Другие варианты форматирования включают SPACE_SEPARATED, COMMA_SEPARATED и TAB_SEPARATED. Пользователи могут изменить форматирование (если FORMATTED указывается) такой строкой формата:
++ // Set the labels for this formatted output file.+ fileHelper.Set2dFormat ("Time (Seconds) = %.3e\tPacket Byte Count = %.0f");

Наконец, должен быть подключен интересующий источник трассировки. Опять же, в этом примере используется переменныеprobeTypeиtracePath, подключается захват выхода источника трассировки OutputBytes:
++ // Specify the probe type, trace source path (in configuration namespace), and+ // probe output trace source ("OutputBytes") to write.+ fileHelper.WriteProbe (probeType,+ tracePath,+ "OutputBytes");+

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

8.6Итоги


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

Глава 9Заключение


9.1На будущее


Этот документ предназначен для использования в качестве живого документа. Мы надеемся и ожидаем, что со временем он будет расти, чтобы охватить все больше и больше гаек и болтовns3. Написание руководств и учебных пособий это не то, что нас всех вдохновляет, но это очень важно для проекта. Если вы являетесь экспертом в одной из этих областей, рассмотрите возможность внести свой вклад вns3, в соответствующей главе или же любую другую главу, которую вы считаете важной.

9.2Завершение


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

  • Руководство поns3
  • Документация библиотеки моделейns3
  • ns3 Doxygen(документация API)
  • ns3вики


Команда разработчиков ns3.
Подробнее..

Из песочницы Использование GitHub Actions с C и CMake

24.06.2020 14:16:49 | Автор: admin

Привет, Хабр! Предлагаю вашему вниманию перевод статьи "Using GitHub Actions with C++ and CMake" о сборке проекта на C++ с использованием GitHub Actions и CMake автора Кристиана Адама.


Использование GitHub Actions с C++ и CMake


В этом посте я хочу показать файл конфигурации GitHub Actions для проекта C++, использующего CMake.


GitHub Actions это предоставляемая GitHub инфраструктура CI/CD. Сейчас GitHub Actions предлагает следующие виртуальные машины (runners):


Виртуальное окружение Имя рабочего процесса YAML
Windows Server 2019 windows-latest
Ubuntu 18.04 ubuntu-latest or ubuntu-18.04
Ubuntu 16.04 ubuntu-16.04
macOS Catalina 10.15 macos-latest

Каждая виртуальная машина имеет одинаковые доступные аппаратные ресурсы:


  • 2х ядерное CPU
  • 7 Гб оперативной памяти
  • 14 Гб на диске SSD

Каждое задание рабочего процесса может выполняться до 6 часов.


К сожалению, когда я включил GitHub Actions в проекте C++, мне предложили такой рабочий процесс:


./configuremakemake checkmake distcheck

Это немного не то, что можно использовать с CMake.


Hello World


Я хочу собрать традиционное тестовое приложение C++:


#include <iostream>int main(){  std::cout << "Hello world\n";}

Со следующим проектом CMake:


cmake_minimum_required(VERSION 3.16)project(main)add_executable(main main.cpp)install(TARGETS main)enable_testing()add_test(NAME main COMMAND main)

TL;DR смотрите проект на GitHub.


Матрица сборки


Я начал со следующей матрицы сборки:


name: CMake Build Matrixon: [push]jobs:  build:    name: ${{ matrix.config.name }}    runs-on: ${{ matrix.config.os }}    strategy:      fail-fast: false      matrix:        config:        - {            name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",            os: windows-latest,            build_type: "Release", cc: "cl", cxx: "cl",            environment_script: "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/VC/Auxiliary/Build/vcvars64.bat"          }        - {            name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",            os: windows-latest,            build_type: "Release", cc: "gcc", cxx: "g++"          }        - {            name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",            os: ubuntu-latest,            build_type: "Release", cc: "gcc", cxx: "g++"          }        - {            name: "macOS Latest Clang", artifact: "macOS.tar.xz",            os: macos-latest,            build_type: "Release", cc: "clang", cxx: "clang++"          }

Свежие CMake и Ninja


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


Виртуальное окружение Версия CMake
Windows Server 2019 3.16.0
Ubuntu 18.04 3.12.4
macOS Catalina 10.15 3.15.5

Это значит, что нужно будет ограничить минимальную версию CMake до 3.12 или обновить CMake.


CMake 3.16 поддерживает прекомпиляцию заголовков и Unity Builds, которые помогают сократить время сборки.


Поскольку у CMake и Ninja есть репозитории на GitHub, я решил скачать нужные релизы с GitHub.


Для написания скрипта я использовал CMake, потому что виртуальные машины по умолчанию используют свойственный им язык скриптов (bash для Linux и powershell для Windows). CMake умеет выполнять процессы, загружать файлы, извлекать архивы и делать еще много полезных вещей.


- name: Download Ninja and CMake  id: cmake_and_ninja  shell: cmake -P {0}  run: |    set(ninja_version "1.9.0")    set(cmake_version "3.16.2")    message(STATUS "Using host CMake version: ${CMAKE_VERSION}")    if ("${{ runner.os }}" STREQUAL "Windows")      set(ninja_suffix "win.zip")      set(cmake_suffix "win64-x64.zip")      set(cmake_dir "cmake-${cmake_version}-win64-x64/bin")    elseif ("${{ runner.os }}" STREQUAL "Linux")      set(ninja_suffix "linux.zip")      set(cmake_suffix "Linux-x86_64.tar.gz")      set(cmake_dir "cmake-${cmake_version}-Linux-x86_64/bin")    elseif ("${{ runner.os }}" STREQUAL "macOS")      set(ninja_suffix "mac.zip")      set(cmake_suffix "Darwin-x86_64.tar.gz")      set(cmake_dir "cmake-${cmake_version}-Darwin-x86_64/CMake.app/Contents/bin")    endif()    set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")    file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)    set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")    file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)    execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)    # Save the path for other steps    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)    message("::set-output name=cmake_dir::${cmake_dir}")    if (NOT "${{ runner.os }}" STREQUAL "Windows")      execute_process(        COMMAND chmod +x ninja        COMMAND chmod +x ${cmake_dir}/cmake      )    endif()

Шаг настройки


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


- name: Configure  shell: cmake -P {0}  run: |    set(ENV{CC} ${{ matrix.config.cc }})    set(ENV{CXX} ${{ matrix.config.cxx }})    if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")      execute_process(        COMMAND "${{ matrix.config.environment_script }}" && set        OUTPUT_FILE environment_script_output.txt      )      file(STRINGS environment_script_output.txt output_lines)      foreach(line IN LISTS output_lines)        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")          set(ENV{${CMAKE_MATCH_1}} "${CMAKE_MATCH_2}")        endif()      endforeach()    endif()    file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/ninja" ninja_program)    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake        -S .        -B build        -D CMAKE_BUILD_TYPE=${{ matrix.config.build_type }}        -G Ninja        -D CMAKE_MAKE_PROGRAM=${ninja_program}      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Bad exit status")    endif()

Я установил переменные окружения CC и CXX, а для MSVC мне пришлось выполнить скрипт vcvars64.bat, получить все переменные окружения и установить их для выполняющегося скрипта CMake.


Шаг сборки


Шаг сборки включает в себя запуск CMake с параметром --build:


- name: Build  shell: cmake -P {0}  run: |    set(ENV{NINJA_STATUS} "[%f/%t %o/sec] ")    if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")      file(STRINGS environment_script_output.txt output_lines)      foreach(line IN LISTS output_lines)        if (line MATCHES "^([a-zA-Z0-9_-]+)=(.*)$")          set(ENV{${CMAKE_MATCH_1}} "${CMAKE_MATCH_2}")        endif()      endforeach()    endif()    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake --build build      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Bad exit status")    endif()

Что бы увидеть скорость компиляции на разном виртуальном окружении, я установил переменную NINJA_STATUS.


Для переменных MSVC я использовал скрипт environment_script_output.txt, полученный на шаге настройки.


Шаг запуска тестов


На этом шаге вызывается ctest с передачей числа ядер процессора через аргумент -j:


- name: Run tests  shell: cmake -P {0}  run: |    include(ProcessorCount)    ProcessorCount(N)    execute_process(      COMMAND ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/ctest -j ${N}      WORKING_DIRECTORY build      RESULT_VARIABLE result    )    if (NOT result EQUAL 0)      message(FATAL_ERROR "Running tests failed!")    endif()

Шаги установки, упаковки и загрузки


Эти шаги включают запуск CMake с --install, последующий вызов CMake для создания архива tar.xz и загрузку архива как артефакта сборки.


- name: Install Strip  run: ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake --install build --prefix instdir --strip- name: Pack  working-directory: instdir  run: ${{ steps.cmake_and_ninja.outputs.cmake_dir }}/cmake -E tar cJfv ../${{ matrix.config.artifact }} .- name: Upload  uses: actions/upload-artifact@v1  with:    path: ./${{ matrix.config.artifact }}    name: ${{ matrix.config.artifact }}

Я не стал использовать CMake в качестве языка сценариев для простых вызовов CMake с параметрами, оболочки по умолчанию прекрасно с этим справляются.


Обработка релизов


Когда вы помечаете релиз в git, вы также хотите, чтобы артефакты сборки прикрепились к релизу:


git tag -a v1.0.0 -m "Release v1.0.0"git push origin v1.0.0

Ниже приведён код для этого, который сработает, если git refpath содержит tags/v:


release:  if: contains(github.ref, 'tags/v')  runs-on: ubuntu-latest  needs: build  steps:  - name: Create Release    id: create_release    uses: actions/create-release@v1.0.0    env:      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}    with:      tag_name: ${{ github.ref }}      release_name: Release ${{ github.ref }}      draft: false      prerelease: false  - name: Store Release url    run: |      echo "${{ steps.create_release.outputs.upload_url }}" > ./upload_url  - uses: actions/upload-artifact@v1    with:      path: ./upload_url      name: upload_urlpublish:  if: contains(github.ref, 'tags/v')  name: ${{ matrix.config.name }}  runs-on: ${{ matrix.config.os }}  strategy:    fail-fast: false    matrix:      config:      - {          name: "Windows Latest MSVC", artifact: "Windows-MSVC.tar.xz",          os: ubuntu-latest        }      - {          name: "Windows Latest MinGW", artifact: "Windows-MinGW.tar.xz",          os: ubuntu-latest        }      - {          name: "Ubuntu Latest GCC", artifact: "Linux.tar.xz",          os: ubuntu-latest        }      - {          name: "macOS Latest Clang", artifact: "macOS.tar.xz",          os: ubuntu-latest        }  needs: release  steps:  - name: Download artifact    uses: actions/download-artifact@v1    with:      name: ${{ matrix.config.artifact }}      path: ./  - name: Download URL    uses: actions/download-artifact@v1    with:      name: upload_url      path: ./  - id: set_upload_url    run: |      upload_url=`cat ./upload_url`      echo ::set-output name=upload_url::$upload_url  - name: Upload to Release    id: upload_to_release    uses: actions/upload-release-asset@v1.0.1    env:      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}    with:      upload_url: ${{ steps.set_upload_url.outputs.upload_url }}      asset_path: ./${{ matrix.config.artifact }}      asset_name: ${{ matrix.config.artifact }}      asset_content_type: application/x-gtar

Это выглядит сложным, но это необходимо, так как actions/create-release можно вызвать однократно, иначе это действие закончится ошибкой. Это обсуждается в issue #14 и issue #27.


Несмотря на то, что вы можете использовать рабочий процесс до 6 часов, токен secrets.GITHUB_TOKEN действителен один час. Вы можете создать личный токен или загрузить артефакты в релиз вручную. Подробности в обсуждении сообщества GitHub.


Заключение


Включить GitHub Actions в вашем проекте на CMake становится проще, если создать файл .github/workflows/build_cmake.yml с содержимым из build_cmake.yml.


Вы можете посмотреть GitHub Actions в моем проекте Hello World GitHub.


Оригинальный текст опубликован под лицензией CC BY 4.0.

Подробнее..

Учебник по симулятору сети ns-3 теперь одним pdf-файлом

25.06.2020 16:23:50 | Автор: admin


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

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

Как первокурсники Питерской Вышки за семестр написали торрент-клиент, анализатор кода, фоторедактор и не только

26.06.2020 14:09:33 | Автор: admin
Учиться программированию, изучая только теорию, это то же самое, что учиться играть на рояле, слушая лекции об игре на рояле. Первокурсники Прикладной математики и информатики в Питерской Вышке начинают изучать C++ в первом семестре. В дополнение к домашним работам с февраля они пишут на С++ семестровые командные проекты. Тему ребята придумывают самостоятельно, от игр и игровых движков до собственного анализатора кода.

Под катом подробности внутренней кухни: рассказ о том, как была устроена работа над проектами.



Кратко: каждой команде назначили менторов из числа действующих разработчиков или студентов старших курсов с опытом работы в IT-компаниях. Еженедельно первокурсники рассказывали о прогрессе и сложностях, с которыми пришлось столкнуться, в анкете и во время созвона с ментором. Чтобы команды потренировались выступать публично (а заодно чтобы установить дедлайны, к которым нужно готовиться), мы организовали две предзащиты. На финальной защите все команды успешно представили проекты, средний балл студентов 9,0 по 10-балльной шкале.

Теперь подробнее:

Об организаторах


Мы Наташа Мурашкина, Саша Орлова и Оля Лупуляк студентки старших курсов Прикладной математики и информатики в Питерской Вышке. Мы занимались организацией проектов под руководством куратора направления и лектора курса по С++ Егора Суворова.

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

О проектах


Цель проектов на первом курсе дать студентам попробовать силы в промышленном программировании, а также показать особенности длительной работы над проектом и разработки в командах.

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


Демо-видео проекта Fivaproljo (мультиплеерный платформер)

К концу семестра необходимо было получить хотя бы MVP проекта, но многие команды успели добавить больше функциональностей, а кто-то даже написать тесты и оформить страницу на GitHub с описанием и инструкцией по запуску. Примеры: Моделирование и визуализация динамических систем, Анализатор кода UB-tester tool, Компьютерная версия игры Колонизаторы.

Подготовка


В начале семестра студенты разделились на 21 команду по 3 человека. Каждая команда самостоятельно выбрала тему проекта и согласовала ее с организаторами, после чего получила контакты своего ментора.

Ментор это сотрудник IT-компании или студент старших курсов с опытом промышленной разработки. Менторы помогали с распределением задач и решением технических проблем. Они еженедельно созванивались со студентами, чтобы обсудить прогресс и составить план работ на следующую неделю. В число менторов вошли стажеры и сотрудники JetBrains, Яндекса, ВКонтакте, Huawei, Google, Delightex, VeeRoute и других компаний, преподаватели Летней компьютерной школы (ЛКШ), а также студенты нашего факультета, московского кампуса Вышки и кафедры КТ университета ИТМО.

В качестве эксперимента мы даже пригласили мета-ментора (ментора для ментора): опытный сотрудник Google наставлял нашего старшекурсника в менторстве команды.

Работа в течение семестра


Каждую неделю студентам нужно было созвониться с ментором и заполнить анкету о проделанной работе.

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

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

Воркшопы


За семестр студенты дважды тренировались выступать перед публикой и отвечать на вопросы во время воркшопов. Каждая команда представляла работу за 7 минут, показывала короткое демо-видео и еще 7 минут отвечала на вопросы аудитории.

Идея проводить репетиции не нова, и в нашем случае мы вдохновлялись воркшопами по публичным выступлениям, в которых Наташа участвовала во время стажировки в Google.

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

Структура презентации для воркшопов


  • Титульный слайд
  • Введение в область
  • Краткое описание проекта
  • Сравнение с аналогами
  • Сравнение технологий
  • Разбиение на подзадачи для каждого участника (по слайду на участника)
    • Описание подзадачи, решение, выводы
  • Описание текущего состояния проекта (что было сделано с начала)
  • Описание прогресса с предыдущей презентации
  • Планы до конца разработки
  • Можно сделать демо-видео не длиннее 30 секунд

Кстати, в блоге Питерской Вышки на Хабре уже есть инструкция по подготовке коротких докладов от декана нашего факультета Александра Владимировича Омельченко.


Демо-видео проекта Моделирование и визуализация динамических систем

Как оценивали вклад каждого участника


Здесь нам помогали коммиты студентов на GitHub и анкеты менторов.

К первому воркшопу и к защитам мы попросили студентов залить код проекта на GitHub. Количество строчек кода это, конечно, нечестная метрика, и нельзя судить только по ней, но ее мы использовали как первый фильтр. После вчитывались в содержание.

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

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

  • 30% работа в течение семестра. Для хорошей оценки нужно вовремя сдавать анкеты.
  • 35% оценка ментора. Для хорошей оценки нужно, чтобы ментор оценил прогресс.
  • 35% оценка комиссии на защите. Для хорошей оценки нужно, чтобы в итоге что-то получилось.


Итоги


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


Скриншот с защиты проекта TorrentX

Мы довольны результатами ребят. Трое студентов (из 61) получили оценку удовлетворительно, все остальные хорошо и отлично. Средний балл 9,0 по 10-балльной шкале.

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

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

Менторы, судя по обратной связи, получили удовольствие от работы.

Как и мы :) Так что в следующем году планируем продолжить.

Планы на будущее


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

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

  • Улучшить матчинг команда ментор, возможно, ввести собеседования между командой и ментором и маленькие тестовые задания. Учитывать пожелания менторов руководить более подготовленной (требует больше знаний) или менее опытной но не менее старательной! командой (требует лучших управленческих навыков).

  • Улучшить качество обратной связи на воркшопах. Планируем вовлечь менторов и создать список блиц-вопросов по содержанию, которые мы со студентами сможем задавать выступающей команде.

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

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

И еще +100 заметок, которые мы бережно сохраняли в течение семестра.

Если вы хотели бы на волонтёрских основах побыть ментором студентов-первокурсников в следующем году, заполните форму или напишите нам в Телеграме (@murfel). Будем рады!
Подробнее..

IDA Pro работа с библиотечным кодом (не WinAPI)

01.07.2020 20:13:32 | Автор: admin

Всем привет,



При работе в IDA мне, да и, наверняка, вам тоже, часто приходится иметь дело с приложениями, которые имеют достаточно большой объём кода, не имеют символьной информации и, к тому же, содержат много библиотечного кода. Зачастую, такой код нужно уметь отличать от написанного пользователем. И, если на вход библиотечного кода подаются только int, void *, да const char *, можно отделаться одними лишь сигнатурами (созданные с помощью FLAIR-утилит sig-файлы). Но, если нужны структуры, аргументы, их количество, тут без дополнительной магии не обойдёшься В качестве примера я буду работать с игрой для Sony Playstation 1, написанной с использованием PSYQ v4.7.


Дополнительная магия


Представим ситуацию: вам попалась прошивка от какой-нибудь железки. Обычный Bare-metal ROM (можно даже с RTOS). Или же ROM игры. В подобных случаях, скорее всего, при компиляции использовался какой-то SDK/DDK, у которого имеется набор LIB/H/OBJ файлов, которые вклеиваются линкером в итоговый файл.


Наш план действий будет примерно таким:


  1. Взять все lib/obj-файлы, и создать из них сигнатуры (или набор сигнатур). Это поможет нам отделить статически влинкованный библиотечный код.
  2. Взять все h-файлы и создать из них библиотеку типов. Эта библиотека хранит не только типы данных, но и информацию об именах и типах аргументов функций, в которых объявленные типы используются.
  3. Применить сигнатуры, чтобы у нас определились библиотечные функции и их имена.
  4. Применить библиотеки типов, чтобы применить прототипы функций и используемые типы данных.

Создаём sig-файлы


Для создания файла сигнатур необходимо воспользоваться набором FLAIR-утилит, доступных лицензионным пользователям IDA. Список необходимых утилит следующий:


  • pcf LIB/OBJ-parser, создаёт PAT-файл из COFF-объектных файлов
  • pelf LIB/OBJ-парсер, создаёт PAT-файл из ELF-файлов (Unix)
  • plb LIB/OBJ-parser, создаёт PAT-файл из OMF-объектных файлов
  • pmacho MACH-O-парсер, создаёт PAT-файл из MACH-O-файлов (MacOS)
  • ppsx OBJ-парсер, создаёт PAT-файл из библиотечных файлов PSYQ
  • ptmobj OBJ-парсер, создаёт PAT-файл из библиотечных файлов Trimedia
  • sigmake конвертирует ранее созданный PAT-файл в SIG-файл, перевариваемый IDA

В моём случае это ppsx. Собираю bat-файл, в котором перечисляю все lib- и obj-файлы, и добавляю к каждой строке вызов ppsx, чтобы получилось формирование итогового PAT-файла. Получилось следующее содержимое:


run_47.bat
@echo offppsx -a 2MBYTE.OBJ psyq47.patppsx -a 8MBYTE.OBJ psyq47.patppsx -a LIBAPI.LIB psyq47.patppsx -a LIBC.LIB psyq47.patppsx -a LIBC2.LIB psyq47.patppsx -a LIBCARD.LIB psyq47.patppsx -a LIBCD.LIB psyq47.patppsx -a LIBCOMB.LIB psyq47.patppsx -a LIBDS.LIB psyq47.patppsx -a LIBETC.LIB psyq47.patppsx -a LIBGPU.LIB psyq47.patppsx -a LIBGS.LIB psyq47.patppsx -a LIBGTE.LIB psyq47.patppsx -a LIBGUN.LIB psyq47.patppsx -a LIBHMD.LIB psyq47.patppsx -a LIBMATH.LIB psyq47.patppsx -a LIBMCRD.LIB psyq47.patppsx -a LIBMCX.LIB psyq47.patppsx -a LIBPAD.LIB psyq47.patppsx -a LIBPRESS.LIB psyq47.patppsx -a LIBSIO.LIB psyq47.patppsx -a ashldi3.obj psyq47.patppsx -a ashrdi3.obj psyq47.patppsx -a CACHE.OBJ psyq47.patppsx -a clear_cache.obj psyq47.patppsx -a CLOSE.OBJ psyq47.patppsx -a cmpdi2.obj psyq47.patppsx -a CREAT.OBJ psyq47.patppsx -a ctors.obj psyq47.patppsx -a divdi3.obj psyq47.patppsx -a dummy.obj psyq47.patppsx -a eh.obj psyq47.patppsx -a eh_compat.obj psyq47.patppsx -a exit.obj psyq47.patppsx -a ffsdi2.obj psyq47.patppsx -a fixdfdi.obj psyq47.patppsx -a fixsfdi.obj psyq47.patppsx -a fixtfdi.obj psyq47.patppsx -a fixunsdfdi.obj psyq47.patppsx -a fixunsdfsi.obj psyq47.patppsx -a fixunssfdi.obj psyq47.patppsx -a fixunssfsi.obj psyq47.patppsx -a fixunstfdi.obj psyq47.patppsx -a fixunsxfdi.obj psyq47.patppsx -a fixunsxfsi.obj psyq47.patppsx -a fixxfdi.obj psyq47.patppsx -a floatdidf.obj psyq47.patppsx -a floatdisf.obj psyq47.patppsx -a floatditf.obj psyq47.patppsx -a floatdixf.obj psyq47.patppsx -a FSINIT.OBJ psyq47.patppsx -a gcc_bcmp.obj psyq47.patppsx -a LSEEK.OBJ psyq47.patppsx -a lshrdi3.obj psyq47.patppsx -a moddi3.obj psyq47.patppsx -a muldi3.obj psyq47.patppsx -a negdi2.obj psyq47.patppsx -a new_handler.obj psyq47.patppsx -a op_delete.obj psyq47.patppsx -a op_new.obj psyq47.patppsx -a op_vdel.obj psyq47.patppsx -a op_vnew.obj psyq47.patppsx -a OPEN.OBJ psyq47.patppsx -a PROFILE.OBJ psyq47.patppsx -a pure.obj psyq47.patppsx -a read.obj psyq47.patppsx -a shtab.obj psyq47.patppsx -a snctors.obj psyq47.patppsx -a SNDEF.OBJ psyq47.patppsx -a SNMAIN.OBJ psyq47.patppsx -a SNREAD.OBJ psyq47.patppsx -a SNWRITE.OBJ psyq47.patppsx -a trampoline.obj psyq47.patppsx -a ucmpdi2.obj psyq47.patppsx -a udiv_w_sdiv.obj psyq47.patppsx -a udivdi3.obj psyq47.patppsx -a udivmoddi4.obj psyq47.patppsx -a umoddi3.obj psyq47.patppsx -a varargs.obj psyq47.patppsx -a write.obj psyq47.patppsx -a LIBSND.LIB psyq47.patppsx -a LIBSPU.LIB psyq47.patppsx -a LIBTAP.LIB psyq47.patppsx -a LOW.OBJ psyq47.patppsx -a MCGUI.OBJ psyq47.patppsx -a MCGUI_E.OBJ psyq47.patppsx -a NOHEAP.OBJ psyq47.patppsx -a NONE3.OBJ psyq47.patppsx -a NOPRINT.OBJ psyq47.patppsx -a POWERON.OBJ psyq47.pat

LIBSN.LIB файл имеет формат, отличный от остальных библиотек, поэтому пришлось разложить его на OBJ-файлы утилитой PSYLIB2.EXE, которая входит в комплект PSYQ. Запускаем run_47.bat. Получаем следующий выхлоп:


run_47.bat output
2MBYTE.OBJ: skipped 0, total 18MBYTE.OBJ: skipped 0, total 1LIBAPI.LIB: skipped 0, total 89LIBC.LIB: skipped 0, total 55LIBC2.LIB: skipped 0, total 50LIBCARD.LIB: skipped 0, total 18LIBCD.LIB: skipped 0, total 51LIBCOMB.LIB: skipped 0, total 3LIBDS.LIB: skipped 0, total 36LIBETC.LIB: skipped 0, total 8LIBGPU.LIB: skipped 0, total 60LIBGS.LIB: skipped 0, total 167LIBGTE.LIB: skipped 0, total 535LIBGUN.LIB: skipped 0, total 2LIBHMD.LIB: skipped 0, total 585LIBMATH.LIB: skipped 0, total 59LIBMCRD.LIB: skipped 0, total 7LIBMCX.LIB: skipped 0, total 31LIBPAD.LIB: skipped 0, total 21LIBPRESS.LIB: skipped 0, total 7LIBSIO.LIB: skipped 0, total 4ashldi3.obj: skipped 0, total 1ashrdi3.obj: skipped 0, total 1CACHE.OBJ: skipped 0, total 1clear_cache.obj: skipped 0, total 1CLOSE.OBJ: skipped 0, total 1cmpdi2.obj: skipped 0, total 1CREAT.OBJ: skipped 0, total 1ctors.obj: skipped 0, total 0divdi3.obj: skipped 0, total 1dummy.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000022Deh_compat.obj: skipped 0, total 1exit.obj: skipped 0, total 1ffsdi2.obj: skipped 0, total 1fixdfdi.obj: skipped 0, total 1fixsfdi.obj: skipped 0, total 1fixtfdi.obj: skipped 0, total 0fixunsdfdi.obj: skipped 0, total 1fixunsdfsi.obj: skipped 0, total 1fixunssfdi.obj: skipped 0, total 1fixunssfsi.obj: skipped 0, total 1fixunstfdi.obj: skipped 0, total 0fixunsxfdi.obj: skipped 0, total 0fixunsxfsi.obj: skipped 0, total 0fixxfdi.obj: skipped 0, total 0floatdidf.obj: skipped 0, total 1floatdisf.obj: skipped 0, total 1floatditf.obj: skipped 0, total 0floatdixf.obj: skipped 0, total 0FSINIT.OBJ: skipped 0, total 1gcc_bcmp.obj: skipped 0, total 1LSEEK.OBJ: skipped 0, total 1lshrdi3.obj: skipped 0, total 1moddi3.obj: skipped 0, total 1muldi3.obj: skipped 0, total 1negdi2.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000013Dop_delete.obj: skipped 0, total 1op_new.obj: skipped 0, total 1op_vdel.obj: skipped 0, total 1op_vnew.obj: skipped 0, total 1OPEN.OBJ: skipped 0, total 1PROFILE.OBJ: skipped 0, total 1pure.obj: skipped 0, total 1Fatal: Unknown record type 60 at 0000015Fshtab.obj: skipped 0, total 0Fatal: Unknown record type 60 at 000000EESNDEF.OBJ: skipped 0, total 0SNMAIN.OBJ: skipped 0, total 1SNREAD.OBJ: skipped 0, total 1SNWRITE.OBJ: skipped 0, total 1trampoline.obj: skipped 0, total 0ucmpdi2.obj: skipped 0, total 1udiv_w_sdiv.obj: skipped 0, total 1udivdi3.obj: skipped 0, total 1udivmoddi4.obj: skipped 0, total 1umoddi3.obj: skipped 0, total 1varargs.obj: skipped 0, total 1Fatal: Unknown record type 60 at 00000160LIBSND.LIB: skipped 0, total 223LIBSPU.LIB: skipped 0, total 126LIBTAP.LIB: skipped 0, total 1LOW.OBJ: skipped 0, total 1Fatal: can't find symbol F003MCGUI_E.OBJ: skipped 0, total 1NOHEAP.OBJ: skipped 0, total 1NONE3.OBJ: skipped 0, total 1NOPRINT.OBJ: skipped 0, total 1POWERON.OBJ: skipped 0, total 1

Видим некоторое количество ошибок парсинга, но, в тех файлах всего 1 сигнатура (total 1), поэтому, думаю, что это не критично. Далее преобразовываем PAT-файл в SIG-файл:


sigmake -n"PsyQ v4.7" psyq47.pat psyq47.sigpsyq47.sig: modules/leaves: 1345/2177, COLLISIONS: 21See the documentation to learn how to resolve collisions.

В итоге получаем следующий список файлов:


  • psyq47.err его не трогаем
  • psyq47.exc его нужно будет отредактировать
  • psyq47.pat его тоже не трогаем

Открываем на редактирование .exc-файл. Видим:


;--------- (delete these lines to allow sigmake to read this file); add '+' at the start of a line to select a module; add '-' if you are not sure about the selection; do nothing if you want to exclude all modules

Если удалить ---------, всё, что содержится в файле ниже, будет учитываться. Давайте взглянем на то, что там есть. Вот пример:


CdPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300DsPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300

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


В итоге, если всё сделано правильно, получаем SIG-файл. Его нужно положить в соответствующую папку в каталоге установка IDA.


Создаём til-файлы


Эти файлы нужны для хранения информации о типах, об аргументах функций и т.п. По-умолчанию. Создаются они с помощью утилиты tilib, которую необходимо положить в каталог с Идой (ей, почему-то, нужен ida.hlp файл).


Данной утилите нужно скормить include-файлы вашего SDK/DDK. При том парсинг этой утилитой отличается от такового средством "Parse C header file..." в самой IDA. Вот описание из readme:


Its functionality overlaps with "Parse C header file..." from IDA Pro.
However, this utility is easier to use and provides more control
over the output. Also, it can handle the preprocessor symbols, while
the built-in command ignores them.

У этой утилиты есть один нюанс: она по умолчанию использует режим, когда символы заманглены, либо имеют стоящее в начале имени нижнее подчёркивание. В случае работы со статически влинкованным кодом этот режим нужно выключить флагом -Gn.


По-умолчанию, данная утилита принимает на вход только один include-файл. Если же файлов много, нужно соорудить include-файл следующего содержания:


#include "header1.h"#include "header2.h"#include "header3.h"// ...

Этот файл передаётся с помощью флага -hFileName.h. Далее, передаём путь поиска остальных header-файлов и получаем следующую командую строку:


tilib -c -Gn -I. -hpsyq47.h psyq47.til

На выходе получаем til-файл, пригодный для использования. Кладём его в соответствующий каталог IDA: sig\mips.


Проверяем результат


Закидываем ROM-файл в IDA, дожидаемся окончания анализа. Далее, необходимо указать компилятор. Для этого заходим в Options->Compiler:



Теперь просто меняем Unknown на GNU C++ (в случае PSX). Остальное оставляем как есть:



Теперь жмём Shift+F5 (либо меню View->Open subviews->Signatures), жмём Insert и выбираем нужный файл сигнатур:



Жмём OK и ждём, пока применяются сигнатуры (у меня получилось 482 распознанных функции).



Далее необходимо применить библиотеку типов (til-файл). Для этого жмём Shift+F11 (либо View->Open subviews->Type libraries) и понимаем, что IDA не может определить компилятор (не смотря на то, что мы его уже указали):



Но это нам всё равно не помешает выбрать til-файл (всё так же, через Insert):



Получаем то, что так хотели:



Теперь декомпилятор успешно подхватывает информацию о типах, и выхлоп становится куда лучше:



P.S.


Надеюсь, эта информация окажется для вас полезной. Удачного реверс-инжиниринга!

Подробнее..

Перевод О нет! Моя Data Science ржавеет

04.07.2020 14:10:42 | Автор: admin
Привет, Хабр!

Предлагаем вашему вниманию перевод интереснейшего исследования от компании Crowdstrike. Материал посвящен использованию языка Rust в области Data Science (применительно к malware analysis) и демонстрирует, в чем Rust на таком поле может посоперничать даже с NumPy и SciPy, не говоря уж о чистом Python.


Приятного чтения!

Python один из самых популярных языков программирования для работы с data science, и неслучайно. В индексе пакетов Python (PyPI) найдется огромное множество впечатляющих библиотек для работы с data science, в частности, NumPy, SciPy, Natural Language Toolkit, Pandas и Matplotlib. Благодаря изобилию высококачественных аналитических библиотек в доступе и обширному сообществу разработчиков, Python очевидный выбор для многих исследователей данных.

Многие из этих библиотек реализованы на C и C++ из соображений производительности, но предоставляют интерфейсы внешних функций (FFI) или привязки Python, так, чтобы из функции можно было вызывать из Python. Эти реализации на более низкоуровневых языках призваны смягчить наиболее заметные недостатки Python, связанные, в частности, с длительностью выполнения и потреблением памяти. Если удается ограничить время выполнения и потребление памяти, то сильно упрощается масштабируемость, что критически важно для сокращения расходов. Если мы сможем писать высокопроизводительный код, решающий задачи data science, то интеграция такого кода с Python станет серьезным преимуществом.

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

Парсинг вредоносного ПО это в принципе опасное занятие. Вредоносное ПО часто непредусмотренным образом обращается со структурами данных, касающимися формата файлов, таким образом выводя из строя аналитические утилиты. Относительно распространенная западня, поджидающая нас в Python, связана с отсутствием хорошей типобезопасности. Python, великодушно принимающий значения None, когда на месте них ожидался bytearray, может скатиться в полный бедлам, избежать которого можно, лишь нашпиговав код проверками на None. Такие допущения, связанные с утиной типизацией часто приводят к обвалам.

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

Пример приложения для Data Science


Data science очень широкая предметная область со множеством прикладных аспектов, и обсудить их все в одной статье невозможно. Простая задача для data science вычисление информационной энтропии для байтовых последовательностей. Общая формула для расчета энтропии в битах приводится в Википедии:



Чтобы рассчитать энтропию для случайной переменной X, мы сначала считаем, сколько раз встречается каждое возможное байтовое значение , а потом делим это число на общее количество встречающихся элементов, чтобы вычислить вероятность встретить конкретное значение , соответственно . Затем считаем отрицательное значение от взвешенной суммы вероятностей конкретного значения xi, встречающегося , а также так называемую собственную информацию . Поскольку мы вычисляем энтропию в битах, здесь используется (обратите внимание на основание 2 для бит).

Давайте испробуем Rust и посмотрим, как он справляется с вычислением энтропии по сравнению с чистым Python, а также с некоторыми популярнейшими библиотеками Python, упомянутыми выше. Это упрощенная оценка потенциальной производительности Rust в области data science; данный эксперимент не является критикой Python или отличных библиотек, имеющихся в нем. В этих примерах мы сгенерируем собственную библиотеку C из кода Rust, который сможем импортировать из Python. Все тесты проводились на Ubuntu 18.04.

Чистый Python


Начнем с простой функции на чистом Python (в entropy.py) для расчета энтропии bytearray, воспользуемся при этом только математическим модулем из стандартной библиотеки. Эта функция не оптимизирована, возьмем ее в качестве отправной точки для модификаций и измерения производительности.

import mathdef compute_entropy_pure_python(data):    """Compute entropy on bytearray `data`."""    counts = [0] * 256    entropy = 0.0    length = len(data)    for byte in data:        counts[byte] += 1    for count in counts:        if count != 0:            probability = float(count) / length            entropy -= probability * math.log(probability, 2)    return entropy

Python с NumPy и SciPy


Неудивительно, что в SciPy предоставляется функция для расчета энтропии. Но сначала мы воспользуемся функцией unique() из NumPy для расчета частот байтов. Сравнивать производительность энтропийной функции SciPy с другими реализациями немного нечестно, так как в реализации из SciPy есть дополнительный функционал для расчета относительной энтропии (расстояния Кульбака-Лейблера). Опять же, мы собираемся провести (надеюсь, не слишком медленный) тест-драйв, чтобы посмотреть, какова будет производительность скомпилированных библиотек Rust, импортированных из Python. Будем придерживаться реализации из SciPy, включенной в наш скрипт entropy.py.

import numpy as npfrom scipy.stats import entropy as scipy_entropydef compute_entropy_scipy_numpy(data):    """Вычисляем энтропию bytearray `data` с SciPy и NumPy."""    counts = np.bincount(bytearray(data), minlength=256)    return scipy_entropy(counts, base=2)

Python с Rust


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

cargo new --lib rust_entropyCargo.toml

Начинаем с обязательного файла манифеста Cargo.toml, в котором определяем пакет Cargo и указываем имя библиотеки, rust_entropy_lib. Используем общедоступный контейнер cpython (v0.4.1), доступный на сайте crates.io, в реестре пакетов Rust Package Registry. В статье мы используем Rust v1.42.0, новейшую стабильную версию, доступную на момент написания.

[package] name = "rust-entropy"version = "0.1.0"authors = ["Nobody <nobody@nowhere.com>"] edition = "2018"[lib] name = "rust_entropy_lib"crate-type = ["dylib"][dependencies.cpython] version = "0.4.1"features = ["extension-module"]

lib.rs


Реализация библиотеки Rust весьма проста. Как и в случае с нашей реализацией на чистом Python, мы инициализируем массив counts для каждого возможного значения байт и перебираем данные для наполнения counts. Для завершения операции вычисляем и возвращаем отрицательную сумму вероятностей, умноженную на вероятностей.

use cpython::{py_fn, py_module_initializer, PyResult, Python};/// вычисляем энтропию массива байтfn compute_entropy_pure_rust(data: &[u8]) -> f64 {    let mut counts = [0; 256];    let mut entropy = 0_f64;    let length = data.len() as f64;    // collect byte counts    for &byte in data.iter() {        counts[usize::from(byte)] += 1;    }    // вычисление энтропии    for &count in counts.iter() {        if count != 0 {            let probability = f64::from(count) / length;            entropy -= probability * probability.log2();        }    }    entropy}

Все, что нам остается взять из lib.rs это механизм для вызова чистой функции Rust из Python. Мы включаем в lib.rs функцию, приспособленную к работе с CPython (compute_entropy_cpython()) для вызова нашей чистой функции Rust (compute_entropy_pure_rust()). Поступая таким образом, мы только выигрываем, так как будем поддерживать единственную чистую реализацию Rust, а также предоставим обертку, удобную для работы с CPython.

/// Функция Rust для работы с CPython fn compute_entropy_cpython(_: Python, data: &[u8]) -> PyResult<f64> {    let _gil = Python::acquire_gil();    let entropy = compute_entropy_pure_rust(data);    Ok(entropy)}// инициализируем модуль Python и добавляем функцию Rust для работы с CPython py_module_initializer!(    librust_entropy_lib,    initlibrust_entropy_lib,    PyInit_rust_entropy_lib,    |py, m | {        m.add(py, "__doc__", "Entropy module implemented in Rust")?;        m.add(            py,            "compute_entropy_cpython",            py_fn!(py, compute_entropy_cpython(data: &[u8])            )        )?;        Ok(())    });

Вызов кода Rust из Python


Наконец, вызываем реализацию Rust из Python (опять же, из entropy.py). Для этого сначала импортируем нашу собственную динамическую системную библиотеку, скомпилированную из Rust. Затем просто вызываем предоставленную библиотечную функцию, которую ранее указали при инициализации модуля Python с использованием макроса py_module_initializer! в нашем коде Rust. На данном этапе у нас всего один модуль Python (entropy.py), включающий функции для вызова всех реализаций расчета энтропии.

import rust_entropy_libdef compute_entropy_rust_from_python(data):    ""Вычисляем энтропию bytearray `data` при помощи Rust."""    return rust_entropy_lib.compute_entropy_cpython(data)

Мы собираем вышеприведенный библиотечный пакет Rust на Ubuntu 18.04 при помощи Cargo. (Эта ссылка может пригодиться пользователям OS X).

cargo build --release

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

Проверка производительности: результаты


Мы измеряли производительность каждой реализации функции при помощи контрольных точек pytest, рассчитав энтропию более чем для 1 миллиона случайно выбранных байт. Все реализации показаны на одних и тех же данных. Эталонные тесты (также включенные в entropy.py) показаны ниже.

# ### КОНТРОЛЬНЕ ТОЧКИ #### генерируем случайные байты для тестирования w/ NumPyNUM = 1000000VAL = np.random.randint(0, 256, size=(NUM, ), dtype=np.uint8)def test_pure_python(benchmark):    """тестируем чистый Python."""    benchmark(compute_entropy_pure_python, VAL)def test_python_scipy_numpy(benchmark):    """тестируем чистый Python со SciPy."""    benchmark(compute_entropy_scipy_numpy, VAL)def test_rust(benchmark):    """тестируем реализацию Rust, вызываемую из Python."""    benchmark(compute_entropy_rust_from_python, VAL)

Наконец, делаем отдельные простые драйверные скрипты для каждого метода, нужного для расчета энтропии. Далее идет репрезентативный драйверный скрипт для тестирования реализации на чистом Python. В файле testdata.bin 1 000 000 случайных байт, используемых для тестирования всех методов. Каждый из методов повторяет вычисления по 100 раз, чтобы упростить захват данных об использовании памяти.

import entropywith open('testdata.bin', 'rb') as f:    DATA = f.read()for _ in range(100):    entropy.compute_entropy_pure_python(DATA)

Реализации как для SciPy/NumPy, так и для Rust показали хорошую производительность, легко обставив неоптимизированную реализацию на чистом Python более чем в 100 раз. Версия на Rust показала себя лишь немного лучше, чем версия на SciPy/NumPy, но результаты подтвердили наши ожидания: чистый Python гораздо медленнее скомпилированных языков, а расширения, написанные на Rust, могут весьма успешно конкурировать с аналогами на C (побеждая их даже в таком микротестировании).

Также существуют и другие методы повышения производительности. Мы могли бы использовать модули ctypes или cffi. Могли бы добавить подсказки типов и воспользоваться Cython для генерации библиотеки, которую могли бы импортировать из Python. При всех этих вариантах требуется учитывать компромиссы, специфичные для каждого конкретного решения.



Мы также измерили расход памяти для каждой реализации функции при помощи приложения GNU time (не путайте со встроенной командой оболочки time). В частности, мы измерили максимальный размер резидентной части памяти (resident set size).

Тогда как в реализациях на чистом Python и Rust максимальные размеры этой части весьма схожи, реализация SciPy/NumPy потребляет ощутимо больше памяти по данному контрольному показателю. Предположительно это связано с дополнительными возможностями, загружаемыми в память при импорте. Как бы то ни было, вызов кода Rust из Python, по-видимому, не привносит серьезных накладных расходов памяти.



Итоги


Мы крайне впечатлены производительностью, достигаемой при вызове Rust из Python. В ходе нашей откровенно краткой оценки реализация на Rust смогла потягаться в производительности с базовой реализацией на C из пакетов SciPy и NumPy. Rust, по-видимому, отлично подходит для эффективной крупномасштабной обработки.

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

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

Мы не призываем портировать SciPy или NumPy на Rust, так как эти библиотеки Python уже хорошо оптимизированы и поддерживаются классными сообществами разработчиков. С другой стороны, мы настоятельно рекомендуем портировать с чистого Python на Rust такой код, который не предоставляется в высокопроизводительных библиотеках. В контексте приложений для data science, используемых для анализа безопасности, Rust представляется конкурентоспособной альтернативой для Python, учитывая его скорость и гарантии безопасности.
Подробнее..

PVS-Studio теперь в Compiler Explorer

06.07.2020 16:22:04 | Автор: admin
image1.png

Совсем недавно произошло знаменательное событие: PVS-Studio появился в Compiler Explorer! Теперь вы можете быстро и легко проанализировать код на наличие ошибок прямо на сайте godbolt.org (Compiler Explorer). Это нововведение открывает большое количество новых возможностей от утоления любопытства по поводу способностей анализатора до возможности быстро поделиться результатом проверки с другом. О том, как использовать эти возможности, и пойдёт речь в этой статье. Осторожно большие гифки!

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

Теперь давайте обо всём по порядку. Compiler Explorer интерактивный онлайн-сервис для исследования компиляторов. Здесь вы можете прямо на сайте писать код и сразу видеть, какой ассемблерный вывод сгенерирует для него тот или иной компилятор:

image2.gif

Помимо этого, на сайте существует еще целый набор возможностей, и сегодня мы рассмотрим возможность проведения анализа с помощью PVS-Studio. Для того, чтобы провести анализ, необходимо сделать следующее:

  1. Зайти на сайт godbolt.org,
  2. Во вкладке с выводом компилятора нажать "Add tool...",
  3. В выпадающем списке выбрать "PVS-Studio".

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

image3.gif

Готово! Теперь в появившемся окне будут автоматически выводиться все предупреждения, которые выдаются анализатором на ваш код. Анализировать можно как собственноручно написанный код, так и исходные файлы, загруженные по отдельности. Естественно, файлы должны быть самодостаточными и не использовать include на файлы, не входящие в стандартную библиотеку. В противном случае файл не получится ни скомпилировать, ни проверить.

На данный момент анализ с помощью PVS-Studio доступен на сайте для всех версий GCC и Clang под платформы x86 и x64. Мы планируем расширить возможности сайта и на другие поддерживаемые нами компиляторы (например, MSVC или компиляторы для ARM), если на это будет спрос.

Сейчас на сайте включены только General-диагностики уровней error, warning и note. Мы специально не стали включать остальные режимы (Optimization, 64-bit, Custom и MISRA), чтобы в выводе остались только самые важные предупреждения. Также, в отличие от самого PVS-Studio, Compiler Explorer пока не поддерживает языки C# и Java мы планируем запустить анализ кода на этих языках, как только они там появятся :)

Compiler Explorer имеет весьма умную систему окон, поэтому вы можете двигать их или, например, накладывать друг на друга. Если сейчас вас не интересует вывод компилятора, его можно "спрятать". Вот так:

image4.gif

Вы можете как сразу писать код в окне Compiler Explorer, так и загружать отдельные файлы. Для этого нужно нажать "Save/Load" и в открывшейся вкладке выбрать "File system". Также можно "скачать" написанный вами код на компьютер, нажав Ctrl + S.

image5.gif

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

Если вам хочется увидеть вывод вашей программы, то можно открыть окно выполнения, нажав "Add new > Execution only" в окне для написания кода (не в окне с компилятором). На гифке ниже вы можете увидеть вывод лабораторной работы, взятой из нашей страницы про бесплатное использование PVS-Studio студентами и преподавателями.

image6.gif

Кстати, вы заметили, что при переходе по ссылкам на godbolt у вас открывается уже заранее вписанный код в заранее расставленных окнах? Да, вы можете генерировать постоянные ссылки, полностью сохраняющие состояние страницы в момент генерации! Для этого вам нужно нажать на кнопку "Share" в правом верхнем углу экрана.

image7.gif

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

Мы планируем использовать эту возможность для работы с клиентами в техподдержке: зачастую использование Compiler Explorer является очень удобным для составления минимально воспроизводимых примеров, которые можно быстро взять и отправить по почте.

Также в выпадающей вкладке "Share" есть пункт создания Embedded-ссылки, с помощью которой можно встроить окно с Compiler Explorer на какой-нибудь другой сайт.

В Compiler Explorer всегда находится актуальная версия PVS-Studio, поэтому после каждого нашего релиза на сайте можно будет найти всё больше и больше ошибок. Тем не менее, использование PVS-Studio на godbolt.org не дает полного представления о его возможностях, ведь PVS-Studio это не только диагностики, но и развитая инфраструктура:

  • Анализ кода на языках C, C++, C# и Java для куда большего количества платформ и компиляторов;
  • Плагины для Visual Studio 2010-2019, JetBrains Rider, IntelliJ IDEA;
  • Возможность интеграции в TeamCity, PlatformIO, Azure DevOps, Travis CI, CircleCI, GitLab CI/CD, Jenkins, SonarQube и т.д.
  • Утилита мониторинга компиляции для проведения анализа независимо от IDE или сборочной системы;
  • И многое, многое другое.

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

Чтобы всегда быть в курсе, следите за нашими новостями. Также читайте наш блог: там мы публикуем не только новости и статьи о поиске багов в реальных проектах, но и различные интересные моменты, связанные с C, C++, C# и Java.

Наши соцсети:



Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: George Gribkov. PVS-Studio is now in Compiler Explorer!.
Подробнее..

IDA Pro каким не должен быть SDK

05.07.2020 22:23:42 | Автор: admin

Приветствую,



Эта статья будет о том, как не нужно делать, когда разрабатываешь SDK для своего продукта. А примером, можно даже сказать, самым ярким, будет IDA Pro. Те, кто хоть раз что-то разрабатывал под неё и старался поддерживать, при чтении этих строк, наверняка, сейчас вздрогнули и покрылись холодным потом. Здесь я собрал опыт сопровождения проектов, начиная с IDA v6.5, и заканчивая последней на момент написания статьи версии v7.5. В общем, погнали.


Краткое описание


SDK для IDA Pro позволяет вам разрабатывать следующие типы приложений:


  • Загрузчики различных форматов
  • Процессорные модули
  • Плагины, расширяющие функционал (процессорных модулей, интерфейса и т.п.)
  • IDC-скрипты (свой внутренний язык) и Python-скрипты (вторые использует стороннюю разработку IDAPython, которая стала неотъемлемой частью IDA)

По информации с сайта Hex-Rays, стоимость плана поддержки SDK 10000 USD. На практике же если у вас есть лицензия, вам даётся код доступа к Support-зоне, в которой вы его скачиваете и работаете с ним. Стоимость же указана на тот случай, если у вас будут появляться вопросы и вы захотите задать их разработчикам: без плана поддержки вам скажут, мол, напишите это сами; с поддержкой же, как я понимаю, отказать вам не могут. К сожалению, я не знаю ни одного человека (фирмы), который купил данный план.


Немного подробнее


С того момента, как у вас появляется желание написать что-то под IDA, и вы скачиваете SDK, вы ступаете на достаточно скользкую дорожку, на которой, к тому же, очень легко сойти с ума. И вот почему:


1) В современной разработке вы, скорее всего, уже привыкли к тому, что у любого более-менее серьёзного проекта есть либо doxygen-сгенерированная справка по API, либо вручную написанная хорошая справка, которая вам говорит что куда передаётся, что нужно заполнять, чтобы работало, и т.п., с кучей примеров.
В IDA практически в большинстве доступных пользователю API указаны лишь имена параметров и их типы, а что туда передавать можно отгадывать лишь по именам аргументов. В последнее время, конечно, со справкой у IDA стало получше, но не то что бы значительно.


2) Во многих заполняемых структурах требуется задавать callback-функции, при этом некоторые указаны как необязательные, мол, не укажешь (передашь NULL) и ладно. В действительности крэши приложения при попытке запуска вашего плагина. И, т.к. колбэков много (пример плагин-отладчик), ты начинаешь поочерёдно задавать все, которые "можно не задавать". В итоге это очень сильно утомляет, ты открываешь x64dbg/ollyDbg, в нём idaq.exe/ida.exe, грузишь плагин, ставишь точки остановки, и пытаешься словить момент, когда управление передаётся в 0x00000000.


Эх, помню те времена, когда так много папок с проектами были забиты 200MB dmp-файлами, которые создавались при крэше IDA Но мне они ничем не помогали.


3) Самая болезненная тема для IDA Pro обратная совместимость. В принципе, это достаточно тяжёлая для любого разработчика задача наперёд продумывать интерфейсы, структуры, модульность и т.п. Поэтому здесь и возникает два пути:


  • Хранить обратную совместимость со всеми старыми версиями
  • Не заниматься обратной совместимостью

В первом случае очень быстро накапливается legacy-код, от которого потом становится невозможно избавиться. Вычистить его редко кому удаётся. Потому как, фактически, нужно бросать все силы не на разработку нового функционала, а на выпиливание/перенос старого.


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


Что же в случае Hex-Rays? Вы удивитесь, но они пошли двумя путями одновременно! Известно, что проект развивается с очень и очень бородатых времён, когда основной целевой платформой был лишь MS-DOS (и, следовательно, реверс-инжиниринг написанных под него приложений). Нужно было поддерживать сегментные регистры, селекторы, параграфы и другую подобную атрибутику. Шло время, в IDA начали появляться другие платформы, процессорные модули и загрузчики, где модель памяти уже была плоской (flat), но пережиток в виде перечисленных мной MS-DOS "фич" сохраняется до сих пор! Весь интерфейс IDA пронизан этим. При разработке процессорных модулей, в который только flat, вам всё равно придётся указываться сегментные регистры (правда уже виртуальные).


А вот с SDK ни о какой обратной совместимости речи идти не может вовсе. В каждой новой версии (даже внутри минорных билдов 6.x и 7.x) что-то да ломается: у колбэков появляются новые аргументы, у старых структур переименовываются и теряются поля, функции API, которые раньше делали одну задачу, теперь делают другую. И таких примеров много.


И ладно бы это всё хоть как-то сопровождалось разработчиком, мол, в этой версии поменялось то и это, теперь нужно так и так. Так нет же! Гайд есть, да: IDA 7.0 SDK: Porting from IDA 4.9-6.x API to IDA 7.0 API, но это всё. Более того, по нему вам не удастся перевести свой проект на новую версию, т.к. он не включает очень многих, но мелких, изменений, о которых, конечно же, вам никто не сообщит. К тому же, это последний гайд для C/C++ разработчика, а с тех пор вышло ещё где-то 5-6 версий SDK.


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


Реальный пример


Когда-то я взял на себя смелость попытаться разработать свой первый плагин-отладчик Motorola 68000 под IDA. В поставляемом SDK был пример отладчика (который, фактически, используется в IDA Pro и сейчас в качестве локального и удалённого), но он был выполнен настолько плохо, что пытаться по нему сделать свой было невозможно. Тогда я полез в интернет и нашёл единственный плагин-отладчик для PS3, который, что забавно, был выполнен на базе того самого кода из SDK.


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



Видите cpp-файлы, которые включены через #include? И так по всему исходнику. Тем не менее, тщательно изучив исходный код отладчика PS3, мне удалось вычленить из него что-то рабочее и сделать свой для Sega Mega Drive.


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



Для этого пришлось снова отлаживать IDA, процессорный модуль m68k, и делать исправления для последнего (об этом я писал в "Модернизация IDA Pro. Исправляем косяки процессорных модулей").


Несмотря на все трудности, мне удалось написать хороший отладчик! К сожалению, вышла новая версия SDK В ней изменилась структура debugger_t, отвечающая за отладчик и его колбэки, и всё, что я пытался сделать, приводило к крэшам самой IDA. Спустя время я и с этим справился.


Но вышла новая версия SDK x64, без совместимости с x86! А эмулятор Gens, на базе которого я делал отладчик, не умел в x64, и проект заглох на много лет. Когда же я нашёл эмулятор, который был способен работать в x64, вышло так много версий SDK, что снова пытаться понять, почему мой плагин не работает, я не решился.


Выводы


Проблемы SDK для IDA Pro в отсутствии нормальной документации об изменениях в каждой из версий; в том, что изменения выходят скопом их много, они кардинальные, и за ними очень тяжело угнаться.


Если уж ваша фирма/команда выкладывает какой-то публичный SDK, и у него есть пользователи, которые платят деньги будьте добры, думайте и о них тоже! Их разработки могут помочь вашему продукту стать лучше и популярнее, как это произошло с IDAPython. Понятно, что хранить обратную совместимость очень сложно, но, если уж решились не поддерживать старые версии, постарайтесь документировать все изменения, которые вы делаете.


Я видел на Github большое количество полезных проектов, которые так и остались непортированными на IDA v7.x. Можно подумать, что их функционал стал ненужным в новых версиях? Может и так, но, как по мне, это усталость и нежелание бороться с постоянно меняющимся API в совокупности с хоббийностью проекта.


IDA Pro Book


Ещё хотелось бы вспомнить об одной бесценной книге, которая мне когда-то очень помогла, но которая сейчас абсолютно бесполезна для разработчика плагинов к IDA IDA Pro Book от Chris Eagle. Всё описанное в ней относится к версии 6.x (ориентировочно v6.5-v6.8). С тех пор изменилось практически всё.


Спасибо.

Подробнее..

Из песочницы Хеш-таблицы

02.07.2020 14:05:51 | Автор: admin

Предисловие


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


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


image


Мотивация использовать хеш-таблицы


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


Контейнер \ операция insert remove find
Array O(N) O(N) O(N)
List O(1) O(1) O(N)
Sorted array O(N) O(N) O(logN)
Бинарное дерево поиска O(logN) O(logN) O(logN)
Хеш-таблица O(1) O(1) O(1)

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


Понятие хеш-таблицы


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


Для начала объяснение в нескольких словах. Мы определяем функцию хеширования, которая по каждому входящему элементу будет определять натуральное число. А уже дальше по этому натуральному числу мы будем класть элемент в (допустим) массив. Тогда имея такую функцию мы можем за O(1) обработать элемент.


Теперь стало понятно, почему же это именно хеш-таблица.


Проблема коллизии


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


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


Решения проблемы коллизии методом двойного хеширования


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


Одна хеш-функция (при входе g) будет возвращать натуральное число s, которое будет для нас начальным. То есть первое, что мы сделаем, попробуем поставить элемент g на позицию s в нашем массиве. Но что, если это место уже занято? Именно здесь нам пригодится вторая хеш-функция, которая будет возвращать t шаг, с которым мы будем в дальнейшем искать место, куда бы поставить элемент g.


Мы будем рассматривать сначала элемент s, потом s + t, затем s + 2*t и т.д. Естественно, чтобы не выйти за границы массива, мы обязаны смотреть на номер элемента по модулю (остатку от деления на размер массива).


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




Реализация хеш-таблицы


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


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)        hash_result = (key * hash_result + s[i]) % table_size;    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1); // ключи должны быть взаимопросты, используем числа <размер таблицы> плюс и минус один.    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};

Чтобы идти дальше, нам необходимо разобраться с проблемой: что же будет, если мы удалим элемент из таблицы? Так вот, его нужно пометить флагом deleted, но просто удалять его безвозвратно нельзя. Ведь если мы так сделаем, то при попытке найти элемент (значение хеш-функции которого совпадет с ее значением у нашего удаленного элемента) мы сразу наткнемся на пустую ячейку. А это значит, что такого элемента и не было никогда, хотя, он лежит, просто где-то дальше в массиве. Это основная сложность использования данного метода решения коллизий.


Помня о данной проблеме построим наш класс.


template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8; // начальный размер нашей таблицы    constexpr static const double rehash_size = 0.75; // коэффициент, при котором произойдет увеличение таблицы    struct Node    {        T value;        bool state; // если значение флага state = false, значит элемент массива был удален (deleted)        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr; // соответственно в массиве будут хранится структуры Node*    int size; // сколько элементов у нас сейчас в массиве (без учета deleted)    int buffer_size; // размер самого массива, сколько памяти выделено под хранение нашей таблицы    int size_all_non_nullptr; // сколько элементов у нас сейчас в массиве (с учетом deleted)};

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


...public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr; // заполняем nullptr - то есть если значение отсутствует, и никто раньше по этому адресу не обращался    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }

Из необходимых методов осталось еще реализовать динамическое увеличение, расширение массива метод Resize.
Увеличиваем размер мы стандартно вдвое.


void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value); // добавляем элементы в новый массив        }        // удаление предыдущего массива        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Немаловажным является поддержание асимптотики O(1) стандартных операций. Но что же может повлиять на скорость работы? Наши удаленные элементы (deleted). Ведь, как мы помним, мы ничего не можем с ними сделать, но и окончательно обнулить их не можем. Так что они тянутся за нами огромным балластом. Для ускорения работы нашей хеш-таблицы воспользуемся рехешом (как мы помним, мы уже выделяли под это очень странные переменные).


Теперь воспользуемся ими, если процент реальных элементов массива стало меньше 50 %, то мы производим Rehash, а именно делаем то же самое, что и при увеличении таблицы (resize), но не увеличиваем. Возможно, это звучит глуповато, но попробую сейчас объяснить. Мы вызовем наши хеш-функции от всех элементов, переместим их в новых массив. Но с deleted-элементами это не произойдет, мы не будем их перемещать, и они удалятся вместе со старой таблицей.


Но к чему слова, код все разъяснит:


void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        // удаление предыдущего массива        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }

Ну теперь мы уже точно на финальной, хоть и длинной, и полной колючих кустарников, прямой. Нам необходимо реализовать вставку (Add), удаление (Remove) и поиск (Find) элемента.


Начнем с самого простого метод Find элемент по значению.


bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size); // значение, отвечающее за начальную позицию        int h2 = hash2(value, buffer_size); // значение, ответственное за "шаг" по таблице        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true; // такой элемент есть            h1 = (h1 + h2) % buffer_size;            ++i; // если у нас i >=  buffer_size, значит мы уже обошли абсолютно все ячейки, именно для этого мы считаем i, иначе мы могли бы зациклиться.        }        return false;    }

Далее мы реализуем удаление элемента Remove. Как мы это делаем? Находим элемент (как в методе Find), а затем удаляем, то есть просто меняем значение state на false, но сам Node мы не удаляем.


bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }

Ну и последним мы реализуем метод Add. В нем есть несколько очень важных нюансов. Именно здесь мы будем проверять на необходимость рехеша.


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


bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash(); // происходит рехеш, так как слишком много deleted-элементов        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1; // запоминаем первый подходящий (удаленный) элемент        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false; // такой элемент уже есть, а значит его нельзя вставлять повторно            if (!arr[h1]->state && first_deleted == -1) // находим место для нового элемента                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1) // если не нашлось подходящего места, создаем новый Node        {            arr[h1] = new Node(value);            ++size_all_non_nullptr; // так как мы заполнили один пробел, не забываем записать, что это место теперь занято        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size; // и в любом случае мы увеличили количество элементов        return true;    }

В заключение приведу полную реализацию хеш-таблицы:


int HashFunctionHorner(const std::string& s, int table_size, const int key){    int hash_result = 0;    for (int i = 0; s[i] != 0; ++i)    {        hash_result = (key * hash_result + s[i]) % table_size;    }    hash_result = (hash_result * 2 + 1) % table_size;    return hash_result;}struct HashFunction1 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size - 1);    }};struct HashFunction2 {    int operator()(const std::string& s, int table_size) const    {        return HashFunctionHorner(s, table_size, table_size + 1);    }};template <class T, class THash1 = HashFunction1, class THash2 = HashFunction2>class HashTable{    static const int default_size = 8;    constexpr static const double rehash_size = 0.75;    struct Node    {        T value;        bool state;        Node(const T& value_) : value(value_), state(true) {}    };    Node** arr;    int size;    int buffer_size;    int size_all_non_nullptr;    void Resize()    {        int past_buffer_size = buffer_size;        buffer_size *= 2;        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < past_buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < past_buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }    void Rehash()    {        size_all_non_nullptr = 0;        Node** arr2 = new Node * [buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr2[i] = nullptr;        std::swap(arr, arr2);        for (int i = 0; i < buffer_size; ++i)        {            if (arr2[i] && arr2[i]->state)                Add(arr2[i]->value);        }        for (int i = 0; i < buffer_size; ++i)            if (arr2[i])                delete arr2[i];        delete[] arr2;    }public:    HashTable()    {        buffer_size = default_size;        size = 0;        size_all_non_nullptr = 0;        arr = new Node*[buffer_size];        for (int i = 0; i < buffer_size; ++i)            arr[i] = nullptr;    }    ~HashTable()    {        for (int i = 0; i < buffer_size; ++i)            if (arr[i])                delete arr[i];        delete[] arr;    }    bool Add(const T& value, const THash1& hash1 = THash1(),const THash2& hash2 = THash2())    {        if (size + 1 > int(rehash_size * buffer_size))            Resize();        else if (size_all_non_nullptr > 2 * size)            Rehash();        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        int first_deleted = -1;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return false;            if (!arr[h1]->state && first_deleted == -1)                first_deleted = h1;            h1 = (h1 + h2) % buffer_size;            ++i;        }        if (first_deleted == -1)        {            arr[h1] = new Node(value);            ++size_all_non_nullptr;        }        else        {            arr[first_deleted]->value = value;            arr[first_deleted]->state = true;        }        ++size;        return true;    }    bool Remove(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)            {                arr[h1]->state = false;                --size;                return true;            }            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }    bool Find(const T& value, const THash1& hash1 = THash1(), const THash2& hash2 = THash2())    {        int h1 = hash1(value, buffer_size);        int h2 = hash2(value, buffer_size);        int i = 0;        while (arr[h1] != nullptr && i < buffer_size)        {            if (arr[h1]->value == value && arr[h1]->state)                return true;            h1 = (h1 + h2) % buffer_size;            ++i;        }        return false;    }
Подробнее..

Game of Life с битовой магией, многопоточностью и на GPU

03.07.2020 20:16:17 | Автор: admin

Всем привет!


Недавняя статья на Хабре в очередной раз показала неостывающий интерес к игре Жизнь в частности и всевозможным оптимизациям в общем. Статья и комментарии к ней, особенно любопытство к вычислениям на GPU, вдохновили меня на то, чтобы поделиться своими изысканиями на данном поприще и, забегая вперёд, скажу, что повествование пойдёт о расчётах на GPU, битовой магии, многопоточности и огромных полях для игры Жизнь, порядка миллиарда клеток.



О себе


Пару слов о себе и проекте. Вот уже несколько лет моим хобби и pet-проектом является написание игры Жизнь, в ходе которого я разбираюсь в интересующих меня темах. Так, сперва я сделал её с помощью библиотеки glut и прикрутил многопользовательский режим, будучи вдохновлённым архитектурой peer-to-peer. А около двух лет назад решил начать всё с нуля, используя Qt и вычисления на GPU.

Масштабы


Идея нового проекта заключалась в том, чтобы сделать кроссплатформенное приложение, в первую очередь для мобильных устройств, в котором пользователи смогут окунуться в игру Жизнь, познакомиться с разнообразием всевозможных паттернов, наблюдать за причудливыми формами и комбинациями этой игры, выбрав скорость от 1 до 100 мс на шаг и задав размер игрового поля вплоть до 32'768 x 32'768, то есть 1'073'741'824 клетки.

На всякий случай напомню об игре Жизнь. Игра происходит на плоскости, поделённой на множество клеток. Клетки квадратные, соответственно, для каждой клетки есть 8 соседей. Каждая клетка может быть либо живой, либо мёртвой, то есть пустой. В рамках игры пользователь заполняет клетки жизнью, выставляя на поле заинтересовавшие его комбинации клеток паттерны.
Далее процесс шаг за шагом развивается по заданным правилам:
  • в пустой клетке, рядом с которой ровно 3 живые клетки, зарождается жизнь
  • если у живой клетки есть 2 или 3 живых соседки, то эта клетка продолжает жить
  • если у живой клетки меньше 2 или больше 3 живых соседки, клетка умирает

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

В моей реализации поле зациклено, то есть после 32'767-й клетки вновь следует 0-я. Таким образом, перемещающиеся паттерны способны обогнуть поле и оказаться в точке, где они были размещены изначально. Забавный эффект случается с паттерном планёрное ружьё, когда выпущенные им планёры в конечном итоге врезаются в само ружьё и разрушают его, этакое самоубийство в мире клеточных автоматов:

Gosper glider gun


Архитектура


Что касается реализации, то в первую очередь хочется отметить, что каждая клетка в игровом поле представляет собой всего лишь 1 бит в памяти. Так, при выборе размера игрового поля в памяти выделяется буфер размером n^2 / 8 байт. Это улучшает локальность данных и снижает объём потребляемой памяти, а главное позволяет применить ещё более хитроумные оптимизации, о которых поговорим ниже. Игровое поле всегда квадратное и его сторона всегда степени 2, в частности затем, чтобы без остатка осуществлять деление на 8.

Архитектурно выделяются два слоя, ответственные за вычисления:
  • низкий уровень платформозависимый; на текущий момент существует реализация на Metal API графическое API от Apple позволяет задействовать GPU на устройствах от Apple, а также fallback-реализация на CPU. Одно время существовала версия на OpenCL. Планируется реализация на Vulkan API для запуска на Android
  • высокий уровень кроссплатформенный; по сути класс-шаблонный метод, делегирующий реализацию нужных методов на низкий уровень

Низкий уровень


Задача низкого уровня заключается непосредственно в расчёте состояния игрового поля и устроена следующим образом. В памяти хранятся два буфера n^2 / 8 байт. Первый буфер служит как input текущее состояние игрового поля. Второй буфер как output, в который записывается новое состояние игрового поля в процессе расчётов. По завершении вычислений достаточно сделать swap буферов и следующий шаг игры готов. Такая архитектура позволяет распараллелить расчёты в силу константности input. Дело в том, что каждый поток может независимо обрабатывать клетки игрового поля.

За счёт многопоточности GPU-подход добивается максимальной эффективности. На CPU также происходит распараллеливание, но, соответственно, с гораздо меньшим числом потоков и меньшей эффективностью, что отражено в секции бенчмарков ниже. Сам алгоритм идентичен для всех реализаций и заключается в следующем.

Назовём атомарной операцией расчёт одного байта буфера, хранящего игровое поле, то есть расчёт восьми клеток. Этот расчёт совершается одним потоком и требует информации о соседних восьми байтах. Попробуем схематично изобразить один байт следующим образом, где точка это бит:

[........]


А участок игрового поля с интересующим нас байтом и его восемью соседними байтами изобразим так:

[........][........][........]
[........][********][........]
[........][........][........]


Исходя из рисунка, можно догадаться, что все соседние клетки можно уложить в один uint64_t (назовём его neighbours), а ещё в одном uint8_t (назовём его self), будет храниться информация о соседях в самом обрабатываемом байте.

Рассмотрим на примере расчёт 0-го бита целевого байта. Звёздочкой отметим интересующий бит, а нулём соседние для него биты:

[.......0][00......][........]
[.......0][*0......][........]
[.......0][00......][........]


Пример посложнее. Здесь звёздочкой отмечены 0-й, 3-й и 7-й биты целевого байта и соответствующими числами соседние биты:

[.......0][00333.77][7.......]
[.......0][*03*3.7*][7.......]
[.......0][00333.77][7.......]


Думаю, кто-то из читателей уже догадался, в чём заключается магия. Имея указанную информацию, остаётся лишь составить битовые маски для каждого бита целевого байта и применить их к neighbours и self соответственно. Результатом станут 2 значения, сумма единичных бит которых будет характеризовать число соседей, что можно интерпретировать как правила игры Жизнь: 2 или 3 бита для поддержания жизни в живой клетке и 3 бита для зарождения новой жизни в пустой клетке. В противном случае клетка остаётся/становится пустой.

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

После заполнения всего выходного буфера игровое поле считается рассчитанным.
Код Metal-шейдера по обработке 1 байта
// Бросается в глаза C++-подобный синтаксис#include <metal_stdlib>#include <metal_integer>using namespace metal;// Вспомогательные функции для вычисления позиции клеткиushort2 pos(uint id) {   return ushort2(id % WIDTH, id / HEIGHT); }uint idx(ushort2 pos) {   return pos.x + pos.y * HEIGHT; }ushort2 loopPos(short x, short y) {   return ushort2((x + WIDTH) % WIDTH, (y + HEIGHT) % HEIGHT); }// Битовые маски для вычисления соседей интересующего битаtemplate<uint Bit> struct Mask {   constant constexpr static uint c_n_e_s_w = 0x70007 << (Bit - 1);   constant constexpr static uint c_nw_ne_se_sw = 0x0;   constant constexpr static uint c_self = 0x5 << (Bit - 1); };template<> struct Mask<0> {   constant constexpr static uint c_n_e_s_w = 0x80030003;   constant constexpr static uint c_nw_ne_se_sw = 0x80000080;   constant constexpr static uint c_self = 0x2; };template<> struct Mask<7> {   constant constexpr static uint c_n_e_s_w = 0xC001C0;   constant constexpr static uint c_nw_ne_se_sw = 0x10100;   constant constexpr static uint c_self = 0x40; };// Для указанного бита функция вычисляет состояние клетки в зависимости от её соседей, применяя соответствующие биту маскиtemplate<uint Bit>uint isAlive(uint self, uint n_e_s_w, uint nw_ne_se_sw) {   /*  [.......0][00333.77][7.......]  [.......0][*03*3.7*][7.......]  [.......0][00333.77][7.......]  */  // До определённой версии в Metal не было 64-битного целого, поэтому составляются две маски  uint neighbours = popcount(Mask<Bit>::c_n_e_s_w & n_e_s_w)     + popcount(Mask<Bit>::c_nw_ne_se_sw & nw_ne_se_sw)     + popcount(Mask<Bit>::c_self & self);   return static_cast<uint>((self >> Bit & 1) == 0     ? neighbours == 3     : neighbours == 2 || neighbours == 3) << Bit;}// Язык Metal даже умеет в шаблонную магиюtemplate<uint Bit>uint calculateLife(uint self, uint n_e_s_w, uint nw_ne_se_sw) {   return isAlive<Bit>(self, n_e_s_w, nw_ne_se_sw)     | calculateLife<Bit - 1>(self, n_e_s_w, nw_ne_se_sw); }template<>uint calculateLife<0>(uint self, uint n_e_s_w, uint nw_ne_se_sw){  return isAlive<0>(self, n_e_s_w, nw_ne_se_sw); }// Главная функция compute-шейдера. На вход подаются два буфера, о которых речь шла выше - константный input и output, а также id - координата целевого байтаkernel void lifeStep(constant uchar* input [[buffer(0)]],                             device uchar* output [[buffer(1)]],                             uint id [[thread_position_in_grid]]) {   ushort2 gid = pos(id * 8);   // Вычисляем соседние байты  uint nw = idx(loopPos(gid.x - 8, gid.y + 1));   uint n  = idx(loopPos(gid.x,     gid.y + 1));   uint ne = idx(loopPos(gid.x + 8, gid.y + 1));   uint e  = idx(loopPos(gid.x + 8, gid.y    ));   uint se = idx(loopPos(gid.x + 8, gid.y - 1));   uint s  = idx(loopPos(gid.x    , gid.y - 1));   uint sw = idx(loopPos(gid.x - 8, gid.y - 1));   uint w  = idx(loopPos(gid.x - 8, gid.y    ));   // Вычисляем байт с целевым битом  uint self = static_cast<uint>(input[id]);   // Подготавливаем битовые маски с соседями  // north_east_south_west  uint n_e_s_w = static_cast<uint>(input[n >> 3]) << 0 * 8     | static_cast<uint>(input[e >> 3]) << 1 * 8     | static_cast<uint>(input[s >> 3]) << 2 * 8     | static_cast<uint>(input[w >> 3]) << 3 * 8;   // north-west_north-east_south-east_south-west  uint nw_ne_se_sw = static_cast<uint>(input[nw >> 3]) << 0 * 8     | static_cast<uint>(input[ne >> 3]) << 1 * 8     | static_cast<uint>(input[se >> 3]) << 2 * 8     | static_cast<uint>(input[sw >> 3]) << 3 * 8;     // В этой строчке рассчитываются все 8 клеток обрабатываемого байта  output[id] = static_cast<uchar>(calculateLife<7>(self, n_e_s_w, nw_ne_se_sw)); };


Высокий уровень


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

Отрисовка клеток выполняется средствами Qt, а именно посредством заполнения пикселей в QImage. Интерфейс выполнен в QML. Пиксели заполняются лишь для небольшой области видимого игроку игрового поля. Таким образом удаётся избежать лишних затрат ресурсов на отрисовку.

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

Бенчмарки


Результаты замеров скорости работы игры для разных машин в зависимости от размеров игрового поля приведены в таблицах. Полученные значения в таблицах указаны в миллисекундах. При проведении тестов игровое поле заполнялось жизнью на 50% с помощью генератора случайных чисел.

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

MacBook Pro 2014
Processor 2,6 GHz Dual-Core Intel Core i5
Memory 8 GB 1600 MHz DDR3
Graphics Intel Iris 1536 MB

GPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 0 0 2 9 43 170
Высокий уровень (min) 0 0 0 1 12 55
100 шагов 293 446 1271 2700 8603 54287
Время на шаг (avg) 3 4 13 27 86 542

CPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 3 25 117 552 2077 21388
Высокий уровень (min) 0 0 0 1 4 51
100 шагов 944 3894 15217 65149 231260 -
Время на шаг (avg) 9 39 152 651 2312 -

MacBook Pro 2017
Processor 2.8 GHz Intel Core i7
Memory 16 GB 2133 MHz LPDDR3
Graphics Intel HD Graphics 630 1536 MB

GPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 0 0 0 2 8 38
Высокий уровень (min) 0 0 0 0 3 13
100 шагов 35 67 163 450 1451 5886
Время на шаг (avg) 0 1 2 5 15 59

CPU реализация
1024 2048 4096 8192 16384 32768
Низкий уровень (min) 1 7 33 136 699 2475
Высокий уровень (min) 0 0 0 0 3 18
100 шагов 434 1283 4262 18377 79656 264711
Время на шаг (avg) 4 13 43 184 797 2647

Видно, что на стареньком макбуке процессор совсем не справляется с огромным игровым полем и приложение становится неюзабельным. Даже процессор нового макбука едва вытягивает такой объём вычислений. Зато видеокарта значительно превосходит по производительности процессор как на старом, так и на новом железе. С использованием GPU новый макбук предлагает вполне комфортный пользовательский опыт даже на огромных игровых пространствах.

Итог


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

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

В будущем хочется провести бенчмарки на мобильных устройствах. Проект находится на ранней стадии разработки, а потому Apple-Developer аккаунта для проведения таких тестов сейчас у меня нет.

Спасибо за внимание! Буду рад любым комментариям к статье и к проекту:
Код на GitHub.

Код Metal реализации нижнего уровня

Код CPU реализации нижнего уровня

Код верхнего уровня
Подробнее..

Разноцветные окошки виртуальный конструктор, CRTP и забористые шаблоны

18.06.2020 00:23:42 | Автор: admin
С достаточно давних времён известен нетривиальный шаблон проектирования, когда производный класс передаётся в параметре базового:
template<class T> class Base{};class Derived : public Base<Derived>{};

Этот шаблон имеет своё собственное название CRTP: Curiously Recurring Template Pattern, что переводится как странно повторяющийся шаблон. Я же к этой и без того странной конструкции добавил ещё больше странностей: обобщил её на целую цепочку наследований. Да, это действительно можно сделать, но ради этого придётся отдать душу заплатить большую цену. Чтобы узнать, как это у меня получилось и какую цену придётся заплатить, за подробностями приглашаю читать дальше эту статью. Здесь мы будем заниматься страшными извращениями различными странными методами и прочими нехорошими вещами.
Сразу хочу предупредить: не воспринимайте описываемый здесь материал как что-то серьёзное. Уверен, что в 95-99% случаев вам всё это ни разу не пригодится на практике. Это нечто вроде занимательной математики, разминки для ума. На практике вряд ли пригодится, но уделить этому время интересно. Только в данном случае в качестве математики выступает язык С++ и его возможности. Предупреждаю заранее, т.к. если вы ищете здесь чего-то серьёзного и практически ориентированного, вы можете разочароваться.
Ещё сразу настраивайтесь на экзотику, будто вы внезапно попали в страну, где две луны, три солнца, листья растений синие или сиреневые, да и вообще многие привычные вещи здесь какие-то странные и необычные Если вы погрязли в серых буднях и давно не читали чего-нибудь этакого, то вы пришли по адресу

Разноцветные окошки


Это было очень давно. Почти три года назад. Я тогда сидел на тяжёлой траве только постигал дзен основы С++11/14 по книге Мейерс С. Эффективный и современный С++. В ней тоже встречается упоминание этого шаблона. После этого, как я почувствовал, что достиг просветления освоил основы нового стандарта и готов смотреть на старые вещи по-новому, я стал освежать в памяти книгу по Windows API: Щупак Ю. Win32 API. Эффективная разработка приложений. В самом начале в ней описывается минимальная программа на языке С для создания и вывода окна:
#include <Windows.h>HWND hMainWnd;TCHAR szClassName[] = TEXT("MyClass");MSG msg;WNDCLASSEX *wc;LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){HDC hDC;PAINTSTRUCT ps;RECT rect;switch(uMsg){case WM_CREATE:SetClassLongPtr(hWnd, -10, (LONG)CreateSolidBrush(RGB(200, 160, 255)));break;case WM_PAINT:hDC = BeginPaint(hWnd, &ps);GetClientRect(hWnd, &rect);DrawText(hDC, TEXT("Hello, world!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);EndPaint(hWnd, &ps);break;case WM_CLOSE:DestroyWindow(hWnd);break;case WM_DESTROY:PostQuitMessage(0);break;default:return DefWindowProc(hWnd, uMsg, wParam, lParam);}return 0;}int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){if(!(wc = new WNDCLASSEX)){MessageBox(NULL, TEXT("Ошибка выделения памяти!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);return 0;}wc->cbSize = sizeof(WNDCLASSEX);wc->style = CS_HREDRAW | CS_VREDRAW;wc->lpfnWndProc = WndProc;wc->cbClsExtra = 0;wc->cbWndExtra = 0;wc->hInstance = hInstance;wc->hIcon = LoadIcon(NULL, IDI_APPLICATION);wc->hCursor = LoadCursor(NULL, IDC_ARROW);wc->hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);wc->lpszMenuName = NULL;wc->lpszClassName = szClassName;wc->hIconSm = LoadIcon(NULL, IDI_APPLICATION);//регистрируем класс окнаif(!RegisterClassEx(wc)){MessageBox(NULL, TEXT("Не удается зарегистрировать класс для окна!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);return 0;}delete wc;//создаём главное окноhMainWnd = CreateWindow(szClassName, TEXT("A Hello1 Application"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, (HWND)NULL, (HMENU)NULL, (HINSTANCE)hInstance, NULL);if(!hMainWnd){MessageBox(NULL, TEXT("Не удается создать окно!"), TEXT("Ошибка"), MB_OK | MB_ICONERROR);return 0;}//показываем наше окноShowWindow(hMainWnd, nCmdShow);//UpdateWindow(hMainWnd);//выполняем цикл обработки сообщений до закрытия приложенияwhile(GetMessage(&msg, NULL, 0, 0)){TranslateMessage(&msg);DispatchMessage(&msg);}//MessageBox(NULL, TEXT("Application is going to quit."), TEXT("Exit"), MB_OK);return 0;}

Я уже делал это много раз, выводя разные окошки по образцу этой книги. И внезапно задумался: я ж только буквально вчера читал про С++! Я ведь могу написать свой класс для вывода этого окна!
Сказано сделано:
class WindowClass//класс окна Windows{//данныеHWND hWnd = NULL;//дескриптор класса окнаWNDCLASSEX wc = { 0 };//структура для регистрации класса окна внутри Windowsconst TCHAR *szWndTitle = nullptr;//заголовок окнаstatic const TCHAR *szWndTitleDefault;//строка заголовка по умолчаниюstatic List wndList;//статический список, единый для всех классов//функцииstatic LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);//оконная процедура (статическая функция)bool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr);//инициализирует и создаёт окно (вызывается из конструкторов)virtual void OnCreate(HWND hWnd);//обработка WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd);//обработка WM_PAINT внутри оконной процедурыvirtual void OnClose(HWND hWnd);//обработка WM_CLOSE внутри оконной процедурыvirtual void OnDestroy(HWND hWnd);//обработка WM_DESTROY внутри оконной процедуры//привилегированные классыfriend List;public://функцииWindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор для инициализации класса по умолчаниюWindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClass(WindowClass&);//конструктор копированияvirtual ~WindowClass();//виртуальный деструктор};

Структура класса тривиальна: объявляются несколько конструкторов (с передачей как только основных параметров, так и ссылки на более подробно заполненную структуру WNDCLASSEX), функция CreateWnd собственно регистрации класса окна и создания окна, вызываемая из конструкторов, а также набор виртуальных функций-членов, выполняющих действия по обработке каждого из сообщений Windows внутри оконной процедуры обратного вызова.
Члены данные класса тоже минимальны: дескриптор окна hWnd; структура WNDCLASSEX, используемая при создании класса; и строка-заголовок окна.
Оконная процедура обратного вызова объявляется как static, чтобы избежать неявной передачи указателя this на объект класса и таким образом нарушить соглашение на тип (сигнатуру) функции оконной процедуры, принятой в Windows (вспоминаем, что эту функцию будет вызывать не мы сами, а Windows, потому параметры и возвращаемый тип этой функции строго заданы).

Оконная процедура и указатель this


Из С++ известно: если член-функция определяется как статическая, указатель на объект класса ей должен передаваться явно. Однако мы не можем передать статической оконной процедуре указатель на объект класса, поскольку формат этой функции не допускает эту передачу. В связи с этим возникает фундаментальная проблема: если имеется несколько объектов класса WindowClass, то как единственная статическая оконная процедура узнает, какому именно объекту класса пришло сообщение?
Выход один: нужно эту связь тем или иным способом установить.
Windows идентифицирует то или иное окно по его дескриптору HWND hWnd. Объект класса, соответствующий этому окну, можно идентифицировать по указателю на этот объект. Следовательно, необходимо установить связь hWnd <-> указатель на объект WindowClass. Например, оконная процедура, будучи одновременно членом класса, могла бы иметь ссылку или указатель на некоторую тоже статическую структуру данных, устанавливающую связь между hWnd и указателем на объект для каждого окна и обновляемую при каждом создании объекта класса. Структура данных должна быть статической, чтобы, во-первых, к ней можно было получить доступ изнутри статической оконной процедуры, не имея указателя на любой объект класса, во-вторых, чтобы она была единственной для всех объектов класса (что логически вытекает из её назначения), и в третьих, чтобы она всё-таки была привязана к классу с соответствующим уровнем доступа, а не являлась некой внешней глобальной переменной.
Теперь, после выяснения того, как эту структуру описать и зачем она нужна, осталось выяснить, что должна представлять собой эта структура.
Можно объявить два динамических массива: один для дескрипторов окон HWND, второй для указателей на объекты WindowClass. Однако это не лучшее решение: неясно, каким выбрать размер массива, какие будут сценарии использования окон, не окажутся ли массивы почти пустующими при неверном выборе их размера, что вызовет перерасход памяти. Либо, наоборот, когда при создании окон их объем исчерпается, потребуется увеличивать их размеры и т.п.
Более лучшим (и даже я бы сказал идеальным) решением в этой ситуации является список (список!). Список это динамическая структура данных, состоящая из набора связанных попарно узлов. Каждый узел (в случае двусвязного списка) имеет указатели на предыдущий и следующий узлы списка, а также дополнительные хранимые данные. В нашей ситуации каждому узлу списка соответствует каждое из окон, а полезные данные это дескриптор окна и указатель на объект класса WindowClass.
Таким образом, при каждом создании нового окна создаётся новый узел списка и добавляется в его конец (становится последним). При закрытии узел удаляется, а указатели предыдущего и следующего узлов настраиваются друг на друга, чтобы заместить удалённый узел. При этом нет никакого перерасхода памяти создаётся ровно столько узлов, сколько создано окон, и удаляются они также одновременно с закрытием окна.
Следовательно, в класс WindowClass следует добавить также новый статический член:
static List wndList;//статический список, единый для всех классов

и объявить его привилегированным, чтобы дать возможность ему обращаться к членам WindowClass:
friend List;

(Я не буду здесь сейчас давать определение класса списка и узла, их функций, поскольку это не относится непосредственно к классу WindowClass, а логика реализации этого класса известна и достаточно тривиальна.)
Таким образом, оконная процедура при поступлении нового сообщения в случае, если оно принадлежит к числу обрабатываемых ею, по переданному ей из Windows дескриптору окна hWnd обращается к списку, выполняет в нём поиск узла по заданному hWnd и, найдя, получает требуемый указатель на объект класса WindowClass. Затем вызывает по указателю виртуальную функцию, соответствующую обрабатываемому сообщению: у переопределённого класса виртуальная функция с тем же именем может выполнять другие действия.
LRESULT CALLBACK WindowClass::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){//оконная процедураListElement * pListElem = nullptr;switch (uMsg){case WM_CREATE:{//lParam содержит указатель на структуру типа CREATESTRUCT, содержающую помимо всего прочего указатель на объект класса WindowClass, который нам//нужен (см. функцию WindowClass::CreateWnd)CREATESTRUCT *cs = reinterpret_cast<CREATESTRUCT *>(lParam);WindowClass *p_wndClass = reinterpret_cast<WindowClass *>(cs->lpCreateParams);p_wndClass->hWnd = hWnd;//инициализируем hWnd объекта класса значением, переданным в оконную процедуру//заносим созданное окно в списокpListElem = wndList.add(p_wndClass);if (pListElem)pListElem->p_wndClass->OnCreate(hWnd);//вызываем виртуальную функцию, соответствующую данному дескриптору}break;case WM_PAINT:pListElem = wndList.search(hWnd);//ищем в списке объект класса по заданному дескриптору окнаif (pListElem)pListElem->p_wndClass->OnPaint(hWnd);//вызываем виртуальную функцию, соответствующую данному дескрипторуbreak;case WM_CLOSE:pListElem = wndList.search(hWnd);//ищем в списке объект класса по заданному дескриптору окнаif (pListElem)pListElem->p_wndClass->OnClose(hWnd);//вызываем виртуальную функцию, соответствующую данному дескрипторуbreak;case WM_DESTROY:pListElem = wndList.search(hWnd);//ищем в списке объект класса по заданному дескриптору окнаif (pListElem)pListElem->p_wndClass->OnDestroy(hWnd);  //вызываем виртуальную функцию, соответствующую данному дескрипторуbreak;default:return DefWindowProc(hWnd, uMsg, wParam, lParam);}return 0;}

Здесь есть один тонкий момент. Он касается инициализации класса и обработки сообщения WM_CREATE.
При создании окна функцией CreateWindow, на момент её вызова, дескриптор окна hWnd ещё не известен: окно ведь ещё не создано! Следовательно, чтобы иметь возможность вызывать виртуальную OnCreate, нужно знать указатель на объект класса. Делается это довольно рискованной передачей указателя this из функции WindowClass::CreateWnd в функцию CreateWindow через указатель lParam. Оконная процедура при обработке WM_CREATE получает из параметра этот указатель, с его помощью инициализирует внутри объекта член hWnd, а затем создаёт новый узел списка для данного окна по указателю на объект класса. После чего вызывает виртуальную OnCreate по указателю.
Для остальных же сообщений выполняется описанная выше логика: поиск узла списка по текущему переданному из Windows дескриптору окна hWnd, а затем вызов нужной виртуальной функции по указателю на объект класса из узла списка.
Скомпилировав программу и убедившись, что всё работает правильно, я, потирая руки от чувства собственного величия от проделанной работы, принялся читать дальше. А там на следующей же странице указывается функция изменения свойств окна:
DWORD SetClassLong(HWND hWnd, int nIndex, LONG dwNewLong);

Я тут же на месте решил создать новое окно на основе старого:
class WindowClassDerived : public WindowClass//построение нового класса с другой логикой работы на основе старого{static unsigned short int usiWndNum;//количество объектов классаpublic:WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор для инициализации класса по умолчаниюWindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassDerived(WindowClassDerived&);//конструктор копированияvirtual ~WindowClassDerived() override;//виртуальный деструкторvirtual void OnCreate(HWND hWnd) override;//обеспечивает обработку WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd) override;//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnDestroy(HWND hWnd) override;//обеспечивает обработку WM_DESTROY внутри оконной процедуры};

Производный класс отличается от базового добавлением статического счётчика окон, а также изменением OnCreate, OnPaint и OnDestroy: функция OnCreate меняет цвет фона окна, OnPaint выводит другое сообщение, а OnDestroy уменьшает статический счётчик окон. Всё очень просто и понятно. Собрал и запустил. Текст сообщения стал другим
а цвет окна не изменился.

Виртуальный конструктор


Я тогда ещё понял, что уже ступил на тонкий лёд. Не все нюансы описаны в базовом материале основных книг. Одна из таких виртуальный конструктор. Я думал, что вызову из конструктора виртуальную функцию производного класса точно так же, как и всюду в других частях программы. Выяснилось, что этого сделать нельзя.
Проблема заключается в том, что виртуальная функция, вызываемая из конструктора, вызывается как не виртуальная: создан только объект базового класса, и то не до конца, а объект производного ещё не создан, и таблица виртуальных функций не сформирована. В нашем случае получается цепочка: конструктор производного -> конструктор базового -> CreateWnd -> CreateWindow -> оконная процедура -> OnCreate, то есть OnCreate вызывается действительно из конструктора. Производный объект ещё не создан, следовательно, вызывается OnCreate для базового класса! Её переопределение в производном, получается, не имеет смысла! Что же делать?
Из С++ известно, что любую переопределённую функцию можно вызвать по её полному имени: имя_класса:: имя_функции. Имя класса это не просто имя: оно идентифицирует собой, фактически, тип объекта. Также из С++ известно, что класс (и функцию) можно сделать шаблонным (шаблонной), передавая ему (ей) тип в качестве параметра. Следовательно, если функцию оконной процедуры сделать шаблонной и передать ей каким-нибудь образом тип производного класса, можно добиться вызова нужной переопределённой функции напрямую в конструкторе базового класса.
Стоп-стоп-стоп!!! Так же делать нельзя!!! Производный класс ещё не создан, его данные не инициализированы: какие функции ты тут собрался вызывать?
Если нельзя, но очень хочется, то можно. Конечно, я не нацеливался на полноценное обращение к производному классу. Я имел ввиду, чтобы вызвать совершенно стороннюю функцию WinAPI, которая не имеет никакого отношения к классу. Но это ведь можно сделать совершенно другими способами, и гораздо проще! скажете вы. Да. Можно. И я напишу об этом в конце статьи. Но в тот момент я отбросил всё это в сторону и сосредоточился на чисто технической стороне вопроса: а всё-таки, можно ли в принципе в конструкторе базового класса вызывать что-нибудь из производного? Это был чисто спортивный интерес, если хотите. О какой-либо практической стороне я в тот момент не думал. Это была нетривиальная задача, и мне стало интересно, смогу ли я её решить.

Шаблонный класс окна способ 1


Итак, возникает сложность: как передать оконной процедуре тип производного класса?
Делать весь базовый класс WindowClass шаблонным я сразу не хотел: для каждого производного класса будет генерироваться свой собственный базовый. Кроме того, поскольку WindowClass станет шаблонным, то и узлы списка, и сам список тоже придётся делать шаблонными: они имеют указатели на объекты класса, а чтобы пользоваться этими указателями, они должны знать их тип, то есть WindowClass и то, чем он будет параметризован. На момент определения класса списка и узла это неизвестно, следовательно, этот тип тоже необходимо передавать как параметр (из WindowClass). Отсюда вытекает, что для каждого производного класса будет создаваться свой собственный список, соответствующий этому производному классу (и только ему)! Да и указатели теперь на базовые классы, соответствующие разным производным, в один массив не засунешь: у них типы разные.
Поэтому я стал искать способ всё же передать тип производного класса, не параметризуя весь класс целиком. Тип базовому классу можно передать только через конструктор: это единственная функция, к которой происходит обращение при создании объекта. Следовательно, она должна быть шаблонной. Однако выяснилось, что указать параметры шаблона ей явно нельзя: это будет выглядеть так же, как передача параметров самому шаблонному классу, а не его конструктору. Поэтому тип может быть только выведен из переданных конструктору параметров. Но добавлять специальный параметр конструктора, служащий только для выведения типа, я тоже не хотел: загромождение списка аргументов чисто служебным параметром. А если пользователь забудет его передать, например, посредством хотя бы банального (DerivedClass *)nullptr? Это ещё не страшно компилятор выведет сообщение об ошибке, что не может инстанцировать класс. Хуже, если пользователь создаст иерархию классов и передаст указатель не того производного класса: всё будет с точки зрения компиляции верно, однако получим неверно работающую программу с непонятной ошибкой.
Короче, это просчёт проектирования такое решение. Таким образом перекладывается ответственность за правильное инстанцирование даже не на создателя производного класса, а на того, кто будет им пользоваться! А тот может быть ни сном, ни духом относительно таких нюансов и искренне не понимать, где находится ошибка.
В конечном итоге, сдавшись, я решил всё же, не меняя параметров конструктора, параметризовать всё же сам WindowClass и заодно с ним связанные классы списка и узла списка.
Шаблонный класс WindowClass:
template<class WndCls> struct ListElement//узел списка{//данные узлаHWND hWnd;//дескриптор окна WindowsWindowClass<WndCls> *p_wndClass;//указатель на объект класса WindowClassListElement *pNext;//указатель на следующий элемент спискаListElement *pPrev;//указатель на предыдущий элемент списка};template<class WndCls> class WindowClass//класс окна Windows{using WndProcCallback = LRESULT (*)(HWND, UINT, WPARAM, LPARAM);//тип функции оконной процедурыprotected://изменение для производных классов!//данныеHWND hWnd = NULL;//дескриптор класса окнаWNDCLASSEX wc = { 0 };//структура для регистрации класса окна внутри Windowsconst TCHAR *szWndTitle = nullptr;//заголовок окнаstatic const TCHAR *szWndTitleDefault;//строка заголовка по умолчаниюstatic List<WndCls> wndList;//статический список, единый для всех классов//функцииbool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr);//инициализирует и создаёт окно (вызывается из конструкторов)static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);//оконная процедура (статическая функция)template<class T, typename = T::OnCreate> void LaunchOnCreate(HWND hWnd, T *p_wndClass)//ошибка! см. проект FirstWin32CPP_DerivedTemplate2{//выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нёмT::OnCreate(hWnd);}template<class T> void LaunchOnCreate(HWND hWnd, T *p_wndClass)//выполняет запуск OnCreate с помощью механизма виртуальных функций по указателю на класс{p_wndClass->OnCreate(hWnd);//запуск с помощью механизма виртуальных функций}void OnCreate(HWND hWnd);//обеспечивает обработку WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd);//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnClose(HWND hWnd);//обеспечивает обработку WM_CLOSE внутри оконной процедурыvirtual void OnDestroy(HWND hWnd);//обеспечивает обработку WM_DESTROY внутри оконной процедуры//привилегированные классыfriend List<WndCls>;public://функцииWindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор для инициализации класса по умолчаниюWindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClass(WindowClass&);//конструктор копированияvirtual ~WindowClass();//виртуальный деструктор};

Производный класс:
class WindowClassDerived : public WindowClass<WindowClassDerived>{static unsigned short int usiWndNum;//количество объектов классаpublic:WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);WindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);WindowClassDerived(WindowClassDerived&);//конструктор копированияvirtual ~WindowClassDerived() override;//виртуальный деструкторvoid OnCreate(HWND hWnd);//обеспечивает обработку WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd) override;//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnDestroy(HWND hWnd) override;//обеспечивает обработку WM_DESTROY внутри оконной процедуры};

Оконная процедура, будучи шаблонным членом шаблонного класса и имея доступ к переданному типу производного класса, вызывает OnCreate производного класса.
Вот мы и приходим естественным образом к шаблону CRTP. Здесь он получился сам собой. Только много позже я узнал, что эта конструкция известный шаблон с соответствующим именем. Но тогда я этого не знал, и мне казалось, что я получил его впервые.
Уже сразу я понял, что это только половина решения. Я ведь легко могу захотеть создать ещё один класс на основе этого производного. А всё: он не шаблонный и больше не принимает никаких параметров. Так я пришёл к идее передачи второго производного класса через первый производный в базовый. (Тонкий лёд под моими ногами стал давать трещину Я уже шёл туда, откуда нет возврата.) Но если я сделаю это один раз, я смогу делать так сколько угодно: даже если у меня будет десять производных классов, я смогу десятый по счёту (самый последний) передать по цепочке в базовый, и он вызовет там нужную мне функцию этого последнего производного (а вообще говоря и любого промежуточного при желании). Задача была ясна. Оставалось только это сделать.

Параметризированный класс окна способ 2


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

Разумеется, для соблюдения указанных требований шаблонным придётся всё-таки сделать конструктор и всё-таки добавить в него спецпараметр. Однако это означает нарушение другого требования.
Какой здесь выход?
Можно разделить исходный базовый класс WindowClass на две составляющие: сам WindowClass (назовём его теперь WindowClassBase), представляющий собой единую незыблемую основу, и дополняющий его производный класс (который можно назвать всё тем же первоначальным именем WindowClass).
Дополняющий класс отвечает за реализацию OnCreate, и, кроме того, его можно параметризировать самого целиком. А он в своём конструкторе паредаст переданный ему тип через спецпараметр в конструктор класса WindowClassBase.
В любом случае, в WindowClassBase относительно исходного теперь придётся внести некоторые изменения. Во-первых, помимо собственно удаления из него OnCreate придётся добавить член-указатель на дополняющий его класс (и, в будущем, производные от него), а также функцию вызова, вызывающую OnCreate по этому указателю: мы не можем вызвать по указателю на базовый, потому что OnCreate в нём уже нет, а OnCreate дополняющего и производных от него классов лучше всё же вызывать по правильному указателю на нужный класс, а не пытаться что-то нахимичить с указателем this базового. В конечном итоге, спецпараметр конструктора WindowClassBase будет нужен не только для вывода типа, но и для сохранения с последующим обращением через него к OnCreate нужного класса.
К сожалению, тип этого указателя пришлось сделать void:
  • класс не шаблонный, и указать компилятору создать указатель с неизвестным типом нельзя;
  • от базового класса наследуются множество производных, у всех них разный тип какой тип указателя использовать?

В конечном итоге я просто объявил его в стиле С: в любой непонятной ситуации используй указатель на void. Указатель физически хранится как на бестиповый, но в момент вызова OnCreate приводится к типу вызываемого класса. Делается это в специальной шаблонной функции вызова, которая принадлежит WindowClassBase и тип-параметр которой на момент вызова известен:
template<class WndCls> void LaunchOnCreate(HWND hWnd){//выполняет запуск OnCreate для класса WndCls, если OnCreate определена в нёмif (p_drvWndCls)(static_cast<WndCls *>(p_drvWndCls))->WndCls::OnCreate(hWnd);}

(Первоначально в качестве второго параметра применялся std::true_type или std::false_type для выбора нужного варианта переопределения функции. Используя метод SFINAE, выяснялось на этапе компиляции, имеет ли класс WndCls функцию-член OnCreate. Если имеет, то вызывается вышеприведённый вариант функции. Если не имеет, то обращение к OnCreate производилось в виде:
(static_cast<WndCls *>(p_drvWndCls))->OnCreate(hWnd);

Впоследствии выяснилось, что в SFINAE нет необходимости: класс, дополняющий WindowClassBase, в любом случае имеет функцию-член OnCreate, потому, даже если переданный класс-параметр WndCls не имеет определённой в нём OnCreate, она есть в одном из базовых по отношению к нему классов, и проверка даст true во всех случаях. Если же каким-то чудом дополняющий класс будет изменён так, что OnCreate будет из него удалена, и во всех производных от него классах её тоже не будет, то тогда нет никакого смысла вызывать её по второму варианту: такой код просто компилироваться не будет. Потому в конечном итоге здесь приведён вышеприведённый вариант.)
Логика приёма и использования типа базового класса в WindowClassBase достаточно проста: тип выводится из указателя на объект производного класса, передаваемый конструктору WindowClassBase, в этом конструкторе этот указатель сохраняется, а переданным типом инстанцируется указатель на шаблонную оконную процедуру, а из неё происходит обращение к вышеуказанной LaunchOnCreate.
Таким образом, класс WindowClassBase примет теперь такой вид:
class WindowClassBase//класс окна Windows{protected://изменение для производных классов!//данныеHWND hWnd = NULL;//дескриптор класса окнаWNDCLASSEX wc = { 0 };//структура для регистрации класса окна внутри Windowsconst TCHAR *szWndTitle = nullptr;//заголовок окнаvoid *p_drvWndCls;//указатель на производный класс, дополняющий этот основной (т.к. шаблонные данные-члены допустимы только//статические, то используем (по старинке) указатель без типа, т.е. указатель на voidstatic const TCHAR *szWndTitleDefault;//строка заголовка по умолчаниюstatic List wndList;//статический список, единый для всех классов//функцииbool CreateWnd(WNDCLASSEX& wc, bool bSkipClassRegister = false, const TCHAR *szWndTitle = nullptr);//инициализирует и создаёт окно (вызывается из конструкторов)template<class WndCls> void LaunchOnCreate(HWND hWnd){//выполняет запуск OnCreate для класса WndClsif (p_drvWndCls)(static_cast<WndCls *>(p_drvWndCls))->WndCls::OnCreate(hWnd);}virtual void OnPaint(HWND hWnd);//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnClose(HWND hWnd);//обеспечивает обработку WM_CLOSE внутри оконной процедурыvirtual void OnDestroy(HWND hWnd);//обеспечивает обработку WM_DESTROY внутри оконной процедуры//привилегированные классы и функцииfriend List;template<class WndCls> friend LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);//оконная процедураpublic://функцииtemplate<class WndCls> WindowClassBase(WndCls *p_wndClass, HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор для инициализации класса по умолчаниюtemplate<class WndCls> WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassBase(WindowClassBase&);//конструктор копированияvirtual ~WindowClassBase();//виртуальный деструктор};

Ну и приведу код самого короткого конструктора:
template<class WndCls> WindowClassBase::WindowClassBase(WndCls *p_wndClass, WNDCLASSEX& wc, const TCHAR *szWndTitle){//создаём окно, инициализируя его параметрами, переданными через wc//на вход: p_wndClass - указатель на производный класс, по типу которого будет выводиться тип шаблонного конструктора, wc - ссылка на структуру класса//окна для регистрации внутри Windows, szWndTitle - строка заголовка окнаWindowClassBase::wc = wc;WindowClassBase::wc.lpfnWndProc = WndProc<WndCls>;WindowClassBase::szWndTitle = szWndTitle;p_drvWndCls = p_wndClass;//сохраняем указатель на производный класс, чтобы вызывать OnCreate() этого класса при обработке сообщения WM_CREATE//создаём окноCreateWnd(WindowClassBase::wc, false, szWndTitle);}

Внутри же оконной процедуры обращение к LaunchOnCreate происходит так:
p_wndClass->LaunchOnCreate<WndCls>(hWnd);

Саму оконную процедуру решил вынести из класса вовне, объявив её привилегированной в классе WindowClassBase. Возможно, в этом не имело особого смысла: какая разница, где плодить её инстанцирования вовне или внутри класса? Сегмент кода-то один! Хотя, признаю, с точки зрения той же инкапсуляции, возможно, следовало всё же оставить её внутри класса статической.
Осталось определить дополняющий класс:
class WindowClass : public WindowClassBase//класс, дополняющий WindowClassBase до полноценно функционирующего класса{public://конструктор для инициализации класса по умолчаниюWindowClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(this, hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(this, wc, szWndTitle) {}virtual void OnCreate(HWND hWnd) {}//обеспечивает обработку WM_CREATE внутри оконной процедуры};

Класс имеет конструктор, имеющий такой же вид, как и у исходного WindowClass до разделения, то есть без спецпараметра, а этот спецпараметр генерируется внутри при обращении к конструктору WindowClassBase передачей указателя this.
Этот WindowClass в такой форме это практически эквивалент исходного WindowClass. В таком виде он не поддерживает наследование с переопределением OnCreate. Тем не менее, это исходная отправная точка для поддержки наследования (как будет показано ниже). В таком виде:
  • базовый класс WindowClassBase не является шаблонным сам по себе, а это значит, что он будет единственным для всех производных классов, какие бы они ни были; список List для обеспечения корректной обработки всех остальных сообщений Windows также будет единственным;
  • конструктор WindowClass не имеет лишнего спецпараметра.

Как видим, два требования из трёх удовлетворены. Осталось разобраться с последним: с наследованием.

Цепочечная передача типа производного класса в WindowClassBase, контрольный тип


Рассмотрим для начала однократное наследование, когда логика инициализации WindowClass нас не устраивает, и мы хотим изменить её через создание производного класса (пока хотя бы одного). Что нужно изменить в WindowClass для обеспечения этого?
Новый вариант дополняющего класса становится шаблонным. Это не страшно, поскольку он фактически не содержит никаких данных, а только функцию OnCreate и конструкторы:
template<class DerWndCls> class WindowClassTemplate : public WindowClassBase{public://конструктор для инициализации класса по умолчаниюWindowClassTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerWndCls *>(this), hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerWndCls *>(this), wc, szWndTitle) {}virtual void OnCreate(HWND hWnd) {}//обеспечивает обработку WM_CREATE внутри оконной процедуры};

Этот класс принимает параметр типа DerWndCls и, преобразуя к нему указатель this, передаёт в WindowClassBase.
Обратите внимание на static_cast. Это важно, потому что первоначально у меня преобразование было написано в стиле С так:
template<class DerWndCls> class WindowClassTemplate : public WindowClassBase{public://конструктор для инициализации класса по умолчаниюWindowClassTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase((DerWndCls *)this, hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase((DerWndCls *)this, wc, szWndTitle) {}virtual void OnCreate(HWND hWnd) {}//обеспечивает обработку WM_CREATE внутри оконной процедуры};

После того, как я перевёл его всюду на static_cast, половина кода (см. далее) не скомпилировалась.
Это тоже тонкий момент: преобразование выполняется на стадии компиляции, но этот класс уже сам по себе имеет функцию OnCreate, а после преобразования к DerWndCls можно обратиться к OnCreate уже класса DerWndCls. В этом разница от описанного выше случая преобразования внутри WindowClassBase.
Таким образом, можно создать некий класс WindowClassDerived, в нём переопределить OnCreate и инстанцировать им описанный выше WindowClassTemplate, снова реализуя тот самый указанный в начале статьи исходный странно повторяющийся шаблон:
class WindowClassDerived : public WindowClassTemplate<WindowClassDerived>{static unsigned short int usiWndNum;//количество объектов классаpublic:WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор для инициализации класса по умолчаниюWindowClassDerived(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassDerived(WindowClassDerived&);//конструктор копированияvirtual ~WindowClassDerived() override;//виртуальный деструкторvirtual void OnCreate(HWND hWnd) override;//обеспечивает обработку WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd) override;//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnDestroy(HWND hWnd) override;//обеспечивает обработку WM_DESTROY внутри оконной процедуры};

И OnCreate этого WindowClassDerived будет вызываться внутри WindowClassBase, что и требовалось!
WindowClassDerived::WindowClassDerived(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle) : WindowClassTemplate(hInstance, szClassName, szWndTitle){usiWndNum++;//увеличиваем количество объектов данного класса}

Но это однократное наследование. При многократном наследовании следует вместо WindowClassDerived, в свою очередь, объявить новый шаблон, потенциально принимающий класс уровнем выше в иерархии и передающий его в WindowClassTemplate. Конкретно выделю два ключевых момента:
  1. Потенциально принимающий класс уровнем выше в иерархии. Это означает, что может и не принимать никакого класса, то есть сам быть тем самым верхним классом иерархии так, чтобы из него можно было создать объект.
  2. Передающий параметр в WindowClassTemplate. Это означает, что принятый аргумент шаблона необходимо передать дальше от класса к классу через всю цепочку наследований в самый низ, в WindowClassTemplate и оттуда в WindowClassBase.

То есть, с одной стороны, класс должен быть шаблонным и принимать некий класс как параметр. С другой стороны, он должен отслеживать ситуацию, что сам является конечным (на момент инстанцирования) классом, и инстанцировать базовый класс собой, а не переданным типом.
При всём при этом хотелось бы, чтобы всё это выполнялось автоматически компилятором: определение нового класса на основе уже созданного не потребует какой-либо переделки последнего тогда вся суть наследования-полиморфизма теряется. То есть: я создаю класс, который в настоящий момент находится в вершине иерархии, но потом, может быть, будет создан новый класс на основе этого, который заместит текущий без изменения его определения.
Как реализовать эту функциональность?
Для решения проблемы автоматизации и интеллектуального принятия решения само собой напрашивается вариант аргумента по умолчанию для шаблона: если текущий создаваемый класс самый верхний, и ему не передаётся параметр шаблона, то мы должны этот параметр ему назначить. Выполняется это с помощью аргумента по умолчанию. Тогда возникают следующие вопросы: каким его выбрать и как соотнести с ситуацией переданного явно параметра, а также передачи себя в случае, если параметр не передан?
К сожалению, нельзя в качестве параметра по умолчанию написать собственный же определяемый класс. Компилятор просто не пропустит код вида:
template<class DerWndCls = WindowClassDerived<>> class WindowClassDerived : public WindowClassTemplate<DerWndCls>

Он сообщает, что рекурсивная зависимость типа слишком сложна.
Зайдём с другой стороны. Введём некий фиктивный класс, ничего функционально не выполняющий и ничего не хранящий, играющий роль лишь пустышки-затычки и сигнализирующий компилятору, что в случае его появления не происходит передача ничего сверху:
class thisclass {};//класс-пустышка, используемый для аргумента по умолчанию

И в аргументе по умолчанию вместо себя подставим эту затычку:
template<class DerWndCls = thisclass> class WindowClassDerived : public WindowClassTemplate<DerWndCls>

При таком варианте в ситуации с аргументом по умолчанию происходит передача thisclass в WindowClassTemplate. Класс thisclass не имеет функции-члена OnCreate, так что такой вариант просто не скомпилируется.
Попробуем тогда ввести второй, вспомогательный контрольный параметр, на основании которого будем принимать решение, какой тип передавать дальше. Для этого, разумеется, нужно изменить WindowClassTemplate, например, так:
template<class DerWndCls, class ControlType> class WindowClassControlBaseTemplate : public WindowClassBase{//если передаётся ControlType == thisclass, то тогда нужно использовать сам DerWndCls, в котором передаётся класс, передаваемый напрямую WindowClassBase//если же ControlType != thisclass, тогда следует использовать ControlType, эквивалентный классу в вершине иерархии наследования класса (при правильно//соблюдённых соглашениях о передаче ControlType вершинным и нижележащими базовыми классами)using DerivedWndClassType = std::conditional_t<std::is_same<ControlType, thisclass>::value, DerWndCls, ControlType>;public://конструктор для инициализации класса по умолчаниюWindowClassControlBaseTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassControlBaseTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), wc, szWndTitle) {}virtual void OnCreate(HWND hWnd) {}//обеспечивает обработку WM_CREATE внутри оконной процедуры};

В него передаётся не один тип, а два. На основании комбинации этих двух типов определяется конечный тип с помощью средств <type_traits>: std::conditional_t и std::is_same. Именно этот тип и передаётся дальше в WindowClassBase. Логика выбора описывается в комментариях: если передаётся в ControlType thisclass, то тогда мы выбираем DerWndCls, в противном случае выбирается сам ControlType.
Теперь построим шаблон, его использующий при наследовании:
template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WndClsDerivedTemplateClass : public WindowClassControlBaseTemplate<WndClsDerivedTemplateClass<DerWndCls>, ControlType>//строим новый класс на основе предыдущего производного{protected:static unsigned short int usiWndNum;//количество объектов классаpublic://конструктор для инициализации класса по умолчаниюWndClsDerivedTemplateClass(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr);//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWndClsDerivedTemplateClass(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr);WndClsDerivedTemplateClass(WndClsDerivedTemplateClass&);//конструктор копированияvirtual ~WndClsDerivedTemplateClass() override;//виртуальный деструкторvirtual void OnCreate(HWND hWnd) override;//обеспечивает обработку WM_CREATE внутри оконной процедурыvirtual void OnPaint(HWND hWnd) override;//обеспечивает обработку WM_PAINT внутри оконной процедурыvirtual void OnDestroy(HWND hWnd) override;//обеспечивает обработку WM_DESTROY внутри оконной процедуры};

Первый параметр по умолчанию инициализируется через thisclass, а ControlType вычисляется на основе самого DerWndCls: если DerWndCls = thisclass, то ControlType := thisclass, иначе ControlType := DerWndCls (специально указал присваивание в стиле Pascal, чтобы отличить от сравнения).
Дальше же передаваться будет сам же класс WndClsDerivedTemplateClass, параметризированный DerWndCls, вместе с вычисленным (на этапе компиляции) контрольным типом.
Если мы создаём объект этого класса, то есть WndClsDerivedTemplateClass сам является вершиной иерархии, то тогда DerWndCls = ControlType = thisclass, и дальше происходит передача <WndClsDerivedTemplateClass, thisclass>. Тот факт, что WndClsDerivedTemplateClass параметризуется пустышкой, не имеет никакого значения этот тип, да и вообще любой переданный на месте DerWndCls, никак не используется внутри класса: из него не создаётся никакого объекта и не вызывается через него никакая функция. Потому формально WndClsDerivedTemplateClass можно инстанцировать буквально чем угодно тип-параметр служит лишь для передачи дальше по линии наследования. Но вот то, что вместо DerWndCls дальше был передан WndClsDerivedTemplateClass< thisclass или любой другой тип>, имеет значение: WndClsDerivedTemplateClass имеет функцию OnCreate, которая будет вызываться внутри WindowClassBase.
При таком варианте в WindowClassControlBaseTemplate на месте ControlType приходит thisclass, и конечный тип выводится как DerWndCls = WndClsDerivedTemplateClass, имеющий нужную функцию OnCreate. Что нам и нужно.
Рассмотрим теперь вариант, когда строится новый класс на базе WindowClassControlBaseTemplate (дальнейшее наследование):
template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>>

В этом случае в WndClsDerivedTemplateClass на место DerWndCls приходит нечто, отличное от thisclass, и ControlType, увидев это отличие, принимает значение переданного DerWndCls.
Тогда в WindowClassControlBaseTemplate идёт следующий вариант параметризации: <WndClsDerivedTemplateClass< WindowClassDerivedTemplateNext>, WindowClassDerivedTemplateNext>.
В WindowClassControlBaseTemplate, в свою очередь, поскольку ControlType != thisclass, то используется сам ControlType, равный WindowClassDerivedTemplateNext, который как раз и является нужным классом для выбора OnCreate.
На первый взгляд при такой схеме всё вроде бы хорошо. Но это не так. Построим ещё один класс на основе последнего:
template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext2 : public WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2<DerWndCls>>

В WindowClassDerivedTemplateNext на место DerWndCls придёт WindowClassDerivedTemplateNext2. ControlType выведется также как WindowClassDerivedTemplateNext2. Затем в WndClsDerivedTemplateClass будет передано WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2>, и в нём ControlType выведется как этот же WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2>. Далее в WindowClassControlBaseTemplate будут переданы эти же значения, и там вместо правильного WindowClassDerivedTemplateNext2<WindowClassDerivedTemplateNext> будет использован WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2>, и будет вызвана функция OnCreate именно класса WindowClassDerivedTemplateNext, а не WindowClassDerivedTemplateNext2.
Напоминаю, что при такой схеме наследования и передачи параметров важен тип самого класса, который пришёл в итоге в WindowClassControlBaseTemplate, а не то, чем он там параметризован.
Следовательно, чтобы тип, для которого будет вызываться OnCreate, выводился правильно, нужно изменить определение класса WindowClassDerivedTemplateNext:
template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>, ControlType>

В таком случае дальше в WndClsDerivedTemplateClass на место ControlType будет передаваться верное значение, равное WindowClassDerivedTemplateNext2 вместо того, чтобы он выводился там в неверное значение.
Таким образом, последний класс, который мы строим, не должен передавать ControlType, давая возможность ближайшему базовому вывести его самостоятельно, а этот базовый и все нижележащие должны передавать ControlType явно, запрещая его автоматический вывод в неверное значение. Такой подход подразумевает изменение определения ближайшего базового класса, что возможно только в том случае, если у нас в наличии имеются его исходный текст либо мы ранее строили его самостоятельно.
Если мы забыли это сделать и нарушили это правило, то при использовании static_cast получим ошибку компиляции, а в случае преобразования указателей в стиле С внутри WindowClassControlBaseTemplate получим неверно работающую программу. Например, если мы попробуем создать объект для класса
template<class DerWndCls = thisclass, class ControlType = std::conditional_t<std::is_same<DerWndCls, thisclass>::value, thisclass, DerWndCls>> class WindowClassDerivedTemplateNext : public WndClsDerivedTemplateClass<WindowClassDerivedTemplateNext<DerWndCls>, ControlType>

то компилятор выдаст ошибку: он не сможет преобразовать типы указателей внутри WindowClassControlBaseTemplate за счёт того, что тип был передан неверный, для которого нельзя выполнить такое преобразование (поскольку мы собираемся создавать объект класса WindowClassDerivedTemplateNext, то для него считаем, что сам класс WindowClassDerivedTemplateNext находится в вершине иерархии, а в этом случае, как было показано выше, ControlType передавать не следует). Без static_cast код скомпилируется и просто будет вызвана OnCreate не того класса. Однако удаление передачи ControlType делает программу снова компилируемой.
В конечном итоге, всё это слишком сложно, ненадёжно и требует наличия исходных текстов всех классов. Кроме того, мы можем создавать объекты только последнего производного класса, а какого-либо из его базовых нельзя из-за передачи ControlType (либо можем в случае передачи указателя в стиле С, но эти объекты будут неверно инициализироваться). Нужно другое решение, более простое и надёжное.

Вариативный шаблон


Тем не менее, вышеприведённый вариант шаблонного наследования и передачи типа создаваемого объекта в класс WindowClassBase, где происходит создание окна и вызов OnCreate, имеет серьёзные недостатки. Нужен какой-либо иной, более надёжный и работоспособный вариант.
С++11 представляет новый тип шаблона: шаблон с переменным числом аргументов, или вариативный шаблон. Его параметры представляют собой последовательность из типов заранее неизвестной длины. Вместо рискованных манипуляций с контрольным типом в предыдущем примере я решил пойти другим путём: чтобы избежать ситуаций, когда промежуточный в иерархии класс замещает собой вышестоящий класс по иерархии через неверную параметризацию (в примере выше это был WindowClassDerivedTemplateNext<WindowClassDerivedTemplateNext2>), можно вообще обойтись от подобного типа параметризации, просто ставя эти классы в последовательности рядом. Например, при трёх последовательных наследованиях в параметрах шаблона в конечном итоге сформируется такой список:
<WndCls3<>, WndCls2<>, WndCls1<>>
Обрабатывая этот список, точнее, один из конечных его элементов (в зависимости от того, как составляли), можно извлечь нужный класс в иерархии и работать с ним.
В этом случае вместо описанных ранее шаблонов WindowClassTemplate и WindowClassControlBaseTemplate, наиболее близких к корневому WindowClassBase и составляющих основу для всех остальных наследований, следует написать новый вариативный шаблонный класс. В самом простом варианте он будет таким:
//реализация, когда нужный нам класс расположен последнимtemplate<class... Classes> class WindowClassVariadicTemplate;//общее объявление класса//специализация, при которой первый в списке параметров класс отделяется от остальныхtemplate<class DerWndCls, class... OtherWindowClasses> class WindowClassVariadicTemplate<DerWndCls, OtherWindowClasses...> : public WindowClassBase{//просто извлекаем самый первый класс в списке: нужный нам класс - DerWndClsusing DerivedWndClassType = DerWndCls;public://конструктор для инициализации класса по умолчаниюWindowClassVariadicTemplate(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassVariadicTemplate(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassBase(static_cast<DerivedWndClassType *>(this), wc, szWndTitle) {}virtual void OnCreate(HWND hWnd) {}//обеспечивает обработку WM_CREATE внутри оконной процедуры};

Сначала объявляется общее описание шаблона класса без тела. Затем определяется его специализация, в которой первый тип отделён от остальных. Именно он нам и интересен. Это справедливо для случая, когда при движении по цепочке иерархии вниз к WindowClassBase каждый очередной класс помещает себя в конец списка параметров. Тогда нужный нам класс будет в начале, и его очень просто отделить от остальных. Можно поступить по-другому: каждый новый класс будет помещать себя в начало списка параметров шаблона. Тогда класс в вершине иерархии будет самым последним в списке, и извлечь его оттуда намного сложнее. В данном конкретном случае эти два подхода совершенно идентичны, но первый намного проще в реализации (в том числе во время компиляции не придётся обрабатывать весь список, извлекая последний элемент из него), и именно он приведён выше.
Первый элемент, являющий самым высшим классом в иерархии, извлекается из списка и передаётся в WindowClassBase. Если для него определена OnCreate, она и будет вызвана. В противном случае будет вызвана OnCreate ближайшего базового класса по отношению к нему. Если вариативный список параметров оказался пустым (мы пытаемся создать объект из WindowClassVariadicTemplate), то компиляция завершится неудачей, требуя наличия хотя бы одного типа в списке параметров.
Первый класс на основе WindowClassVariadicTemplate будет таким:
template<class... PrevWndClasses> class WindowClassVariadic1 : public WindowClassVariadicTemplate<PrevWndClasses..., WindowClassVariadic1<>>{protected:static unsigned short int usiWndNum;//количество объектов классаpublic://конструктор для инициализации класса по умолчаниюWindowClassVariadic1(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassVariadicTemplate(hInstance, szClassName, szWndTitle){usiWndNum++;//увеличиваем количество объектов данного класса}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassVariadic1(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassVariadicTemplate(wc, szWndTitle){usiWndNum++;//увеличиваем количество объектов данного класса}WindowClassVariadic1(WindowClassVariadic1& wcObj) : WindowClassVariadicTemplate(wcObj)//конструктор копирования{usiWndNum++;//увеличиваем количество объектов данного класса}virtual ~WindowClassVariadic1() override//виртуальный деструктор{if (hWnd)this->OnClose(hWnd);//закрываем окно, используя механизм виртуальных функций}virtual void OnCreate(HWND hWnd) override//обеспечивает обработку WM_CREATE внутри оконной процедуры{//обеспечивает обработку WM_CREATE внутри оконной процедурыSetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255)));}virtual void OnPaint(HWND hWnd) override//обеспечивает обработку WM_PAINT внутри оконной процедуры{...}virtual void OnDestroy(HWND hWnd) override//обеспечивает обработку WM_DESTROY внутри оконной процедуры{...}};

Этот класс, приняв неопределённый список параметров PrevWndClasses, передаёт его дальше базовому классу, вставив себя перед ним в качестве первого элемента с пустым списком параметров. Поскольку сам этот класс WindowClassVariadic1 является вариативным, то WindowClassVariadic1<> также будет вариативным, хоть и без параметров, и вся эта последовательность классов фактически есть вариативный шаблон, каждый элемент которого является также вариативным шаблоном.
Следующий производный класс имеет вид:
template<class... PrevWndClasses> class WindowClassVariadic2 : public WindowClassVariadic1<PrevWndClasses..., WindowClassVariadic2<>>{...};

За исключением изменения имени производного и базового, класс имеет точно такой же вид, как и предыдущий. Следующий класс аналогично:
template<class... PrevWndClasses> class WindowClassVariadic3 : public WindowClassVariadic2<PrevWndClasses..., WindowClassVariadic3<>>{...};

В этом и заключается смысл полиморфного многократного наследования: объявив класс таким образом, мы гарантируем не только создание объектов данного типа, но и всех объектов всех остальных классов, производных от него, сколько бы и какими бы они ни были в будущем. При этом в WindowClassBase всегда будет вызвана правильная OnCreate.
Таким образом, этот вариативный шаблон является первым рабочим способом решения проблемы вызова OnCreate при создании окна, полностью удовлетворяющем всем поставленным ранее требованиям.
Забегая вперёд, где в конечном итоге был найден более лучший в данной ситуации метод, реализация наследования через вариативный шаблон позволяет реализовать более сложную логику компиляции в WindowClassBase: имея доступ ко всем типам, по которых произошло наследование, можно гибко выбирать среди них нужный по каким-либо критериям и вызывать определённую в нём функцию-член. Но это всё же уже несколько другой случай.

Класс инициализации


Ещё не подозревая о реакции static_cast на производные типы, я продолжал искать другие способы реализации передачи вершинного класса иерархии в WindowClassBase. В какой-то момент подумал о том, чтобы вывести реализацию OnCreate в отдельный класс, специально для неё созданного:
class WindowClassInit1{public:void OnCreate(HWND hWnd)//обеспечивает обработку WM_CREATE внутри оконной процедуры{//обеспечивает обработку WM_CREATE внутри оконной процедурыSetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255)));}};

Этим классом параметризуется другой класс, реализующий все остальные переопределения для виртуальных функций. Он является производным от уже описанной WindowClassTemplate:
template<class WndClsInit = WindowClassInit1> class WindowClassDerivedI1 : public WindowClassTemplate<WndClsInit>{...};

Таким образом:
  • наследование классов происходит как обычно для виртуальных функций;
  • происходит передача от класса к классу по цепочке наследования только класса инициализации, специально определённого для реализации OnCreate.

Если данный класс расположен в вершине иерархии, то параметр WndClsInit станет равным WindowClassInit1 определённого для этого класса классу инициализации, и произойдёт передача его дальше по цепочке иерархии. Если же этот класс промежуточный в цепочке, то он просто примет переданный ему класс и передаст его дальше. Затем, что такой вариант выгодно отличается от предыдущих тем, что в шаблонах происходит не передача себя, а передача некоторого стороннего класса, что реализуется (и выглядит) намного проще. Шаблон в такой форме также подходит без изменений для реализации всей цепочки наследования: будет происходить только смена названий классов.
Тем не менее, static_cast, в отличие от преобразования в стиле С, внутри WindowClassTemplate не пропустит такую форму наследования: он просто не сможет преобразовать при передаче this от (WindowClassTemplate *) к (WindowClassInit1 *). И это логично: WindowClassInit1 фактически посторонний класс, просто переданный как тип в эту точку, он никак не связан с WindowClassTemplate и всей цепочкой производных от него, потому преобразование указателя к нему недопустимо.

Цепочечная передача типа производного класса в WindowClassBase, условная передача


Ну и наконец был найден самый лучший для данной ситуации способ передачи типа производного класса в корневой базовый WindowClassBase через всю цепочку наследования, лишённый недостатков предыдущих и при этом проще, чем вариативный шаблон. Определим следующий шаблонный класс на основе WindowClassTemplate:
template<class DerWndCls = thisclass> class WindowClassDerivedAlternative1 : public WindowClassTemplate<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative1<>, DerWndCls>>{public://конструктор для инициализации класса по умолчаниюWindowClassDerivedAlternative1(HINSTANCE hInstance, const TCHAR *szClassName, const TCHAR *szWndTitle = nullptr) : WindowClassTemplate(hInstance, szClassName, szWndTitle) {}//конструктор, принимающий ссылку на структуру типа WNDCLASSEX для регистрации окна с настройками, отличными от по умолчаниюWindowClassDerivedAlternative1(WNDCLASSEX& wc, const TCHAR *szWndTitle = nullptr) : WindowClassTemplate(wc, szWndTitle) {}virtual ~WindowClassDerivedAlternative1() override//виртуальный деструктор{if (hWnd)this->OnClose(hWnd);//закрываем окно, используя механизм виртуальных функций}virtual void OnCreate(HWND hWnd) override//обеспечивает обработку WM_CREATE внутри оконной процедуры{//обеспечивает обработку WM_CREATE внутри оконной процедурыSetClassLongPtr(hWnd, GCL_HBRBACKGROUND, (LONG)CreateSolidBrush(RGB(200, 160, 255)));}virtual void OnPaint(HWND hWnd) override//обеспечивает обработку WM_PAINT внутри оконной процедуры{//обеспечивает обработку WM_PAINT внутри оконной процедурыHDC hDC;PAINTSTRUCT ps;RECT rect;hDC = BeginPaint(hWnd, &ps);GetClientRect(hWnd, &rect);DrawText(hDC, TEXT("Шаблонный переопределённый класс с условной передачей параметра (ПЕРВОЕ наследование)."), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);EndPaint(hWnd, &ps);}};

Этот класс принимает в качестве параметра DerWndCls, который по умолчанию приравнивается к thisclass. При передаче происходит сравнение DerWndCls с thisclass: в случае равенства (значение по умолчанию, то есть данный класс находится в вершине иерархии) происходит передача себя с пустым списком параметров. В противном случае дальше передаётся принятый DerWndCls.
Это решение я считаю наилучшим в данной ситуации по всем параметрам:
  • единая форма определения класса для всей цепочки наследования;
  • простая и прозрачная логика передачи класса по всей цепочке наследования;
  • нет накладных расходов из-за вариативного шаблона (в тех случаях, как в данном, когда это и не требуется).


Страшное возмездие


Что всё это означает? Это значит, что если вы хотите использовать такую нетрадиционную форму наследования, вы все свои классы должны оформлять строго определённым образом, чтобы они допускали передачу через себя возможного нового производного. Это весьма нетрудное требование, и при желании его просто соблюдать.
Но есть другой, гораздо более нетривиальный вопрос: соотношение типов и указателей. Писали же умные люди: не надо играться с такими вещами в конструкторе и идти против принципов языка и логики работы компилятора. А я не послушался и всё равно это сделал. Теперь наступает закономерное возмездие.
Итак, у нас есть 4 класса:
template<class DerWndCls = thisclass> class WindowClassDerivedAlternative1 : public WindowClassTemplate<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative1<>, DerWndCls>>{};template<class DerWndCls = thisclass> class WindowClassDerivedAlternative2 : public WindowClassDerivedAlternative1<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative2<>, DerWndCls>>{};template<class DerWndCls = thisclass> class WindowClassDerivedAlternative3 : public WindowClassDerivedAlternative2<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative3<>, DerWndCls>>{};template<class DerWndCls = thisclass> class WindowClassDerivedAlternative4 : public WindowClassDerivedAlternative3<std::conditional_t<std::is_same<DerWndCls, thisclass>::value, WindowClassDerivedAlternative4<>, DerWndCls>>{};

Как я писал выше, конкретное их содержимое и логика работы совершенно неважны. Важно лишь то, что в заголовке определения класса. На основании этих классов мы создаём 4 объекта:
WindowClassDerivedAlternative1<> w1(hInstance, TEXT("WindowClassDerivedAlternative1"), TEXT("WindowClassDerivedAlternative1"));WindowClassDerivedAlternative2<> w2(hInstance, TEXT("WindowClassDerivedAlternative2"), TEXT("WindowClassDerivedAlternative2"));WindowClassDerivedAlternative3<> w3(hInstance, TEXT("WindowClassDerivedAlternative3"), TEXT("WindowClassDerivedAlternative3"));WindowClassDerivedAlternative4<> w4(hInstance, TEXT("WindowClassDerivedAlternative4"), TEXT("WindowClassDerivedAlternative4"));

Развернём определения их типов, скрытые за пустыми скобками с помощью аргументов по умолчанию. Тип w1 является WindowClassDerivedAlternative1. Тип w2 равен WindowClassDerivedAlternative2, а его базовый класс WindowClassDerivedAlternative1<WindowClassDerivedAlternative2>. Тип w3 есть WindowClassDerivedAlternative3, его базовый класс WindowClassDerivedAlternative2<WindowClassDerivedAlternative3>, а базовый класс того WindowClassDerivedAlternative1<WindowClassDerivedAlternative3>. Аналогично для четвёртого объекта. Посмотрите на следующую схему:

Создавая каждый новый производный класс на основе некоторого таким образом определённого базового, вы определяете не просто новый класс, а заодно и всю цепочку его базовых заново и целиком. Она будет параллельна к цепочке его же собственного базового класса. У вашего класса будут свои собственные базовые классы, и ни один из них не удастся привести ни к одному из исходных базовых несмотря на то, что код генерации для всех этих классов единый! Это кажется настоящей фантастикой, но это действительно так! Это означает, что все привычные способы манипуляции наследуемых классов и указателей работать не будут! В данной конкретной архитектуре только базовый WindowClassBase спасает положение, в противном случае даже создать массив из базовых классов (например, на основе WindowClassTemplate) также было бы нельзя, потому что у всех таких классов разные типы.
Таким образом, всем известное и понятное определение вида:
WindowClassDerivedAlternative1<> *p2 = &w2;

перестанет компилироваться, потому что вы пытаетесь создать указатель типа, несовместимый с типом объекта w2 несмотря на то, что полчаса назад сами же написали класс, производный от класса WindowClassDerivedAlternative1<> и на основании которого был создан объект w2.
Когда привычные законы перестают работать, это может вызвать шок. И при всём при этом здесь нет на самом деле никаких грязных хаков компилятора, принудительных преобразований типов и прочих по-настоящему нехороших вещей. Всё предельно чисто и законно: шаблоны, параметры по умолчанию и средства библиотеки по работе с типами. Только привычные методы написания кода перестают работать. Использовать такое в реальном проекте это значит объявить там опасную зону, в которую может входить только квалифицированный специализированный персонал с соответствующими мерами защиты.
Допустим, вы взяли в свой проект нового человека, а там вот такое Если этот человек на самом деле далеко не новичок, а опытный специалист, то он, конечно, сможет с помощью подсказок компилятора и IDE, поломав голову, вытянуть суть того, почему тут всё не так, как у людей. Но, поверьте, он будет очень, очень удивляться

Тёмная история


Представьте, что у вас уютный стартап и вы взяли в себе в команду настоящего новичка, на англоязычном сленге именуемого джуном. Вы просите его написать новый производный класс по образцу предыдущего. То, что тут такое страшное и замысловатое определение класса, он может удивиться, но расспрашивать вряд ли станет, потому что про шаблоны пока только читал, а у вас тут крутой стартап, и у него ещё будет время узнать всё это. Он добросовестно создаёт новый класс с несколько изменённым функционалом относительно предыдущего. И вообще он молодец, старается! Затем вы просите его написать функцию с участием объектов его нового класса и базовых, он начинает это делать и обнаруживает, что многие привычные вещи не работают! Он только полгода назад читал всё это в крутой книге, даже успел попробовать на простеньких программках и уже считает себя настоящим профи, который прямо сейчас сходу справится с любой задачей, а тут всё это не работает! Наверняка потом он может начать думать, что у него сломался компилятор. Ну мало ли, плохо установилось, вирус какой и т.д. Он всё проверяет, переустанавливает, а там снова те же ошибки. Разрывы шаблонов, депрессии, истерики, а, вероятно, даже и глубокие запои гарантированы. От него ждут результатов, а он запнулся на элементарных вещах! Тут один из его коллег проникается к нему искренним сочувствием и за стаканчиком крепкого изливает душу, признаваясь, что сам он грешен, но ваш общий тимлид давно курит траву и поощряет странные и нетрадиционные подходы, а вообще весь стартап это сборище таких же отмороженных на всю голову чуваков. Не исключено, что этот джун на следующий же день, протрезвев, побежит в более нормальное место отсюда как от огня, даже не попросив денег за те пару недель, которые успел тут поработать.
(Это не реальная история, это моя буйная фантазия. В промежутке между созданием этих классов и написанием данной статьи от этих страшных классов не пострадал ни один реальный джун.)

Эксперименты с кодом


Чтобы упростить всем интересующимся эксперименты и сэкономить им время на набор кода, я разместил на GitHub все проекты, которые послужили основой этой статьи:
github.com/SkyCloud555/ECRTP
Только выбирайте один проект по очереди в качестве стартового, иначе утонете в море разноцветных окон.

Заключение


Если всё это такая шутка, то, учитывая затраченные усилия, выглядит слишком серьёзно и натурально. А если не шутка, то ни один нормальный разработчик в здравом уме в реальности ничего подобного использовать не будет. И вообще, чувак, не закончить ли тебе страдать ерундой и не пойти бы заняться чем-нибудь, что приносит деньги, полезным.
Вы правы, если так думаете. В этой статье я всего лишь показал, что С++ и так тоже может. Вопрос о практическом применении этих конструкций остаётся открытым. И вообще, это всё скорее уже относится к обобщённому и метапрограммированию. Вам может не потребоваться вообще создавать никаких объектов этих классов, но сами классы могут быть зачем-то нужны. Да и мало ли какие полезные решения можно придумать на этой основе Исходный CRTP-то используется! Причём используется даже не где-нибудь, а прямо в стандартной библиотеке! Кто не верит или не помнит, погуглите std:: еnаblе_shared_from_this.
Возвращаясь же к исходной задаче с окнами Особенно сейчас, трезво и без травы оглядываясь на всё это спустя три года Даже если отбросить тот факт, что я затронул банальную уже миллион раз изъезженную тему, и это давно никому не интересно, потому что для реальных пацанов нормальных людей есть MFC Qt, я бы просто обеспечил передачу в класс окна какой-нибудь функциональный объект. Через цепочку наследований обеспечить его передачу совсем несложно, но зато он всё сделает просто, понятно и без извращений, и получится совершенно нормальный предсказуемый класс без каких-либо побочных эффектов, сопровождать и развивать который можно отдавать абсолютно любому.
То, что получилось в этой статье, это всего лишь интересная нетривиальная задача, которую мне всё-таки удалось решить. Надеюсь, вам это тоже было интересно.
Подробнее..

Как мы обучили сфинкса для голосового помощника

09.07.2020 12:23:49 | Автор: admin
В процессе разработки проекта голосовой помощник одним из требований была возможность распознавания управляющих команд в оффлайн режиме. Это было нужно, так как в противном случае пришлось бы постоянно слушать и посылать поток с аудиоданными на распознавание, получать ответ и анализировать его.

Это весьма накладное решение, которое сложно реализовать из-за постоянной нагрузки на сервер, большого объема трафика и увеличенного время отклика всей системы. Для распознавания управляющих команд в режиме оффлайн мы выбрали Pocketsphinx.



Что случилось


Изначально Pocketsphinx работал у нас на железе OrangePi, у которого были: RAM 512Mb, несколько ГБ свободного дискового пространства, а на борту крутилась Ubuntu. При этом мы использовали python интерфейс (в виде python пакета) для работы с Pocketsphinx.

Мы установили Pocketsphinx командой

`pip install pocketsphinx`

Python позволил использовать нам имеющиеся библиотеки и повысить скорость разработки кода. Однако, даже один только процесс потреблял порядка 30-40 Мб оперативной памяти.

Это могло стать проблемой, потому что выяснилось, что доступное по цене железо заметно скромнее: для операционной системы доступно 123 из 128 Мб RAM, часть которой отводится под нужды железа. Из них часть памяти уже использовалась ОС Linux. Для нашего ПО оставалось только 62 Мб RAM и несколько десятков Мб свободной флеш памяти для хранения кода и файлов. Нужно было искать альтернативу python.

Решили запустить часть с Pocketsphinx на Си.

Сборка Pocketsphinx на Си


Исходники для Pocketsphinx были взяты с сайта: cmusphinx.github.io

Для работы Pocketsphinx требуются обе части Sphinxbase и Pocketsphinx. Далее нужно было понять как кросс-компилировать имеющиеся исходники компилятором для нашей целевой аппаратной платформы.

Сборку кода решено было делать без поддержки python, для чего использовали ключ

`--without-python`

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

`sphinx/lib`

Для дальнейшей работы с Си кодом требуются библиотеки:

`libpocketsphinx.so libsphinxbase.so libsphinxad.so`

Нюансы


После запуска Pocketsphinx с использованием библиотек оказалось, что Си вариант гораздо менее требователен к ресурсам. Использование RAM сводилось буквально к нескольким Mb.

Приложение после запуска занимало не более 5Mb RAM, но в нем при этом использовались еще с десяток других модулей и ряд библиотек. Если в варианте с python загрузка ядра CPU была весьма заметной (50% или даже более), то тут загрузка ядра была 7-9% (и это на все приложение, а не только на Pocketsphinx).

В процессе работы нами было замечено, что Pocketsphinx начинает определять ключевое слово не сразу, а через некоторое время (от нескольких десятков секунд до пары минут). Начался новый этап работы.

Работа с Pocketsphinx


Работа с Pocketsphinx состоит из этапов:

  1. Подготовка записей в соответствии с текущей задачей.
  2. Подготовка базы данных для модели.
  3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
  4. Реализация алгоритма использования API Pocketsphinx.
  5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

Остановимся на каждом этапе подробнее.

1. Подготовка записей в соответствии с текущей задачей


Подготовить базу записей в формате PCM.

Мы использовали следующие настройки:

Format settings, Endianness: Little
Bit rate: 256 Kbps
Channel(s): 1 channel
Sampling rate: 16.0 KHz
Bit depth: 16 bits

В специальном помещении для записи голоса они произносили ряд слов, каждый в нескольких интонациях (удивление, злость, грусть) и частотах (громко, тихо, нормально). Было сделано 100 записей: 60 мужских и 40 женских.

Примечание: для изменения частоты дискретизации используйте инструмент sox: sox original.wav -b 16 sample.wav каналов 1 скорость 16.

2. Подготовка базы данных для модели


etc
your_db.dic (Phonetic dictionary)
your_db.phone (Phoneset file)
your_db.jsgf (Language grammer)
your_db.filler (List of fillers)
your_db_train.fileids (List of files for training)
your_db_train.transcription (Transcription for training)
your_db_test.fileids (List of files for testing)
your_db_test.transcription (Transcription for testing)
wav
speaker_1
file_1.wav (Recording of speech utterance)
speaker_2
file_2.wav

3. Построение языковой модели


  1. Подготовить базу данных;
  2. Создать учебные файлы и полный словарь и список слов;
  3. Создать словарь из ru.dic и wordlist;
  4. Создайте файл из ru.dic;
  5. Извлечь текст из файла транскрипции;
  6. Построить языковую модель. Скачать инструмент SRILM и создать пакет для построения модели.
  7. Конвертировать файлы lm в файлы dmp.

    sphinx_lm_convert -i model.lm -o model.dmpsphinx_lm_convert -i model.dmp -ifmt dmp -o model.lm -ofmt arpasphinx_lm_convert -i model.lm -o model.lm.binsphinx_lm_convert -i model.lm.bin -ifmt bin -o model.lm -ofmt arpa
    
  8. И, наконец, обучить модель на полученных записях.

Шаги обучения


После подготовки файлов мы должны сгенерировать файл конфигурации: из корневой папки модели мы запускаем:
sphinxtrain -t setup "имя модели"

в конфигурационном файле sphinx_train.cfg:

1- set the value current to the variable CFG_CMN         $CFG_CMN = 'current';2 -$CFG_INITIAL_NUM_DENSITIES = 256; change this value to 13- $CFG_FINAL_NUM_DENSITIES = 256; change this value to 84- $CFG_N_TIED_STATES = 200;  set this value to 200$CFG_CD_TRAIN = 'yes'; set this value to no if the data set is small

Примечание: если мы изменим

$ CFG_CD_TRAIN

на нет, то нам нужно изменить

$ DEC_CFG_MODEL_NAME

на

$ CFG_EXPTNAME.ci_cont

из вашего sphinx_train.cfg. Изменить можно так:

$ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci_cont";

или

$ DEC_CFG_MODEL_NAME = "$ CFG_EXPTNAME.ci _ $ {CFG_DIRLABEL}";

Последний шаг начать тренировки нашей модели: sphinxtrain run.

Языковую модель возможно создать с нуля при помощи Sphinxtrain или можно использовать готовую из источника.

Инструкция по создании модули с нуля можно найти на github.

4. Реализация алгоритма использования API Pocketsphinx


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

  1. Распознавание речи в записаном аудио файле;
  2. Распознавание речи в потоке данных от микрофона.

Обобщенный алгоритм взаимодействия с Pocketsphinx после инициализации необходимых ресурсов:

1. Начать распознавание речи:

`int ps_start_utt(ps_decoder_t *ps)`

2. Передать данные из входного потока в Pocketsphinx:

`int ps_process_raw(ps_decoder_t *ps, int16 const *data, size_t n_samples, int no_search, int full_utt)`

3. Закончить распознавание речи:

`int ps_end_utt(ps_decoder_t *ps)`

4. Получить результат распознавания в виде строки:

`char const *ps_get_hyp(ps_decoder_t *ps, int32 *out_best_score)`

Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из микрофона:

```Cfor (;;) {    if ((k = ad_read(ad, adbuf, 2048)) < 0)        E_FATAL("Failed to read audio\n");    ps_process_raw(ps, adbuf, k, FALSE, FALSE);    in_speech = ps_get_in_speech(ps);    if (in_speech && !utt_started) {        utt_started = TRUE;        E_INFO("Listening...\n");    }    if (!in_speech && utt_started) {        /* speech -> silence transition, time to start new utterance  */        ps_end_utt(ps);        hyp = ps_get_hyp(ps, NULL );        if (hyp != NULL) {            printf("%s\n", hyp);            fflush(stdout);        }        if (ps_start_utt(ps) < 0)            E_FATAL("Failed to start utterance\n");        utt_started = FALSE;        E_INFO("Ready....\n");    }    sleep_msec(100);}```

Подробный пример взаимодействия с API Pocketsphinx, при чтении данных из файла:

```Cwhile ((k = fread(adbuf, sizeof(int16), 2048, rawfd)) > 0) {    ps_process_raw(ps, adbuf, k, FALSE, FALSE);    in_speech = ps_get_in_speech(ps);    if (in_speech && !utt_started) {        utt_started = TRUE;    }     if (!in_speech && utt_started) {        ps_end_utt(ps);        hyp = ps_get_hyp(ps, NULL);        if (hyp != NULL)    printf("%s\n", hyp);        fflush(stdout);        ps_start_utt(ps);        utt_started = FALSE;    }}```

5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом


После создания или скачивая языковой модели в файлах модели возможно найти файл feat.params. Данный файл необходим для задания параметров работы Pocketshinx. Если параметра нет в данном файле, то используется значение по умолчанию, список всех параметров и их описание возможно найти в исходном коде проекта или на сайте mankier.

Пример вывода программы pocketsphinx_continuous при использовании языковой модели.

```$ pocketsphinx_continuous -samprate 8000 -lm ru.lm -dict ru.dic -hmm zero_ru.cd_cont_4000 -remove_noise no -infile decoder-test.wavINFO: pocketsphinx.c(152): Parsed model-specific feature parameters from zero_ru.cd_cont_4000/feat.paramsCurrent configuration:



```

Распознавание из аудио файла или из данных микрофона определяется ключами:

`-infile <file-name>` и `-inmic <yes/no>`

соответственно.

Ключ файл языковой модели:

`-lm ru.lm`

Файл словаря:

`-dict ru.dic`

Путь, где хранятся файлы внутренней кухни Pocketsphinx:

`-hmm zero_ru.cd_cont_4000`

также файл feat.params.

Существует возможность распознавать в речи только часть слов языковой модели. Функцию включает ключ:

`-kws <file-name.txt>`

Например, наш файл с ключевой фразой выглядит так:

```окей скай /1e-15/```

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

Итог


Краткая версия для спешащих: мы использовали Pocketsphinx на python, но потом эту стратегию пришлось изменить из-за ограничений памяти на железе. Мы выбрали язык Си. Столкнулись с некоторыми нюансами:

Сборка не проходила с первого раза, а чаще всего со второго;
Ключевое слово определялось не сразу: от нескольких долей секунд до нескольких минут.

Работа с Pocketsphinx состоит из этапов:

  1. Подготовка записей в соответствии с текущей задачей.
  2. Подготовка базы данных для модели.
  3. Построение языковой модели: обучение модели на полученных записях. Получение Pocketsphinx model and language model.
  4. Реализация алгоритма использования API Pocketsphinx.
  5. Поиск оптимальных параметров Pocketsphinx эмпирическим методом.

Сейчас мы собираем все вместе.
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru