Статический полиморфизм с возможностью выбора варианта одного посетителя против нескольких посетителей и динамического полиморфизма

Я сравниваю производительность следующих методов полиморфизма С++:

Метод [1]. статический полиморфизм с использованием вариантов форсирования с отдельным посетителем для каждого метода Метод [2]. статический полиморфизм с использованием вариантов boost с одним посетителем, который вызывает другой метод с использованием перегрузки метода Метод [3]. Обычный статический динамический полиморфизм

Платформа: - Intel x86 64-бит Red Hat, современный многоядерный процессор, 32 ГБ оперативной памяти - gcc (GCC) 4.8.1 с оптимизацией -O2 - Boost 1.6.0

Некоторые выводы:

  • Метод [1], по-видимому, значительно превзошел методы [2] и [3]
  • Метод [3] превосходит метод [2] в большинстве случаев

Мой вопрос в том, почему метод [2], где я использую посетителя, но используя перегрузку метода для вызова правильного метода, дает худшую производительность, чем виртуальные методы. Я ожидал бы, что статический полиморфизм будет лучше, чем динамический полиморфизм. Я понимаю, что есть некоторая стоимость дополнительного параметра, который передается в методе [2], чтобы определить, какой метод посещения() класса вызывает и, возможно, еще большее разветвление из-за перегрузки метода? Но разве это не превосходит виртуальные методы?

Код ниже:

// qcpptest.hpp

#ifndef INCLUDED_QCPPTEST_H
#define INCLUDED_QCPPTEST_H

#include <boost/variant.hpp>

class IShape {
 public:
  virtual void rotate() = 0;
  virtual void spin() = 0;
};

class Square : public IShape {
 public:
  void rotate() {
   // std::cout << "Square:I am rotating" << std::endl;
    }
  void spin() { 
    // std::cout << "Square:I am spinning" << std::endl; 
  }
};

class Circle : public IShape {
 public:
  void rotate() { 
    // std::cout << "Circle:I am rotating" << std::endl; 
  }
  void spin() {
   // std::cout << "Circle:I am spinning" << std::endl; 
}
};

// template variation

// enum class M {ADD, DEL};
struct ADD {};
struct DEL {};

class TSquare {
    int i;
 public:
    void visit(const ADD& add) {
        this->i++;
    // std::cout << "TSquare:I am rotating" << std::endl;
  }
    void visit(const DEL& del) {
        this->i++;
    // std::cout << "TSquare:I am spinning" << std::endl;
  }

    void spin() {
        this->i++;
     // std::cout << "TSquare:I am rotating" << std::endl; 
 }
    void rotate() {
        this->i++;
     // std::cout << "TSquare:I am spinning" << std::endl; 
 }
};

class TCircle {
    int i;
 public:
    void visit(const ADD& add) {
        this->i++;
    // std::cout << "TCircle:I am rotating" << std::endl;
  }
    void visit(const DEL& del) {
        this->i++;
    // std::cout << "TCircle:I am spinning" << std::endl;
  }
    void spin() { 
        this->i++;
        // std::cout << "TSquare:I am rotating" << std::endl; 
    }
    void rotate() {
    this->i++; 
        // std::cout << "TSquare:I am spinning" << std::endl; 
    }
};

class MultiVisitor : public boost::static_visitor<void> {
 public:
  template <typename T, typename U>

    void operator()(T& t, const U& u) {
    // std::cout << "visit" << std::endl;
    t.visit(u);
  }
};

// separate visitors, single dispatch

class RotateVisitor : public boost::static_visitor<void> {
 public:
  template <class T>
  void operator()(T& x) {
    x.rotate();
  }
};

class SpinVisitor : public boost::static_visitor<void> {
 public:
  template <class T>
  void operator()(T& x) {
    x.spin();
  }
};

#endif

// qcpptest.cpp

#include <iostream>
#include "qcpptest.hpp"
#include <vector>
#include <boost/chrono.hpp>

using MV = boost::variant<ADD, DEL>;
// MV const add = M::ADD;
// MV const del = M::DEL;
static MV const add = ADD();
static MV const del = DEL();

void make_virtual_shapes(int iters) {
  // std::cout << "make_virtual_shapes" << std::endl;
  std::vector<IShape*> shapes;
  shapes.push_back(new Square());
  shapes.push_back(new Circle());

  boost::chrono::high_resolution_clock::time_point start =
      boost::chrono::high_resolution_clock::now();

  for (int i = 0; i < iters; i++) {
    for (IShape* shape : shapes) {
      shape->rotate();
      shape->spin();
    }
  }

  boost::chrono::nanoseconds nanos =
      boost::chrono::high_resolution_clock::now() - start;
  std::cout << "make_virtual_shapes took " << nanos.count() * 1e-6
            << " millis\n";
}

