Я пытался измерить производительность схемы отправления std::variant
(псевдо) stati c, используя std::visit
.
Идея состоит в том, что вместо
struct A {
virtual unsigned f() const noexcept = 0;
virtual ~A() noexcept {}
};
struct B0 : A {
virtual unsigned f() const noexcept { return 0; }
};
// more types like B0 ...
std::unique_ptr<A> a = std::make_unique<B0>();
у меня есть
struct C0 {
unsigned f() const noexcpet { return 0; }
};
std::variant<C0,C1,C2,/*...*/> c = C0();
, и я хочу измерить, насколько быстрее построить серию таких объектов и как быстро отправка. Обратите внимание, что в первом примере (A / Bs ...) требуется динамическая c память поверх диспетчера динамического c, в то время как во втором примере (Cs ...) имеется автоматическое c хранилище.
С этой целью я обобщил B0 и C0 на шаблоны типов:
template <unsigned X>
struct B : A {
virtual unsigned f() const noexcept override { return X; }
};
template <unsigned X>
struct C {
unsigned f() const noexcept { return X; }
};
, а затем написал (возможно, немного перегруженный) тестовый жгут, чтобы заполнить std::vector
и прочитать его обратно. Полный код прилагается ниже. Я запускаю это с -O1
и -O3
как C ++ 17.
Эффективно это псевдослучайно заполняет предварительно выращенные векторы bs
с B<...>
и cs
с C<...>
соответственно , а затем либо вызывает bs[i]->f()
или std::visit([](auto const& c) { return c.f(); },cs[i])
(более подробную информацию см. в прикрепленном коде теста).
Я бы ожидал, что является тестовым экземпляром variant<C<0>>
уносит свою динамику c встречную часть unique_ptr<A>
из воды на порядки (это происходит), но когда я вырасту вариант, скажем variant<C<0>,...,C<127>>
, эффективность visit
начинает go значительно ниже до точки, где динамическая c диспетчеризация происходит быстрее (не так, как ожидалось).
с -O3
(результаты -O1
довольно схожи) я вижу следующие результаты, которые незначительно различаются между прогонами, но кажутся относительно стабильными (время в основном остается в пределах отклонения 10%).
[0,0] time ctor virtual: 35.0315 ns
[0,0] time ctor variant: 2.9425 ns
[0,0] time call virtual: 14.0037 ns (L1)
[0,0] time call variant: 1.44748 ns (L2)
[0,1] time ctor virtual: 34.8007 ns
[0,1] time ctor variant: 2.95368 ns
[0,1] time call virtual: 19.6874 ns
[0,1] time call variant: 7.04521 ns
[0,7] time ctor virtual: 39.6325 ns
[0,7] time ctor variant: 2.97607 ns
[0,7] time call virtual: 30.7592 ns
[0,7] time call variant: 9.22505 ns (L4.1)
[0,31] time ctor virtual: 35.0002 ns
[0,31] time ctor variant: 2.95473 ns
[0,31] time call virtual: 24.3198 ns
[0,31] time call variant: 9.72678 ns (L4.2)
[0,127] time ctor virtual: 36.5918 ns
[0,127] time ctor variant: 2.95542 ns
[0,127] time call virtual: 26.701 ns (L3)
[0,127] time call variant: 9.88592 ns (L4.3)
Обсуждение
Небольшое время для (L1) Я думаю, это объясняется кэшированием и / или предсказанием ветвлений. (L2) абсолютно соответствует ожиданиям: если вариант тривиален, отправка происходит очень быстро. Все времена для строительства также имеют смысл: ctor variant
ничего не делает в любой точке malloc
ничего, поэтому ясно, почему это намного быстрее, чем ctor virtual
, и что время является примерно постоянным независимо от числа динамов c types.
call virtual
остается примерно таким же, как число динамических типов c, возрастающих (L3), что и следовало ожидать. Однако почему call variant
не повышается (больше) между (L4.1) и (L4.3) .
Примечание: учитывая ограничения программирования шаблонов в мой тестовый жгут, я не могу намного увеличить диапазон без взрыва g ++ во время компиляции / исчерпания моей памяти.
В любом случае, учитывая, что функция теста f
настолько проста, насколько это возможно, должно подразумевать, что Измерение записывает понесенные накладные расходы с максимально возможной точностью.
Проверка
Вопросы,
- , как я могу проверить эти результаты так, чтобы они являются репрезентативными и
- следят за тем, чтобы компилятор не оптимизировал соответствующие части?
- Приходят ли другие тесты к тому же выводу, а именно, что
std::variant
отправка всегда быстрее примерно в два раза? 2-3?
Полный тест
// g++ -Wall -Wextra -pedantic -std=c++17 -O3 a.cpp
#include <random>
#include <memory>
#include <variant>
#include <chrono>
#include <iostream>
using chronores = std::nano;
static constexpr char const resstr[] = "ns";
namespace helper {
template <template <unsigned> typename T, unsigned X, unsigned UB, typename... Args>
struct mkvar {
using type = typename mkvar<T,X+1,UB,Args...,T<X>>::type;
};
template <template <unsigned> typename T, unsigned UB, typename... Args>
struct mkvar<T,UB,UB,Args...> {
using type = std::variant<Args...,T<UB>>;
};
template <template <unsigned> typename T, unsigned LB, unsigned UB>
using mkvar_t = typename mkvar<T,LB,UB>::type;
template <unsigned X>
struct Num {
static constexpr unsigned value = X;
using inc = Num<X+1>;
};
template <typename NumX, typename NumUB, template <unsigned> typename T, bool use_variant>
struct ctor_Num {
static constexpr auto X = NumX::value;
static constexpr auto UB = NumUB::value;
template <typename Container>
static void run(unsigned x, Container& container) {
if (x == X) {
if constexpr (use_variant) {
container.emplace_back(T<X>());
} else {
container.emplace_back(std::make_unique<T<X>>());
}
} else {
ctor_Num<typename NumX::inc,NumUB,T,use_variant>::run(x,container);
}
}
};
template <typename NumX, template <unsigned> typename T, bool use_variant>
struct ctor_Num<typename NumX::inc,NumX,T,use_variant> {
template <typename Container>
static void run(unsigned, Container&) { }
};
template <unsigned X, unsigned UB, template <unsigned> typename T, bool use_variant, typename Container>
inline void ctor(unsigned x, Container& container) {
return ctor_Num<Num<X>,Num<UB>,T,use_variant>::run(x,container);
}
struct Time {
double& time;
std::chrono::time_point<std::chrono::steady_clock> start;
Time(double& time) : time(time) {
start = std::chrono::steady_clock::now();
}
~Time() {
auto const finish = std::chrono::steady_clock::now();
time += std::chrono::duration<double,chronores>(finish-start).count();
}
};
}
template <unsigned LB, unsigned UB>
struct measure {
struct A {
virtual unsigned f() const noexcept = 0;
virtual ~A() noexcept {}
};
template <unsigned X>
struct B : A {
virtual unsigned f() const noexcept override { return X; }
};
template <unsigned X>
struct C {
unsigned f() const noexcept { return X; }
};
static void main(std::size_t const N, std::size_t const R = 10, bool warmup = false) {
if (!warmup) main(N,1,true);
using namespace helper;
std::vector<std::unique_ptr<A>> bs;
bs.reserve(N);
std::vector<mkvar_t<C,LB,UB>> cs;
cs.reserve(N);
std::uniform_int_distribution<unsigned> distr(LB,UB);
double time_ctor_virtual = 0;
double time_ctor_variant = 0;
double time_call_virtual = 0;
double time_call_variant = 0;
unsigned volatile sum = 0;
std::mt19937 mt(42); mt.discard(100);
for (std::size_t r = 0; r < R; ++r) {
bs.clear();
cs.clear();
{
Time scope(time_ctor_virtual);
for (std::size_t i = 0; i < N; ++i) {
bs.emplace_back(std::make_unique<B<UB>>());
}
}
{
Time scope(time_ctor_variant);
for (std::size_t i = 0; i < N; ++i) {
cs.emplace_back(C<UB>());
}
}
bs.clear();
cs.clear();
for (std::size_t i = 0; i < N; ++i) {
auto const rn = distr(mt);
// effectively calls bs.emplace_back(std::make_unique<B<rn>>())
ctor<LB,UB,B,false>(rn,bs);
// effectively calls cs.emplace_back(C<rn>())
ctor<LB,UB,C,true >(rn,cs);
}
{
Time scope(time_call_variant);
for (std::size_t i = 0; i < N; ++i) {
sum += std::visit([](auto const& c) { return c.f(); },cs[i]);
}
}
{
Time scope(time_call_virtual);
for (std::size_t i = 0; i < N; ++i) {
sum += bs[i]->f();
}
}
}
(void)sum;
if (!warmup) {
std::cout << "[" << LB << "," << UB << "] time ctor virtual: " << (time_ctor_virtual/N/R) << " " << resstr << "\n";
std::cout << "[" << LB << "," << UB << "] time ctor variant: " << (time_ctor_variant/N/R) << " " << resstr << "\n";
std::cout << "[" << LB << "," << UB << "] time call virtual: " << (time_call_virtual/N/R) << " " << resstr << "\n";
std::cout << "[" << LB << "," << UB << "] time call variant: " << (time_call_variant/N/R) << " " << resstr << "\n";
}
}
};
int main() {
static constexpr std::size_t N = 400000;
measure<0,0>::main(N);
std::cout << "\n";
measure<0,1>::main(N);
std::cout << "\n";
measure<0,7>::main(N);
std::cout << "\n";
measure<0,31>::main(N);
std::cout << "\n";
measure<0,127>::main(N);
std::cout << "\n";
}