CPP

Всем привет! Меня зовут Герман Диаго, по образованию я инженер компьютерной науки и уже долгое время увлечён разработкой разного рода ПО. 

Большую часть своей карьеры я работал в бэкенд над ОС реального времени и высоконадёжными системами. Начиная с 2002 и по сей день самой актуальной технологией для меня была и остаётся C++. 

Время идёт, и однажды по воле случая на одном веб-сайте я столкнулся с интересной задачей. Назывался этот сайт “Tom’s Data Onion” (луковица данных Тома). Найти его можно по следующей ссылке: https://www.tomdalling.com/toms-data-onion/.

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

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

Самостоятельная цель проекта

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

  1. Я хотел последовать рабочему процессу TDD, поэтому для тестирования мне потребовались зависимости.
  2. Я решил использовать последнюю версию C++, а именно C++20.
  3. Целью было получить что-то, что сможет декодировать полезную нагрузку base85. Правильность проверяется посредством тестов (TDD). В завершении же я решил добавить декодирование файлов в качестве базового бенчмарка на случай, если захочу улучшить полученный результат более эффективными реализациями. 

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

Выбор среды для написания кода

Как я уже говорил, опыт в написании кода я имею достаточный. Мне не просто нравится этим заниматься, но я также люблю доводить дело до конца. Поэтому мне предстояло выбрать из 200 000 пользовательских вариантов рабочих циклов, подходящих для C++, наиболее целесообразный. Можно было выбрать Makefiles, но он актуален только в Unix-системах. Другой вариант Autotools, но с ним та же история. Можно было также использовать решение с Visual Studio, но этот вариант подошёл бы только для Windows. Я же предпочитаю задействовать портативную систему сборки. Ей могут быть как Scons или Waf, так и Tup, Cmake или Build2. Какую же выбрать?

Что касается IDE, то можно рассматривать варианты, начиная с Visual Studio и полноценных IDE CLion, вплоть до текстовых редакторов вроде Vim, Emacs, Sublime или Visual Studio Code…

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

В итоге я сформировал для задачи следующую среду:

  • Doom Emacs;
  • Meson Build System (+ обёртка для внешних зависимостей);
  • автодополнение кода Lsp.

Насколько эффективной оказалась среда?

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

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

Процесс написания энкодера

Как только инструменты установлены и настроены, вам нужно:

  1. Настроить репозиторий Git.
  2. Установить и запустить ccl для автодополнения кода Lsp.
  3. Добавить meson-mode из Emacs через MELPA.
  4. Обеспечить минимальную инфраструктуру сборочных файлов meson для запуска проекта.

Настройку репозитория я произвёл из терминала, выполнив команду git init в директории проекта. 

ccls можно установить из пакетного менеджера через apt и запустить также из терминала. Я для простоты запускаю его с помощью nohup ccls &, хотя вы можете создать собственные системные интеграции или что-то подобное. 

Можно установить множество пакетов из Emacs через MELPA. Я установил meson-mode и уже не припомню устанавливал ли что-то ещё, поскольку Doom Emacs содержит много всего по умолчанию, хотя всё это требует активации (раскомментирования + .emacs.d/bin/doom sync). Doom Emacs поддерживает пошаговое обновление, чтобы при сбое чего-либо вы могли вернуться к предыдущей настройке, хотя сам я этим не пользовался. 

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

project(‘onionbase85’,
 ‘cpp’,
 default_options : [‘cpp_std=c++2a’])

subdir(‘src’)

Я добавил исполняемый файл и тест. В Meson это делается достаточно просто. Как вы видите, для теста нужна зависимость catch2, которая не является частью стандартной библиотеки:

catch2_dep = dependency(‘catch2’, fallback : [‘catch2’, ‘catch2_dep’])

test_decoding = executable(‘testDecoding’, [‘TestDecoding.cpp’],
 dependencies :
 [catch2_dep])

test(‘Base85 decoding tests’, test_decoding)

Вы можете настроить проект в Emacs при помощи библиотеки projectile, нажав F10 и перейдя в пункт меню Projectile -> Configure project либо выполнив команду Control-c p C. Сокращения существуют для всех команд. Projectile использует в качестве корневой директории каталог .git и достаточно сообразительна, чтобы обнаружить, что это проект Meson, поэтому в первый раз она подсказывает мне команду для конфигурации, которую я могу изменить, но настройка по умолчанию вполне меня устраивают. Кроме этого, также присутствуют команды для запуска, тестирования и компиляции.

С помощью F9 вы можете скрыть/показать виджет дерева навигации слева. F10 вызывает глобальное меню, а правый клик открывает контекстное меню для файла. 

Doom Emacs показывает дерево навигации после нажатия F9. Повторное нажатие F9 скрывает его.

Как только проект был настроен, я добавил (не уверен, что это по-прежнему нужно, но тогда было необходимо) ссылку на мой файл compile_commands.json в корневом каталоге для автодополнения кода:

