Как я могу проверить результаты моего посетителя (вариант) отправки stati c против polymorphi c dynamici c сравнительного теста? - PullRequest
3 голосов
/ 24 апреля 2020

Я пытался измерить производительность схемы отправления 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 настолько проста, насколько это возможно, должно подразумевать, что Измерение записывает понесенные накладные расходы с максимально возможной точностью.

Проверка

Вопросы,

  1. , как я могу проверить эти результаты так, чтобы они являются репрезентативными и
  2. следят за тем, чтобы компилятор не оптимизировал соответствующие части?
  3. Приходят ли другие тесты к тому же выводу, а именно, что 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";
}
...