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

Производительность компилятора при работе с концептами в C20

Привет, меня зовут Александр, я старший разработчик ПО в Центре разработки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. Ну а мы сфокусируемся на том, какой ценой достигается подобный функционал, чтобы убедиться, чтоэтонетолькопросто, быстро и красиво, ноещёи эффективно.

Описание эксперимента

Итак, мы будем наблюдать за следующими показателями:

  1. Время компиляции

  1. Размер объектного файла

  1. Количество символов в записи (или же количество кода), в некоторых случаях

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

  • Во-первых, при подсчёте количества символов в записи мы будем считать все не пустые.

  • Во-вторых, в данной статье мы посмотрим лишь на самые простые (буквально несколько строк) случаи, чтобы быть уверенными на 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 представлены ниже. Один из показателей, а именнообъем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.

Код
incrementable_03.cpp
// 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 &lt;class T&gt; any(T const&amp;); };tag operator++(any const &amp;);template&lt;typename T&gt;static yes test(T const &amp;);static no test(tag);template&lt;typename _T&gt; struct IsInc{    static _T &amp; 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);}
incrementable_17.cpp
#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);}
incrementable_20.cpp
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и проверим:

incrementable_no_tt.cpp
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, имеющего деструктор по умолчанию в случае, если помещаемый объект тривиален, а иначедеструктор, выполняющийдеинициализациюкорректно. Код представлен ниже:

Код
optional_like_17.cpp
#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;}
optional_like_20.cpp
#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: Влияние использования концептов на время компиляции

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

Код
inline.cpp
template<typename T>void foo() requires (sizeof(T) >= 4) { }template<typename T>void foo() {}void later() {    foo<char>();    foo<int>();}
concept.cpp
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

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

Код
instead_of_typename.cpp
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);}
after_template.cpp
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);}
with_auto.cpp
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);}
requires_clause.cpp
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: Влияние сложности концепта на время компиляции

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

Код
concept_complexity_1.cpp
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);}
concept_complexity_2.cpp
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);}
concept_complexity_3.cpp
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

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

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

Источник: habr.com
К списку статей
Опубликовано: 20.06.2021 18:15:44
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании orion innovation

Высокая производительность

Программирование

C++

Концепты

Производительность

Эксперименты

Замеры

Категории

Последние комментарии

© 2006-2021, personeltest.ru