Политика против полиморфной скорости - PullRequest
5 голосов
/ 05 января 2012

Я прочитал, что использование класса политики для функции, которая будет вызываться в узком цикле, намного быстрее, чем использование полиморфной функции.Тем не менее, я настроил эту демонстрацию, и время показывает, что это как раз наоборот !?Версия политики занимает в 2-3 раза больше, чем полиморфная версия.

#include <iostream>

#include <boost/timer.hpp>

// Policy version
template < typename operation_policy>
class DoOperationPolicy : public operation_policy
{
  using operation_policy::Operation;

public:
  void Run(const float a, const float b)
  {
    Operation(a,b);
  }
};

class OperationPolicy_Add
{
protected:
  float Operation(const float a, const float b)
  {
    return a + b;
  }
};

// Polymorphic version
class DoOperation
{
public:
  virtual float Run(const float a, const float b)= 0;
};

class OperationAdd : public DoOperation
{
public:
  float Run(const float a, const float b)
  {
    return a + b;
  }
};

int main()
{
  boost::timer timer;

  unsigned int numberOfIterations = 1e7;

  DoOperationPolicy<OperationPolicy_Add> policy_operation;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
    {
    policy_operation.Run(1,2);
    }
  std::cout << timer.elapsed() << " seconds." << std::endl;
  timer.restart();

  DoOperation* polymorphic_operation = new OperationAdd;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
    {
    polymorphic_operation->Run(1,2);
    }
  std::cout << timer.elapsed() << " seconds." << std::endl;

}

Что-то не так с демо?Или просто неверно, что политика должна быть быстрее?

Ответы [ 5 ]

4 голосов
/ 05 января 2012

Ваш тест не имеет смысла (извините).

К сожалению, сделать реальные тесты сложно, так как компиляторы очень умны.

Что искать здесь:

  • девиртуализация: полиморфный вызов, как ожидается, будет медленнее, потому что он должен быть виртуальным, но здесь компилятор может понять, что polymorphic_operation обязательно является OperationAdd и, таким образом, напрямую вызывает OperationAdd::Run без вызова диспетчеризации во время выполнения
  • встраивание: поскольку компилятор имеет доступ к телу методов, он может их встроить и вообще избежать вызовов функций.
  • «удаление мертвого хранилища»: неиспользуемые значения не нужно сохранять, а вычислений, которые приводят к ним и не вызывают побочных эффектов, можно полностью избежать.

Действительно, весь ваш код теста можно оптимизировать для:

int main()
{
  boost::timer timer;

  std::cout << timer.elapsed() << " seconds." << std::endl;

  timer.restart();

  DoOperation* polymorphic_operation = new OperationAdd;

  std::cout << timer.elapsed() << " seconds." << std::endl;
}

Именно тогда вы понимаете, что не рассчитываете время, которое хотели бы ...

Чтобы сделать ваш тест значимым, вам необходимо:

  • предотвратить девиртуализацию
  • побочные эффекты силы

Чтобы предотвратить девиртуализацию, просто объявите функцию DoOperation& Get(), а затем в другом файле cpp: DoOperation& Get() { static OperationAdd O; return O; }.

Чтобы вызвать побочные эффекты (необходимо, только если методы встроены): верните значение и накапливайте его, затем отобразите его.


В действии с помощью этой программы:

// test2.cpp
namespace so8746025 {

  class DoOperation
  {
  public:
    virtual float Run(const float a, const float b) = 0;
  };

  class OperationAdd : public DoOperation
  {
  public:
    float Run(const float a, const float b)
    {
      return a + b;
    }
  };

  class OperationAddOutOfLine: public DoOperation
  {
  public:
    float Run(const float a, const float b);
  };

  float OperationAddOutOfLine::Run(const float a, const float b)
  {
    return a + b;
  }

  DoOperation& GetInline() {
    static OperationAdd O;
    return O;
  }

  DoOperation& GetOutOfLine() {
    static OperationAddOutOfLine O;
    return O;
  }

} // namespace so8746025

