Привет, меня зовут Александр, я старший разработчик ПО в Центре разработки Orion Innovation. Хочу признаться, я люблю рассказывать про C++ и не только на различных митапах и конференциях. И вот я добрался до Хабра. На CppConf Russia Piter 2020 я рассказывал про концепты и после выступления получил очень много вопросов про производительность компилятора при работе с ними. Замеры производительности не были целью моего доклада: мне было известно, что концепты компилируются с примерно такой же скоростью, что и обычные метапрограммы, а до детального сравнения я смог добраться совершенно недавно. Спешу поделиться результатом!
Несколько слов о концептах
Концепты — переосмысление метапрограммирования, аналогичное constexpr. Если constexpr — это про вычисление выражений во время компиляции, будь то факториал, экспонента и так далее, то концепты — это про перегрузки, специализации, условия существования сущностей. В общем, про «чистое метапрограммирование». Иными словами, в C++20 появилась возможность писать конструкции без единой, привычной для нас треугольной скобки, тем самым получая возможность быстро и читаемо описать какую-либо перегрузку или специализацию:// #1
void increment(auto & arg) requires requires { ++arg; };
// #2
void increment(auto &);
struct Incrementable { Incrementable & operator++() { return *this; } };
struct NonIncrementable {};
void later() {
Incrementable i;
NonIncrementable ni;
increment(i); // Вызывается #1
increment(ni); // Вызывается #2
}
О том, как всё это работает, есть море информации, например, отличный гайд "Концепты: упрощаем реализацию классов STD Utility" по мотивам выступления Андрея Давыдова на C++ Russia 2019. Ну а мы сфокусируемся на том, какой ценой достигается подобный функционал, чтобы убедиться, что это не только просто, быстро и красиво, но ещё и эффективно.
Описание эксперимента
Итак, мы будем наблюдать за следующими показателями:- Время компиляции
- Размер объектного файла
- Количество символов в записи (или же количество кода), в некоторых случаях
- Во-первых, при подсчёте количества символов в записи мы будем считать все не пустые.
- Во-вторых, в данной статье мы посмотрим лишь на самые простые (буквально несколько строк) случаи, чтобы быть уверенными на 100%, что мы сравниваем абсолютно аналогичные фрагменты кода.
- В-третьих, поскольку компилируемые примеры крайне просты, время компиляции выражается в десятках миллисекунд. Чтобы исключить погрешность, для времени компиляции мы будем использовать усреднённые значения за 100 запусков.
В качестве операционной системы выступит Ubuntu 16.04, запущенная на Windows 10 через WSL2. На всякий случай прилагаю характеристики ПК:
Характеристики ПК
Эксперименты
После необходимых отступлений мы можем, наконец, начать эксперименты.Эксперимент №1: Эволюция метапрограммирования
Для начала посмотрим на то, как компиляторы справляются с созданием перегрузки функции для инкрементируемых и неинкрементируемых типов данных аргумента. Компилируемый код для C++ 03, 17 и 20 представлены ниже. Один из показателей, а именно — объем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.Код
Давайте взглянем на результаты:
Файл | Компиляция | Время, мс | Размер объектного файла, байт | Количество символов, шт | |
incrementable_03.cpp | clang | O0 | 43,02 | 1304 | 782 |
incrementable_17.cpp | clang | O0 | 67,46 | 1320 | 472 |
incrementable_20.cpp | clang | O0 | 43,42 | 1304 | 230 |
incrementable_03.cpp | clang | O3 | 47,21 | 1296 | 782 |
incrementable_17.cpp | clang | O3 | 77,77 | 1304 | 472 |
incrementable_20.cpp | clang | O3 | 45,70 | 1288 | 230 |
incrementable_03.cpp | gcc | O0 | 19,89 | 1568 | 782 |
incrementable_17.cpp | gcc | O0 | 34,71 | 1568 | 472 |
incrementable_20.cpp | gcc | O0 | 17,62 | 1480 | 230 |
incrementable_03.cpp | gcc | O3 | 18,44 | 1552 | 782 |
incrementable_17.cpp | gcc | O3 | 38,94 | 1552 | 472 |
incrementable_20.cpp | gcc | O3 | 18,57 | 1464 | 230 |
incrementable_no_tt.cpp
И давайте обновим нашу таблицу:
Файл | Компиляция | Время, мс | Размер объектного файла, байт | Количество символов, шт | |
incrementable_03.cpp | clang | O0 | 43,02 | 1304 | 782 |
incrementable_17_no_tt.cpp | clang | O0 | 44,498 | 1320 | 714 |
incrementable_20.cpp | clang | O0 | 43,419 | 1304 | 230 |
incrementable_03.cpp | clang | O3 | 47,205 | 1296 | 782 |
incrementable_17_no_tt.cpp | clang | O3 | 47,327 | 1312 | 714 |
incrementable_20.cpp | clang | O3 | 45,704 | 1288 | 230 |
incrementable_03.cpp | gcc | O0 | 19,885 | 1568 | 782 |
incrementable_17_no_tt.cpp | gcc | O0 | 21,163 | 1584 | 714 |
incrementable_20.cpp | gcc | O0 | 17,619 | 1480 | 230 |
incrementable_03.cpp | gcc | O3 | 18,442 | 1552 | 782 |
incrementable_17_no_tt.cpp | gcc | O3 | 19,057 | 1568 | 714 |
incrementable_20.cpp | gcc | O3 | 18,566 | 1464 | 230 |
Эксперимент №2: Ограничения для методов
Давайте взглянем на еще одну особенность: ограничение для функции или метода (в том числе и для конструкторов и деструкторов) на примере типа OptionalLike, имеющего деструктор по умолчанию в случае, если помещаемый объект тривиален, а иначе — деструктор, выполняющий деинициализацию корректно. Код представлен ниже:Код
Давайте взглянем на результаты:
Файл | Компиляция | Время, мс | Размер объектного файла, байт | Количество символов, шт | |
optional_like_17.cpp | clang | O0 | 487,62 | 1424 | 319 |
optional_like_20.cpp | clang | O0 | 616,8 | 1816 | 253 |
optional_like_17.cpp | clang | O3 | 490,07 | 944 | 319 |
optional_like_20.cpp | clang | O3 | 627,64 | 1024 | 253 |
optional_like_17.cpp | gcc | O0 | 202,29 | 1968 | 319 |
optional_like_20.cpp | gcc | O0 | 505,82 | 1968 | 253 |
optional_like_17.cpp | gcc | O3 | 205,55 | 1200 | 319 |
optional_like_20.cpp | gcc | O3 | 524,54 | 1200 | 253 |
Эксперимент №3: Влияние использования концептов на время компиляции
Мы знаем, что накладывать ограничения на тип можно используя именованные наборы требований, они же концепты. Также можно указать требования непосредственно в момент объявления шаблонной сущности. Давайте посмотрим, есть ли разница с точки зрения компилятора. Компилировать будем следующие фрагменты:Код
Сразу взглянем на результаты:
Файл | Компиляция | Время, мс | Размер объектного файла, байт | |
inline.cpp | clang | O0 | 38,666 | 1736 |
concept.cpp | clang | O0 | 39,868 | 1736 |
concept.cpp | clang | O3 | 42,578 | 1040 |
inline.cpp | clang | O3 | 43,610 | 1040 |
inline.cpp | gcc | O0 | 14,598 | 1976 |
concept.cpp | gcc | O0 | 14,640 | 1976 |
concept.cpp | gcc | O3 | 14,872 | 1224 |
inline.cpp | gcc | O3 | 14,951 | 1224 |
Эксперимент №4: Варианты ограничения функции
Теперь посмотрим на варианты наложения ограничения на шаблонные параметры на примере функций. Ограничить функцию можно аж четырьмя способами:- Имя концепта вместо typename
- Requires clause после template<>
- Имя концепта рядом с auto
- Trailing requires clause
Код
А вот и результаты:
Файл | Компиляция | Время, мс | Размер объектного файла, байт | |
function_with_auto.cpp | clang | O0 | 40,878 | 1760 |
function_after_template.cpp | clang | O0 | 41,947 | 1760 |
function_requires_clause.cpp | clang | O0 | 42,551 | 1760 |
function_instead_of_typename.cpp | clang | O0 | 46,893 | 1760 |
function_with_auto.cpp | clang | O3 | 43,928 | 1024 |
function_requires_clause.cpp | clang | O3 | 45,176 | 1032 |
function_after_template.cpp | clang | O3 | 45,275 | 1032 |
function_instead_of_typename.cpp | clang | O3 | 50,42 | 1032 |
function_requires_clause.cpp | gcc | O0 | 16,561 | 2008 |
function_with_auto.cpp | gcc | O0 | 16,692 | 2008 |
function_after_template.cpp | gcc | O0 | 17,032 | 2008 |
function_instead_of_typename.cpp | gcc | O0 | 17,802 | 2016 |
function_requires_clause.cpp | gcc | O3 | 16,233 | 1208 |
function_with_auto.cpp | gcc | O3 | 16,711 | 1208 |
function_after_template.cpp | gcc | O3 | 17,216 | 1208 |
function_instead_of_typename.cpp | gcc | O3 | 18,315 | 1216 |
- Вариант с использованием имени концепта вместо typename оказался самым медленным во всех случаях.
- Варианты trailing requires clause или использование концепта рядом с auto оказались самыми быстрыми.
- Варианты, где присутствует template<>на 5‒10% медленнее остальных.
- Размеры объектных файлов изменяются незначительно, однако вариант с именем концепта вместо typename оказался самым объемным в случае gcc, а вариант с auto оказался наименее объемным в случае clang.
Эксперимент №5: Влияние сложности концепта на время компиляции
Последнее, что мы рассмотрим в рамках данной статьи, и, наверное, самое интересное: влияние сложности концепта на время компиляции. Давайте возьмём и скомпилируем следующие примеры, где сложность используемого концепта (количество проверок или условий) возрастает от первого к последнему.Код
Давайте взглянем на результат:
Файл | Компиляция | Время, мс | Количество символов, шт | |
concept_complexity_1.cpp | clang | O0 | 37,441 | 201 |
concept_complexity_2.cpp | clang | O0 | 38,211 | 2244 |
concept_complexity_3.cpp | clang | O0 | 39,989 | 4287 |
concept_complexity_1.cpp | clang | O3 | 40,062 | 201 |
concept_complexity_2.cpp | clang | O3 | 40,659 | 2244 |
concept_complexity_3.cpp | clang | O3 | 43,314 | 4287 |
concept_complexity_1.cpp | gcc | O0 | 15,352 | 201 |
concept_complexity_2.cpp | gcc | O0 | 16,077 | 2244 |
concept_complexity_3.cpp | gcc | O0 | 18,091 | 4287 |
concept_complexity_1.cpp | gcc | O3 | 15,243 | 201 |
concept_complexity_2.cpp | gcc | O3 | 17,552 | 2244 |
concept_complexity_3.cpp | gcc | O3 | 18,51 | 4287 |
Заключение
В результате вышеописанных экспериментов мы можем сделать следующие выводы:- Концепты позволяют создавать более читабельный код, который компилируется в меньший объектный файл, по сравнению с классическим метапрограммированием.
- Несмотря на это, код, содержащий концепты/constraint’ы зачастую компилируется дольше, иногда довольно значительно, как это было в случае ограничения для методов.
- Время компиляции прямо пропорционально сложности концептов/constraint'ов.
Post Scriptum
Во-первых, к статье прилагаю ссылку на гитхаб, пройдя по которой вы можете найти скрипты для запуска тестов, а также используемые в статье фрагменты кода и повторить некоторые (а может и все) тесты локально.Ну а во-вторых, мне бы очень хотелось увидеть, как ведут себя компиляторы с более сложными конструкциями. Если вы знаете/придумали подходящие примеры, смело пишите о них в комментариях, и я с радостью произведу замеры.
Источник статьи: https://habr.com/ru/company/orioninc/blog/562410/