ln -s build/compile_commands.json compile_commands.json

После этого я перезапустил emacs, и Lsp будет индексировать всё до тех пор, пока запущен ccls, поскольку ccls является демоном. 

Есть также внешняя зависимость — Catch2. Что же происходит с ней? Всё просто: если она обнаруживается в системе, то используется. Если же не обнаруживается, то вы облажались… а может и нет…

Использование зависимостей Meson wrap

Meson wrap — это система зависимостей пакетов (вроде Conan). Она содержит коллекцию зависимостей и очень хорошо работает с Meson, поскольку интегрирована на уровне источников. Кстати, при помощи pkg-config вы также можете использовать с Meson и Conan.

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

Как бы то ни было, я сделал следующее:

$ meson wrap search catch

catch
catch2
catchorg-clara

Вы можете установить подпроекты в Meson в директорию subprojects/. Для этого просто нужно создать эту директорию и выполнить:

$ meson wrap install catch2

Если вы хотите узнать больше о доступных версиях и прочем, то выполните:

$ meson wrap info catch2

Я использовал C++20, но с gcc9. К несчастью, на тот момент в этой версии gcc ещё не было std::span. Я снова использовал meson wrap для установки microsoft-gsl, у которой была версия с поддержкой std::span.

Спустя пару минут после обращения к зависимости в Meson я смог приступить к написанию тестов. 

Мне нужно было лишь добавить код в src/TestDecoding.cpp и запустить тесты. Я использую сокращённую команду Control-c — P.

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

Итоговое решение вы можете посмотреть в репозитории по ссылке, приведённой в конце статьи. По сути, я разделил основную функцию для Catch в TestMain.cpp, и время итерации существенно улучшилось. Вместо ожидания 6–7 секунд до компиляции, я ждал всего около 1 секунды.

В Emacs вы можете легко выполнить команду компиляции или тестирования с помощью Control-p с с или Control-p c P соответственно. В Doom Emacs вы также можете использовать больше Vim-подобных команд (по умолчанию). Но я использую сокращения Emacs, поскольку так привык.

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

Мой TDD заключался в преобразовании блоков теста и последующем декодировании потока с помощью предварительно сохранённой в файле полезной нагрузки base85.

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

Как только все итерации TDD были завершены, и тесты пройдены, я произвёл разделение на директорию tests/ для тестов и автономную библиотеку в src/. 

Благодаря отличному интерфейсу Magit, я смог сохранить свою работу на Git, выполнив Control-x g. Этот интерфейс предоставляет спектр возможностей, начиная от сохранения файлов и заканчивая их отправкой на удалённые ресурсы или сравнение. Всё это делается легко (хотя и в стиле emacs), и я использовал эти возможности на протяжении всего своего опыта работы с Emacs.

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

Выводы

Настройка проекта

Настроить Emacs под Linux несложно. Вы просто устанавливаете его из пакетного менеджера через apt и получаете Doom Emacs, а затем следуете инструкциям. 

Настройка режима lsp не так легка, если сравнивать с использованием среды вроде Visual Studio или CLion, и вам потребуется произвести кое-какие настройки. К счастью, Doom Emacs несколько более дружелюбен, чем некоторые его альтернативы и при запуске представляет вам следующий экран:

Экран запуска Doom Emacs

Вам нужно открыть приватную конфигурацию (private configutation) для init.el и раскомментировать “(cc+lsp)”. Если вам нужен lens mode, то понадобиться добавить его самостоятельно в файл config.el. Я добавил приведённые далее две строки, поскольку я также запускаю emacs в развёрнутом виде: 

(toggle-frame-maximized)
(add-hook! ‘lsp-mode-hook ‘lsp-lens-mode)

Вам нужен meson, значит необходимо установить его через pip, выполнив команду python3 -m pip install meson. Вам также нужно выполнить sudo apt install ninja-build.

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

Рабочий цикл разработки

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

Контекстное меню для перемещения, выделения одинаковых переменных, получения результатов тестов через Projectile и ссылочных линз в Emacs
Автодополнение кода работает даже для внешних зависимостей через compile_commands.json и lsp

Единственное, чего мне недостаёт, — это панели выполнения тестов вроде тех, что есть в Visual Studio Code или CLion, и при помощи которых вы можете легко выполнять тесты иерархически, наглядно кликая правой кнопкой. 

Время итерации

Это была первая проблема, связанная со временем компиляции Catch2. Как только она была решена, всё стало хорошо. Предполагается, что уменьшение времени компиляции в C++ достигается при помощи модулей, но иногда приходится прибегать к иным мерам. В моём случае решением было использование отдельного основного файла для Catch, и сработало оно отлично. 

Модернизация C++

С++ на протяжении последних лет был существенно улучшен. Когда я начинал в 2002 году, C++98 не имел лямбд, основанных на диапазоне циклов for, семантики перемещений, функций элементов по умолчанию, интеллектуальных указателей (хотя в Boost они появились задолго до C++11), пропуска копий, шаблонов или концепций переменных.