// test.cpp
#include <iostream>

#include <boost/timer.hpp>

namespace so8746025 {

  // Policy version
  template < typename operation_policy>
  struct DoOperationPolicy
  {
    float Run(const float a, const float b)
    {
      return operation_policy::Operation(a,b);
    }
  };

  struct OperationPolicy_Add
  {
    static float Operation(const float a, const float b)
    {
      return a + b;
    }
  };

  // Polymorphic version
  class DoOperation
  {
  public:
    virtual float Run(const float a, const float b) = 0;
  };

  class OperationAdd : public DoOperation
  {
  public:
    float Run(const float a, const float b)
    {
      return a + b;
    }
  };

  class OperationAddOutOfLine: public DoOperation
  {
  public:
    float Run(const float a, const float b);
  };


  DoOperation& GetInline();
  DoOperation& GetOutOfLine();

} // namespace so8746025

using namespace so8746025;

int main()
{
  unsigned int numberOfIterations = 1e8;

  DoOperationPolicy<OperationPolicy_Add> policy;

  OperationAdd stackInline;
  DoOperation& virtualInline = GetInline();

  OperationAddOutOfLine stackOutOfLine;
  DoOperation& virtualOutOfLine = GetOutOfLine();


  boost::timer timer;

  float result = 0;

  for(unsigned int i = 0; i < numberOfIterations; ++i)  {
    result += policy.Run(1,2);
  }
  std::cout << "Policy: " << timer.elapsed() << " seconds (" << result << ")" << std::endl;


  timer.restart();
  result = 0;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    result += stackInline.Run(1,2);
  }
  std::cout << "Stack Inline: " << timer.elapsed() << " seconds (" << result << ")" << std::endl;

  timer.restart();
  result = 0;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    result += virtualInline.Run(1,2);
  }
  std::cout << "Virtual Inline: " << timer.elapsed() << " seconds (" << result << ")" << std::endl;

  timer.restart();
  result = 0;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    result += stackOutOfLine.Run(1,2);
  }
  std::cout << "Stack Out Of Line: " << timer.elapsed() << " seconds (" << result << ")" << std::endl;

  timer.restart();
  result = 0;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    result += virtualOutOfLine.Run(1,2);
  }
  std::cout << "Virtual Out Of Line: " << timer.elapsed() << " seconds (" << result << ")" << std::endl;

}

Получаем:

$ gcc --version
gcc (GCC) 4.3.2

$ ./testR
Policy: 0.17 seconds (6.71089e+07)
Stack Inline: 0.17 seconds (6.71089e+07)
Virtual Inline: 0.52 seconds (6.71089e+07)
Stack Out Of Line: 0.6 seconds (6.71089e+07)
Virtual Out Of Line: 0.59 seconds (6.71089e+07)

Обратите внимание на тонкую разницу между девиртуализацией + inline и отсутствием девиртуализации.

3 голосов
/ 05 января 2012

FWIW Я сделал это

  • политика, в отличие от смешанной
  • вернуть значение
  • используйте энергозависимое, чтобы избежать оптимизации вне цикла и несвязанная оптимизация цикла (например, уменьшение нагрузки / накоплений из-за развертывания цикла и векторизации на объектах, которые его поддерживают).
  • сравнить с прямым статическим вызовом функции
  • использовать намного больше итераций
  • скомпилировать с -O3 на gcc

Сроки:

DoDirect: 3.4 seconds.
Policy: 3.41 seconds.
Polymorphic: 3.4 seconds.

Ergo: разницы нет. Главным образом, потому что GCC может статически анализировать тип DoOperation *, который будет DoOperationAdd - в цикле есть поиск vtable:)

ВАЖНО

Если вы хотите сравнить reallife производительность этого точного цикла, вместо накладных расходов на вызов функции, сбросьте volatile. Время теперь стало

DoDirect: 6.71089e+07 in 1.12 seconds.
Policy: 6.71089e+07 in 1.15 seconds.
Polymorphic: 6.71089e+07 in 3.38 seconds.

