Я хотел измерить и сравнить накладные расходы на различные вызовы функций. Разные в смысле двух альтернативных способов борьбы с расширением класса при минимизации модификации кода:
- с использованием абстрактного базового класса и обеспечения реализаций в виртуальных функциях-членах
- с использованием класса хоста политики и определения различных политик со статическими и функциями-членами
Оба этих параметра сравниваются с вызовом никакой функции вообще. Я также знаю, что NVI идиома обычно используется при проектировании классов, поддерживающих динамический полиморфизм - пример, который я использовал, был просто ориентиром для накладных расходов,
Вот код, который я пытался использовать для этой цели:
#include <iostream>
#include <vector>
#include <chrono>
#include <ctime>
#include <memory>
class Interface
{
public:
virtual double calculate(double t) = 0;
virtual ~Interface() = default;
};
class Square
:
public Interface
{
public:
double calculate(double d)
{
return d*d;
}
};
class SquareStaticFunction
{
public:
static double calculate(double d)
{
return d*d;
}
};
class SquareMemberFunction
{
public:
double calculate(double d)
{
return d*d;
}
};
template<typename Function>
class Generic
:
public Function
{
public:
using Function::calculate;
};
using namespace std;
int main(int argc, const char *argv[])
{
vector<double> test(1e06, 5);
unique_ptr<Interface> sUptr(new Square());
Interface* sPtr = new Square();
Generic<SquareStaticFunction> gStatic;
Generic<SquareMemberFunction> gMember;
double result;
typedef std::chrono::high_resolution_clock Clock;
auto start = Clock::now();
for (auto d : test)
{
result = d * d;
}
auto end = Clock::now();
auto noFunction = end - start;
start = Clock::now();
for (auto d : test)
{
result = sUptr->calculate(d);
}
end = Clock::now();
auto virtualMemberFunction = end - start;
start = Clock::now();
for (auto d : test)
{
result = sPtr->calculate(d);
}
end = Clock::now();
auto virtualMemberFunctionRaw = end - start;
start = Clock::now();
for (auto d : test)
{
result = gStatic.calculate(d);
}
end = Clock::now();
auto staticPolicy = end - start;
start = Clock::now();
for (auto d : test)
{
result = gMember.calculate(d);
}
end = Clock::now();
auto memberPolicy = end - start;
cout << noFunction.count() << " " << virtualMemberFunction.count()
<< " " << virtualMemberFunctionRaw.count()
<< " " << staticPolicy.count()
<< " " << memberPolicy.count() << endl;
delete sPtr;
sPtr = nullptr;
return 0;
}
Я скомпилировал код с помощью gcc 4.8.2 и на машине Linux x86_64 со следующей моделью процессора: Intel (R) Core (TM) i7-4700MQ CPU @2.40GHz.
Доступ к функции виртуального члена осуществляется в одном тесте через необработанный указатель, а другой - через unique_ptr
. Сначала я скомпилировал код без каких-либо оптимизаций:
g++ -std=c++11 main.cpp -o main
и выполнил 1000 тестов со следующей командой оболочки:
for i in {1..1000}; do ./main >> results; done
Файл результатов, который я построил, используя следующий gnuplot
script (отметить логарифмическую ось y):
set terminal png size 1600,800
set logscale y
set key out vert right top
set out 'results.png'
plot 'results' using 0:1 title "no function" , \
'results' using 0:2 title "virtual member function (unique ptr)", \
'results' using 0:3 title "virtual member function (raw ptr)", \
'results' using 0:4 title "static policy", \
'results' using 0:5 title 'member function policy'
Для неоптимизированного кода диаграмма выглядит так:
Q1 Действительно ли вызов виртуальной функции через unique_ptr
становится самым дорогим, потому что он включает перенаправление, когда разыменования указатель на управляемый объект?
Затем я включил оптимизацию и скомпилировал код с помощью:
g++ -std=c++11 -O3 main.cpp -o main
что привело к следующей диаграмме:
Q2: Являются ли виртуальные члены наиболее дорогостоящими в этом случае, поскольку при доступе через указатель базового класса или ссылку (отправка виртуальной таблицы включена), невозможно для компилятор, чтобы сделать их встроенными?
Q3: Этот вопрос заставил меня опубликовать все это: как в оптимизированной диаграмме возможно, что статические и членские политики в конечном итоге быстрее, чем развернутый код для этого простого примера?
Изменить: создание result
volatile
и компиляция с включенными оптимизациями увеличивает время выполнения политик намного больше, но они похожи на код необработанного умножения:
Изменить, изменив код так, чтобы результат был добавлен вместо назначенного (предложенный dyk в комментариях) без использования volatile
:
result += ...
с той же диаграммой, что и для исходного кода.