void make_template_shapes(int iters) {
  // std::cout << "make_template_shapes" << std::endl;
  using TShapes = boost::variant<TSquare, TCircle>;
  // using MV = boost::variant< M >;

  // xyz
  std::vector<TShapes> tshapes;
  tshapes.push_back(TSquare());
  tshapes.push_back(TCircle());
  MultiVisitor mv;

  boost::chrono::high_resolution_clock::time_point start =
      boost::chrono::high_resolution_clock::now();

  for (int i = 0; i < iters; i++) {
    for (TShapes& shape : tshapes) {
      boost::apply_visitor(mv, shape, add);
      boost::apply_visitor(mv, shape, del);
      // boost::apply_visitor(sv, shape);
    }
  }
  boost::chrono::nanoseconds nanos =
      boost::chrono::high_resolution_clock::now() - start;
  std::cout << "make_template_shapes took " << nanos.count() * 1e-6
            << " millis\n";
}

void make_template_shapes_single(int iters) {
  // std::cout << "make_template_shapes_single" << std::endl;
  using TShapes = boost::variant<TSquare, TCircle>;
  // xyz
  std::vector<TShapes> tshapes;
  tshapes.push_back(TSquare());
  tshapes.push_back(TCircle());
  SpinVisitor sv;
  RotateVisitor rv;

  boost::chrono::high_resolution_clock::time_point start =
      boost::chrono::high_resolution_clock::now();

  for (int i = 0; i < iters; i++) {
    for (TShapes& shape : tshapes) {
      boost::apply_visitor(rv, shape);
      boost::apply_visitor(sv, shape);
    }
  }
  boost::chrono::nanoseconds nanos =
      boost::chrono::high_resolution_clock::now() - start;
  std::cout << "make_template_shapes_single took " << nanos.count() * 1e-6
            << " millis\n";
}

int main(int argc, const char* argv[]) {
  std::cout << "Hello, cmake" << std::endl;

  int iters = atoi(argv[1]);

  make_virtual_shapes(iters);
  make_template_shapes(iters);
  make_template_shapes_single(iters);

  return 0;
}

Ответ 1

Метод 2 в основном неэффективно выполняет динамическую диспетчеризацию. Когда у вас есть:

shape->rotate();
shape->spin();

Это включает поиск правильной функции в vtable и ее вызов. Неэффективность этого поиска. Но когда у вас есть:

boost::apply_visitor(mv, shape, add);

Это грубо всплывает (при условии, что шаблон функции add<>, который является просто reinterpret_cast без проверки):

if (shape.which() == 0) {
    if (add.which() == 0) {
        mv(shape.as<TSquare&>(), add.as<ADD&>());
    }
    else if (add.which() == 1) {
        mv(shape.as<TSquare&>(), add.as<DEL&>());
    }
    else {
        // ???
    }
}
else if (shape.which() == 1) {
    if (add.which() == 0) {
        mv(shape.as<TCircle&>(), add.as<ADD&>());
    }
    else if (add.which() == 1) {
        mv(shape.as<TCircle&>(), add.as<DEL&>());
    }
    else {
        // ???
    }
}
else {
   // ???
}

Здесь мы имеем комбинаторный взрыв ветвей (что нам не нужно было делать в методе 1), но мы действительно должны проверять каждый возможный статический тип каждого варианта, чтобы выяснить, что мы должны были сделать (что мы сделали не нужно делать в методе 3). И эти ветки не смогут быть предсказаны, так как каждый раз вы принимаете разные, поэтому вы не можете конвейерного кода какого-либо кода, не дожидаясь остановки.

Перегрузка на mv() бесплатна - выясняется, что мы называем mv, а это не так. Обратите внимание также на время дельта, которое будет происходить, основываясь на изменении любой из двух осей:

+---------------+----------------+----------------+----------+
|               |    Method 1    |    Method 2    | Method 3 |
+---------------+----------------+----------------+----------+
|    New Type   | More Expensive | More Expensive |   Free   |
| New Operation |      Free      | More Expensive |   Free*  |
+---------------+----------------+----------------+----------+

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

Способ 3 может добавлять новые типы и освобождать для добавления новых операций - единственное изменение - увеличение vtable. Это будет иметь некоторые эффекты из-за размера объекта, но обычно будет меньше, чем увеличенная итерация по типам.