Но я должен отдельно отметить std::span (вообще-то в своём коде я использовал gsl::span). Этот класс настолько полезен, что я не представляю, как его могло раньше не быть. Правильное сочетание возможностей C++ и алгоритмов вроде std::copy и std::remove_if позволяет мне написать энкодер так, что я буду уверен как в его эффективности по сравнению с тем, что я могу сделать сам, так и в его пригодности (при помощи лямбд, например, для предикатов). Тем не менее мне по-прежнему недостаёт в этом языке сокращённых лямбд.

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

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

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

Конечно же, это C++, и вам придётся подумать о возможности копирования, срезания объектов (slicing), а также о некоторых других моментах. Но язык всё равно стал существенно лучше. 

Инструменты сборки нового поколения

Я выбрал Meson, поскольку к нему я привык и вернулся после попытки использовать Cmake. В Meson интегрирован пакетный менеджер, его гораздо легче понять, чем такие инструменты, как Autotools, кроме того, он переносится на Windows, что очень удобно для совместной работы. 

В общем такие инструменты, как Cmake (хоть я и не люблю его за многое) и Meson во многом улучшили эту область за последние годы. Однако сделать предстоит ещё многое. В этом могут помочь модули, но необходимо учитывать, что C++ в его текущей реализации является нативно компилируемым языком. Это предоставляет сложности для таких компонентов, как пакетные менеджеры.

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

Вы можете генерировать проекты для Visual Studio, ninja и прочего. Также есть файл compile_commands.json, сгенерированный для интеграции с фреймворками автодополнения кода. Все эти возможности ушли намного вперёд от той точки в прошлом, когда я начинал писать код. 

Насколько легко будет дальнейшее подключение людей для сотрудничества?

Сегодня у нас есть Gihub для настройки проектов и их публикации. Я пропустил эту настройку, поскольку она относится к последнему шагу. 

Рабочий процесс сотрудничества на Github всем прекрасно известен (форк, пул-реквест). Всё это совместно с новыми инструментами генерации вроде Meson существенно упрощает сотрудничество. Вы можете использовать свой проект из Visual Studio в Windows так же, как я использовал его из Linux с продемонстрированной мной настройкой Emacs, применив то же самое дерево источников. 

Вы также можете использовать CLion, если склонны задействовать compile_commands.json в качестве файла проекта (и после добавления пары скриптов для сборки/отладки), и получить полную структуру рефакторинга, запускать код и выполнять его отладку, сохраняя при этом портативность между Linux, Windows и Mac, а также одну среду разработки (я пробовал это в другом своём проекте). CLion также имеет преимущество в виде панели тестов, позволяющей запускать тесты выборочно, чего мне так не хватает в Emacs. 

Всё несомненно стало лучше, хотя мне недостаёт прямой интеграции Meson для IDE. Что касается сейчас, то compile_commands.json совместно с некоторыми связующими компонентами или настройками даёт весьма неплохой результат. Я думаю, что CMake идёт впереди относительно интеграции IDE, но я уже просто не могу вернуться к ней, поскольку нахожу Meson намного удобнее в добавлении компонентов для создания скриптов. 

Перемещение по коду и рефакторинг

В перемещении по коду был сделан большой шаг вперёд при помощи lsp и таких бэкенд элементов, как cquery, ccls и clangd. Я использовал ccls для настройки. Раньше переименование не было целесообразным за исключением замены текста, что по своей сути достаточно шатко.

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

CLion предлагает лучший в своей области рефакторинг, хотя я эту функцию полноценно ещё не испробовал. 

Подытоживая, можно сказать, что опыт показался мне весьма продуктивным, но всё ещё требующим доработки, в отличии, к примеру, от Java или коммерческих IDE C#. Но если сравнивать со временем, когда я только начинал писать код, то всё претерпело значительные улучшения, особенно в течение последних пяти лет.

Заключение

Имея опыт использования Visual Studio + Resharper для C#, могу сказать, что эти IDE всё ещё не дотягивают по уровню до лучших IDE C# или Java. Например, в C# легко удалить неиспользуемые инструкции using (эквивалент #includes больше или меньше), да и генерация кода работает на высоком уровне. 

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

Если вам нужна кроссплатформенность в отношении настройки среды разработки, то я могу посоветовать рассмотреть лицензионную версию CLion. Она вполне доступна по цене и работает очень хорошо. Однажды и я её приобрету. Но для разработки только под Linux или Windows, на мой взгляд, будет вполне достаточно Visual Studio или Emacs даже на профессиональном уровне.

Проекты на Github вы можете найти здесь.

Благодарю за чтение!

Читайте также:


Перевод статьи German Diago Gomez: Testing a Modern C++ workflow by coding a base85 decoder from scratch

Предыдущая статьяПошаговое построение логистической регрессии в Python
Следующая статьяРуководство разработчика по оптимизации скорости работы веб-сайтов