Привет, меня зовут Александр, я старший разработчик ПО в Центре разработкиOrionInnovation. Хочу признаться, я люблю рассказывать про C++ и не только на различных митапах и конференциях.Ивотядобрался доХабра. НаCppConfRussiaPiter2020 я рассказывал про концепты и послевыступленияполучилочень много вопросов про производительность компилятора при работе сними.Замеры производительности не были цельюмоегодоклада:мне было известно, что концепты компилируются с примерно такой же скоростью, что и обычные метапрограммы,адодетального сравнения я смог добраться совершенно недавно.Спешуподелиться результатом!
Несколько слов о концептах
Концептыпереосмыслениеметапрограммирования, аналогичноеconstexpr.Еслиconstexprэто про вычисление выраженийво время компиляции, будь то факториал, экспонента и так далее, то концептыэто про перегрузки, специализации, условия существования сущностей.Вобщем, про чистоеметапрограммирование. Иными словами, в C++20 появилась возможность писать конструкциибез единой, привычной для нас треугольной скобки, тем самым получая возможность быстро и читаемо описать какую-либо перегрузку или специализацию:
// #1void increment(auto & arg) requires requires { ++arg; }; // #2void 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запусков.
В замерах будут участвовать clang 12.0.0 и g++ 10.3.0, как с полной оптимизацией, так и без неё.
В качестве операционной системы выступит Ubuntu 16.04, запущенная на Windows 10 через WSL2.На всякий случай прилагаю характеристики ПК:
Характеристики ПК
------------------System Information------------------ Operating System: Windows 10 Enterprise 64-bit (10.0, Build 19043) (19041.vb_release.191206-1406) Language: Russian (Regional Setting: Russian) System Manufacturer: Dell Inc. System Model: Latitude 5491 BIOS: 1.12.0 (type: UEFI) Processor: Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz (8 CPUs), ~2.3GHz Memory: 32768MB RAM Available OS Memory: 32562MB RAM Page File: 9995MB used, 27430MB available------------------------Disk & DVD/CD-ROM Drives------------------------ Drive: C: Free Space: 26.5 GBTotal Space: 243.0 GBFile System: NTFS Model: SAMSUNG SSD PM871b M.2 2280 256GB
Эксперименты
Посленеобходимыхотступлениймы можем,наконец,начать эксперименты.
Эксперимент 1: Эволюция метапрограммирования
Для началапосмотрим на то, как компиляторы справляются с созданием перегрузки функции для инкрементируемых инеинкрементируемыхтипов данных аргумента. Компилируемый код для C++ 03, 17 и 20 представлены ниже. Один из показателей, а именнообъем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.
Код
// copied from boosttemplate<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};namespace is_inc { typedef char (&yes)[1]; typedef char (&no)[2];struct tag {};struct any { template <class T> any(T const&); };tag operator++(any const &);template<typename T>static yes test(T const &);static no test(tag);template<typename _T> struct IsInc{ static _T & type_value; static const bool value = sizeof(yes) == sizeof(test(++type_value));};}template<typename T>struct IsInc : public is_inc::IsInc<T> {};template<class Ty>typename enable_if<IsInc<Ty>::value>::type increment(Ty &);template<class Ty>typename enable_if<!IsInc<Ty>::value>::type increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() { Incrementable i; NonIncrementable ni; increment(i); increment(ni);}
#include <type_traits>template<class, class = std::void_t<>>struct IsInc : std::false_type {};template<class T>struct IsInc<T, std::void_t<decltype( ++std::declval<T&>() )>> : std::true_type{};template<class Ty>std::enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>std::enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() { Incrementable i; NonIncrementable ni; increment(i); increment(ni);}
void increment(auto & arg) requires requires { ++arg; };void increment(auto &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() { Incrementable i; NonIncrementable ni; increment(i); increment(ni);}
Давайте взглянем на результаты:
Файл |
Компиляция |
Время, мс |
Размер объектного файла, байт |
Количество символов, шт |
|
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 |
Как уже отмечалось ранее,количество кода существенно уменьшается
по мере развития языка: c 782 до 472 и затем до 230.Разницапочти в
3,5 раза, если сравнитьС++20 и С++03 (на самом деле даже
больше,т.к.порядка150170символов во всех примерахтестирующий код).
Размеры объектного файла также постепенно уменьшаются. Что же
современем компиляции? Странно, новремя компиляции 03 и 20 примерно
равно, а вот в С++17в два раза больше. Давайте взглянем на код
наших примеров: помимо всего прочего, в глаза
бросается#include
в случае C++17. Давайте
реализуемdeclval
,enable_if
иvoid_t
и
проверим:
template<bool C, typename T = void>struct enable_if { typedef T type; };template<typename T>struct enable_if<false, T> {};template<bool B, typename T = void>using enable_if_t = typename enable_if<B, T>::type;template<typename ...>using void_t = void;template<class T>T && declval() noexcept;template<class, class = void_t<>>struct IsInc { constexpr static bool value = false;};template<class T>struct IsInc<T, void_t<decltype( ++declval<T&>() )>>{ constexpr static bool value = true;};template<class Ty>enable_if_t<IsInc<Ty>::value> increment(Ty &);template<class Ty>enable_if_t<!IsInc<Ty>::value> increment(Ty &);struct Incrementable { Incrementable & operator++() { return *this; } };struct NonIncrementable {};void later() { Incrementable i; NonIncrementable ni; increment(i); increment(ni);}
И давайте обновим нашу таблицу:
Файл |
Компиляция |
Время, мс |
Размер объектного файла, байт |
Количество символов, шт |
|
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 |
Время компиляции на 17 стандарте нормализовалось и стало практически равно времени компиляции 03 и 20, однако количество кода стало близко к самому тяжёлому, базовому варианту. Так что, если у вас есть под рукой C++20 и нужно написать какую-то простую мета-перегрузку,смело можно использовать концепты. Это читабельнее, компилируется примерно с такой же скоростью, а результат компиляции занимает меньше места.
Эксперимент 2: Ограничения для методов
Давайте взглянем на еще одну особенность: ограничение для
функции или метода (в том числе и для конструкторов и деструкторов)
на примере типаOptionalLike
, имеющего деструктор по
умолчанию в случае, если помещаемый объект тривиален, а
иначедеструктор, выполняющийдеинициализациюкорректно. Код
представлен ниже:
#include <type_traits>#include <string>template<typename T, typename = void>struct OptionalLike { ~OptionalLike() { /* Calls d-tor manually */ }};template<typename T>struct OptionalLike<T, std::enable_if_t<std::is_trivially_destructible<T>::value>>{ ~OptionalLike() = default;};void later() { OptionalLike<int> oli; OptionalLike<std::string> ols;}
#include <type_traits>#include <string>template<typename T>struct OptionalLike{ ~OptionalLike() { /* Calls d-tor manually */ } ~OptionalLike() requires (std::is_trivially_destructible<T>::value) = default;};void later() { OptionalLike<int> oli; OptionalLike<std::string> ols;}
Давайте взглянем на результаты:
Файл |
Компиляция |
Время, мс |
Размер объектного файла, байт |
Количество символов, шт |
|
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 |
Мы видим, что новый вариант выглядит более читабельным и лаконичным (253 символа против 319 у классического), однако платим за это временем компиляции: оба компилятора как с оптимизацией, так и без показали худшее время компиляции в случае с концептами. GCC аж в 22,5 раза медленнее. При этом размер объектного файла уgccне изменяется вовсе, а в случаеclangбольше для концептов. Классический компромисс: либо меньше кода, но дольше компиляция, либо больше кода, но быстрее компиляция.
Эксперимент 3: Влияние использования концептов на время компиляции
Мы знаем, что накладывать ограничения на типможноиспользуя именованные наборы требований, они же концепты.Такжеможно указать требования непосредственно в момент объявления шаблонной сущности. Давайте посмотрим, есть ли разница с точки зрения компилятора. Компилировать будем следующие фрагменты:
Код
template<typename T>void foo() requires (sizeof(T) >= 4) { }template<typename T>void foo() {}void later() { foo<char>(); foo<int>();}
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo() requires IsBig<T> { }template<typename T>void foo() {}void later() { foo<char>(); foo<int>();}
Сразу взглянем на результаты:
Файл |
Компиляция |
Время, мс |
Размер объектного файла, байт |
|
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 |
Как мы можем заметить,размеры получившихся объектных файлов идентичны, а показатели времени компиляции практически совпадают. Так что при выборе концепт илиinline-требованиеможнонезадумываться о производительности компилятора.
Эксперимент 4: Варианты ограничения функции
Теперь посмотрим на варианты наложения ограничения на шаблонные параметры на примере функций. Ограничить функцию можно аж четырьмя способами:
-
Имя концепта вместо
typename
-
Requires clauseпосле
template<>
-
Имя концепта рядом с
auto
-
Trailing requiresclause
Давайте узнаем, какой же из предложенных способов самый оптимальный с точки зрения компиляции. Компилируемый код представлен ниже:
Код
template<typename T>concept IsBig = sizeof(T) >= 4;template<IsBig T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() { foo<char>('a'); foo<int>(1);}
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T> requires IsBig<T>void foo(T const &) { }template<typename T>void foo(T const &) {}void later() { foo<char>('a'); foo<int>(1);}
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(IsBig auto const &) { }template<typename T>void foo(auto const &) {}void later() { foo<char>('a'); foo<int>(1);}
template<typename T>concept IsBig = sizeof(T) >= 4;template<typename T>void foo(T const &) requires IsBig<T> { }template<typename T>void foo(T const &) {}void later() { foo<char>('a'); foo<int>(1);}
А вот и результаты:
Файл |
Компиляция |
Время, мс |
Размер объектного файла, байт |
|
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 requiresclauseили использование концепта рядом с
auto
оказались самыми быстрыми. -
Варианты, где присутствует
template<>
на510% медленнее остальных. -
Размерыобъектных файлов изменяются незначительно, однако вариант с именем концепта вместо
typename
оказался самым объемным в случаеgcc, а вариант сautoоказался наименее объемным в случаеclang.
Эксперимент 5: Влияние сложности концепта на время компиляции
Последнее, что мырассмотрим в рамкахданной статьи, и, наверное, самое интересное: влияние сложности концепта на время компиляции. Давайте возьмём и скомпилируем следующие примеры, где сложность используемого концепта (количество проверок или условий) возрастает от первого к последнему.
Код
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept TestedConcept = ConceptA<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() { int i { 0 }; int * ip = &i; foo(i); foo(ip);}
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB = requires(T i, int x) { { i++ } noexcept -> ConceptA; { ++i } noexcept -> ConceptA; { i-- } noexcept -> ConceptA; { --i } noexcept -> ConceptA; { i + i } noexcept -> ConceptA; { i - i } noexcept -> ConceptA; { i += i } noexcept -> ConceptA; { i -= i } noexcept -> ConceptA; { i * i } noexcept -> ConceptA; { i / i } noexcept -> ConceptA; { i % i } noexcept -> ConceptA; { i *= i } noexcept -> ConceptA; { i /= i } noexcept -> ConceptA; { i %= i } noexcept -> ConceptA; { i | i } noexcept -> ConceptA; { i & i } noexcept -> ConceptA; { i |= i } noexcept -> ConceptA; { i &= i } noexcept -> ConceptA; { ~i } noexcept -> ConceptA; { i ^ i } noexcept -> ConceptA; { i << x } noexcept -> ConceptA; { i >> x } noexcept -> ConceptA; { i ^= i } noexcept -> ConceptA; { i <<= x } noexcept -> ConceptA; { i >>= x } noexcept -> ConceptA;};template<typename T>concept ConceptC = requires(T i, int x) { { i++ } noexcept -> ConceptB; { ++i } noexcept -> ConceptB; { i-- } noexcept -> ConceptB; { --i } noexcept -> ConceptB; { i + i } noexcept -> ConceptB; { i - i } noexcept -> ConceptB; { i += i } noexcept -> ConceptB; { i -= i } noexcept -> ConceptB; { i * i } noexcept -> ConceptB; { i / i } noexcept -> ConceptB; { i % i } noexcept -> ConceptB; { i *= i } noexcept -> ConceptB; { i /= i } noexcept -> ConceptB; { i %= i } noexcept -> ConceptB; { i | i } noexcept -> ConceptB; { i & i } noexcept -> ConceptB; { i |= i } noexcept -> ConceptB; { i &= i } noexcept -> ConceptB; { ~i } noexcept -> ConceptB; { i ^ i } noexcept -> ConceptB; { i << x } noexcept -> ConceptB; { i >> x } noexcept -> ConceptB; { i ^= i } noexcept -> ConceptB; { i <<= x } noexcept -> ConceptB; { i >>= x } noexcept -> ConceptB;};template<typename T>concept ConceptD = requires(T i, int x) { { i++ } noexcept -> ConceptC; { ++i } noexcept -> ConceptC; { i-- } noexcept -> ConceptC; { --i } noexcept -> ConceptC; { i + i } noexcept -> ConceptC; { i - i } noexcept -> ConceptC; { i += i } noexcept -> ConceptC; { i -= i } noexcept -> ConceptC; { i * i } noexcept -> ConceptC; { i / i } noexcept -> ConceptC; { i % i } noexcept -> ConceptC; { i *= i } noexcept -> ConceptC; { i /= i } noexcept -> ConceptC; { i %= i } noexcept -> ConceptC; { i | i } noexcept -> ConceptC; { i & i } noexcept -> ConceptC; { i |= i } noexcept -> ConceptC; { i &= i } noexcept -> ConceptC; { ~i } noexcept -> ConceptC; { i ^ i } noexcept -> ConceptC; { i << x } noexcept -> ConceptC; { i >> x } noexcept -> ConceptC; { i ^= i } noexcept -> ConceptC; { i <<= x } noexcept -> ConceptC; { i >>= x } noexcept -> ConceptC;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() { int i { 0 }; int * ip = &i; foo(i); foo(ip);}
template<typename T>concept ConceptA = sizeof(T) >= 1;template<typename T>concept ConceptB = requires(T i, int x) { { i++ } noexcept -> ConceptA; { ++i } noexcept -> ConceptA; { i-- } noexcept -> ConceptA; { --i } noexcept -> ConceptA; { i + i } noexcept -> ConceptA; { i - i } noexcept -> ConceptA; { i += i } noexcept -> ConceptA; { i -= i } noexcept -> ConceptA; { i * i } noexcept -> ConceptA; { i / i } noexcept -> ConceptA; { i % i } noexcept -> ConceptA; { i *= i } noexcept -> ConceptA; { i /= i } noexcept -> ConceptA; { i %= i } noexcept -> ConceptA; { i | i } noexcept -> ConceptA; { i & i } noexcept -> ConceptA; { i |= i } noexcept -> ConceptA; { i &= i } noexcept -> ConceptA; { ~i } noexcept -> ConceptA; { i ^ i } noexcept -> ConceptA; { i << x } noexcept -> ConceptA; { i >> x } noexcept -> ConceptA; { i ^= i } noexcept -> ConceptA; { i <<= x } noexcept -> ConceptA; { i >>= x } noexcept -> ConceptA;};template<typename T>concept ConceptC = requires(T i, int x) { { i++ } noexcept -> ConceptB; { ++i } noexcept -> ConceptB; { i-- } noexcept -> ConceptB; { --i } noexcept -> ConceptB; { i + i } noexcept -> ConceptB; { i - i } noexcept -> ConceptB; { i += i } noexcept -> ConceptB; { i -= i } noexcept -> ConceptB; { i * i } noexcept -> ConceptB; { i / i } noexcept -> ConceptB; { i % i } noexcept -> ConceptB; { i *= i } noexcept -> ConceptB; { i /= i } noexcept -> ConceptB; { i %= i } noexcept -> ConceptB; { i | i } noexcept -> ConceptB; { i & i } noexcept -> ConceptB; { i |= i } noexcept -> ConceptB; { i &= i } noexcept -> ConceptB; { ~i } noexcept -> ConceptB; { i ^ i } noexcept -> ConceptB; { i << x } noexcept -> ConceptB; { i >> x } noexcept -> ConceptB; { i ^= i } noexcept -> ConceptB; { i <<= x } noexcept -> ConceptB; { i >>= x } noexcept -> ConceptB;};template<typename T>concept ConceptD = requires(T i, int x) { { i++ } noexcept -> ConceptC; { ++i } noexcept -> ConceptC; { i-- } noexcept -> ConceptC; { --i } noexcept -> ConceptC; { i + i } noexcept -> ConceptC; { i - i } noexcept -> ConceptC; { i += i } noexcept -> ConceptC; { i -= i } noexcept -> ConceptC; { i * i } noexcept -> ConceptC; { i / i } noexcept -> ConceptC; { i % i } noexcept -> ConceptC; { i *= i } noexcept -> ConceptC; { i /= i } noexcept -> ConceptC; { i %= i } noexcept -> ConceptC; { i | i } noexcept -> ConceptC; { i & i } noexcept -> ConceptC; { i |= i } noexcept -> ConceptC; { i &= i } noexcept -> ConceptC; { ~i } noexcept -> ConceptC; { i ^ i } noexcept -> ConceptC; { i << x } noexcept -> ConceptC; { i >> x } noexcept -> ConceptC; { i ^= i } noexcept -> ConceptC; { i <<= x } noexcept -> ConceptC; { i >>= x } noexcept -> ConceptC;};template<typename T>concept ConceptE = requires(T i, int x) { { i++ } noexcept -> ConceptD; { ++i } noexcept -> ConceptD; { i-- } noexcept -> ConceptD; { --i } noexcept -> ConceptD; { i + i } noexcept -> ConceptD; { i - i } noexcept -> ConceptD; { i += i } noexcept -> ConceptD; { i -= i } noexcept -> ConceptD; { i * i } noexcept -> ConceptD; { i / i } noexcept -> ConceptD; { i % i } noexcept -> ConceptD; { i *= i } noexcept -> ConceptD; { i /= i } noexcept -> ConceptD; { i %= i } noexcept -> ConceptD; { i | i } noexcept -> ConceptD; { i & i } noexcept -> ConceptD; { i |= i } noexcept -> ConceptD; { i &= i } noexcept -> ConceptD; { ~i } noexcept -> ConceptD; { i ^ i } noexcept -> ConceptD; { i << x } noexcept -> ConceptD; { i >> x } noexcept -> ConceptD; { i ^= i } noexcept -> ConceptD; { i <<= x } noexcept -> ConceptD; { i >>= x } noexcept -> ConceptD;};template<typename T>concept ConceptF = requires(T i, int x) { { i++ } noexcept -> ConceptE; { ++i } noexcept -> ConceptE; { i-- } noexcept -> ConceptE; { --i } noexcept -> ConceptE; { i + i } noexcept -> ConceptE; { i - i } noexcept -> ConceptE; { i += i } noexcept -> ConceptE; { i -= i } noexcept -> ConceptE; { i * i } noexcept -> ConceptE; { i / i } noexcept -> ConceptE; { i % i } noexcept -> ConceptE; { i *= i } noexcept -> ConceptE; { i /= i } noexcept -> ConceptE; { i %= i } noexcept -> ConceptE; { i | i } noexcept -> ConceptE; { i & i } noexcept -> ConceptE; { i |= i } noexcept -> ConceptE; { i &= i } noexcept -> ConceptE; { ~i } noexcept -> ConceptE; { i ^ i } noexcept -> ConceptE; { i << x } noexcept -> ConceptE; { i >> x } noexcept -> ConceptE; { i ^= i } noexcept -> ConceptE; { i <<= x } noexcept -> ConceptE; { i >>= x } noexcept -> ConceptE;};template<typename T>concept ConceptG = requires(T i, int x) { { i++ } noexcept -> ConceptF; { ++i } noexcept -> ConceptF; { i-- } noexcept -> ConceptF; { --i } noexcept -> ConceptF; { i + i } noexcept -> ConceptF; { i - i } noexcept -> ConceptF; { i += i } noexcept -> ConceptF; { i -= i } noexcept -> ConceptF; { i * i } noexcept -> ConceptF; { i / i } noexcept -> ConceptF; { i % i } noexcept -> ConceptF; { i *= i } noexcept -> ConceptF; { i /= i } noexcept -> ConceptF; { i %= i } noexcept -> ConceptF; { i | i } noexcept -> ConceptF; { i & i } noexcept -> ConceptF; { i |= i } noexcept -> ConceptF; { i &= i } noexcept -> ConceptF; { ~i } noexcept -> ConceptF; { i ^ i } noexcept -> ConceptF; { i << x } noexcept -> ConceptF; { i >> x } noexcept -> ConceptF; { i ^= i } noexcept -> ConceptF; { i <<= x } noexcept -> ConceptF; { i >>= x } noexcept -> ConceptF;};template<typename T>concept TestedConcept = ConceptA<T> && ConceptB<T> && ConceptC<T> && ConceptD<T> && ConceptE<T> && ConceptF<T> && ConceptG<T>;void foo(TestedConcept auto const &) {}void foo(auto const &) {}void later() { int i { 0 }; int * ip = &i; foo(i); foo(ip);}
Давайте взглянем на результат:
Файл |
Компиляция |
Время, мс |
Количество символов, шт |
|
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 |
Чего и следовало ожидать, в общем случае существенное увеличение сложности концепта (обратите внимание, что концепты в примерах рекурсивные,и каждый последующий включает многократные отсылки к предыдущим) приводит к увеличению времени компиляциилишьна 515%.
Заключение
В результате вышеописанных экспериментов мы можем сделать следующие выводы:
-
Концепты позволяют создавать более читабельный код, который компилируется в меньший объектный файл, по сравнению с классическимметапрограммированием.
-
Несмотря на это, код, содержащий концепты/constraintызачастую компилируется дольше, иногда довольно значительно, как это было в случае ограничения для методов.
-
Время компиляции прямо пропорционально сложности концептов/constraint'ов.
Post Scriptum
Во-первых, к статье прилагаюссылку на гитхаб, пройдя по которой вы можете найтискриптыдля запуска тестов,а такжеиспользуемыев статьефрагменты кода и повторить некоторые (а может и все) тесты локально.
Ну а во-вторых,мне быочень хотелосьувидеть, как ведут себя компиляторы с более сложными конструкциями.Если вы знаете/придумали подходящиепримеры, смелопишите о них в комментариях,ияс радостьюпроизведузамеры.