Как видите, без энергозависимости компилятор может оптимизировать некоторые циклы загрузки-хранения; Я предполагаю, что он может выполнять развертывание цикла + распределение регистров (однако я не проверял машинный код). Дело в том, что цикл в целом можно оптимизировать гораздо больше с помощью подхода «политика», чем с динамической диспетчеризацией (то есть виртуальным методом)

КОД

#include <iostream>

#include <boost/timer.hpp>

// Direct version
struct DoDirect {
    static float Run(const float a, const float b) { return a + b; }
};

// Policy version
template <typename operation_policy>
struct DoOperationPolicy {
    float Run(const float a, const float b) const {
        return operation_policy::Operation(a,b);
    }
};

struct OperationPolicy_Add {
    static float Operation(const float a, const float b) {
        return a + b;
    }
};

// Polymorphic version
struct DoOperation {
    virtual float Run(const float a, const float b) const = 0;
};

struct OperationAdd  : public DoOperation { 
    float Run(const float a, const float b) const { return a + b; } 
};

int main(int argc, const char *argv[])
{
    boost::timer timer;

    const unsigned long numberOfIterations = 1<<30ul;

    volatile float result = 0;
    for(unsigned long i = 0; i < numberOfIterations; ++i) {
        result += DoDirect::Run(1,2);
    }
    std::cout << "DoDirect: " << result << " in " << timer.elapsed() << " seconds." << std::endl;
    timer.restart();

    DoOperationPolicy<OperationPolicy_Add> policy_operation;
    for(unsigned long i = 0; i < numberOfIterations; ++i) {
        result += policy_operation.Run(1,2);
    }
    std::cout << "Policy: " << result << " in " << timer.elapsed() << " seconds." << std::endl;
    timer.restart();

    result = 0;
    DoOperation* polymorphic_operation = new OperationAdd;
    for(unsigned long i = 0; i < numberOfIterations; ++i) {
        result += polymorphic_operation->Run(1,2);
    }
    std::cout << "Polymorphic: " << result << " in " << timer.elapsed() << " seconds." << std::endl;

}
2 голосов
/ 05 января 2012

Вы должны включить оптимизацию и убедиться, что

  • обе части кода на самом деле делают одно и то же (чего в настоящее время нет, ваш вариант политики не возвращает результат)
  • результат используется для чего-то, чтобы компилятор вообще не отбрасывал путь к коду (достаточно просто суммировать результаты и вывести их куда-нибудь)
2 голосов
/ 05 января 2012

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

1 голос
/ 05 января 2012

Мне пришлось изменить код вашей политики, чтобы он возвращал вычисленное значение:

float Run(const float a, const float b)
{
  return Operation(a,b);
}

Во-вторых, мне пришлось сохранить возвращенное значение, чтобы убедиться, что цикл не будет оптимизирован:

int main()
{
  unsigned int numberOfIterations = 1e9;
  float answer = 0.0;

  boost::timer timer;
  DoOperationPolicy<OperationPolicy_Add> policy_operation;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    answer += policy_operation.Run(1,2);
  }
  std::cout << "Policy got " << answer << " in " << timer.elapsed() << " seconds" << std::endl;

  answer = 0.0;
  timer.restart();
  DoOperation* polymorphic_operation = new OperationAdd;
  for(unsigned int i = 0; i < numberOfIterations; ++i)
  {
    answer += polymorphic_operation->Run(1,2);
  }
  std::cout << "Polymo got " << answer << " in " << timer.elapsed() << " seconds" << std::endl;

  return 0;
}

Без оптимизации на g ++ 4.1.2:

Policy got 6.71089e+07 in 13.75 seconds
Polymo got 6.71089e+07 in 7.52 seconds

С -O3 на g ++ 4.1.2:

Policy got 6.71089e+07 in 1.18 seconds
Polymo got 6.71089e+07 in 3.23 seconds

Таким образом, политика определенно быстрее после оптимизациивключен.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...