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

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

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

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

  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 GB
Total Space: 243.0 GB
File System: NTFS
      Model: SAMSUNG SSD PM871b M.2 2280 256GB

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

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

Эксперимент №1: Эволюция метапрограммирования

Для начала посмотрим на то, как компиляторы справляются с созданием перегрузки функции для инкрементируемых и неинкрементируемых типов данных аргумента. Компилируемый код для C++ 03, 17 и 20 представлены ниже. Один из показателей, а именно — объем кода, можно оценить уже сейчас: видно, что количество кода существенно сокращается по мере эволюции языка, уступая место читаемости и простоте.

Код
incrementable_03.cpp
template<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);
}
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 (на самом деле даже больше, т.к. порядка 150‒170 символов во всех примерах — тестирующий код). Размеры объектного файла также постепенно уменьшаются. Что же со временем компиляции? Странно, но время компиляции 03 и 20 примерно равно, а вот в С++17 — в два раза больше. Давайте взглянем на код наших примеров: помимо всего прочего, в глаза бросается #include в случае C++17. Давайте реализуем declvalenable_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 аж в 2‒2,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 requires clause 

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

Код
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;

void foo(IsBig auto const &) { }

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 requires clause или использование концепта рядом с auto оказались самыми быстрыми. 

  • Варианты, где присутствует template<>на 5‒10% медленнее остальных. 

  • Размеры объектных файлов изменяются незначительно, однако вариант с именем концепта вместо 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

Чего и следовало ожидать, в общем случае существенное увеличение сложности концепта (обратите внимание, что концепты в примерах рекурсивные, и каждый последующий включает многократные отсылки к предыдущим) приводит к увеличению времени компиляции лишь на 5‒15%.

Заключение

В результате вышеописанных экспериментов мы можем сделать следующие выводы: 

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

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

  • Время компиляции прямо пропорционально сложности концептов/constraint'ов.

Post Scriptum

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

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

89 0