Как измерить накладные расходы на вызовы функций?
Я хотел измерить и сравнить накладные расходы на различные вызовы функций. Разные в смысле двух альтернативных способов борьбы с расширением класса при минимизации модификации кода:
- с использованием абстрактного базового класса и обеспечения реализаций в виртуальных функциях-членах
- с использованием класса хоста политики и определения различных политик со статическими и функциями-членами
Оба этих параметра сравниваются с вызовом никакой функции вообще. Я также знаю, что 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'
Для неоптимизированного кода диаграмма выглядит так:
![Non-optimized function call overhead.]()
Q1 Действительно ли вызов виртуальной функции через unique_ptr
становится самым дорогим, потому что он включает перенаправление, когда разыменования указатель на управляемый объект?
Затем я включил оптимизацию и скомпилировал код с помощью:
g++ -std=c++11 -O3 main.cpp -o main
что привело к следующей диаграмме:
![enter image description here]()
Q2: Являются ли виртуальные члены наиболее дорогостоящими в этом случае, поскольку при доступе через указатель базового класса или ссылку (отправка виртуальной таблицы включена), невозможно для компилятор, чтобы сделать их встроенными?
Q3: Этот вопрос заставил меня опубликовать все это: как в оптимизированной диаграмме возможно, что статические и членские политики в конечном итоге быстрее, чем развернутый код для этого простого примера?
Изменить: создание result
volatile
и компиляция с включенными оптимизациями увеличивает время выполнения политик намного больше, но они похожи на код необработанного умножения:
![enter image description here]()
Изменить, изменив код так, чтобы результат был добавлен вместо назначенного (предложенный dyk в комментариях) без использования volatile
:
result += ...
с той же диаграммой, что и для исходного кода.
Ответы
Ответ 1
Глядя на разборку -O3 -march=native -std=c++11
на ваш код, показано, что компилятор делает "слишком большую" оптимизацию, обнаруживая ненужное повторное воздействие на ту же неиспользуемую переменную. Как было предложено в комментариях, я использовал +=
вместо =
. Я также инициализировал result = 0
и main
возвращает result
вместо 0
, чтобы убедиться, что компилятор вычисляет его значение. Этот модифицированный код дает:
-
noFunction
, staticPolicy
и memberPolicy
опускается как mulsd
, addsd
, addsd
, то есть скалярная инструкция SSE. Clang также не вектурирует (с вариантами ванили), но Intel icc делает (он генерирует векторные и не векторные версии и прыжки в зависимости от выравнивания и подсчета итераций).
-
virtualMemberFunction
и virtualMemberFunctionRaw
приводят к вызову динамической функции (без дешифрования и вставки)
Вы можете сами убедиться, вставив код здесь.
Чтобы ответить на ваш Q1 "указатель vs unique_ptr
в сборке отладки": в -O0
вызовы не встроены автоматически, в частности, unique_ptr::operator->
вызывается явно без вложения, так что 2 вызова функции на итерацию вместо 1 для обычных указателей. Эта разница исчезает для оптимизированных построек
Чтобы ответить на ваш Q2, можно ли встраивать виртуальные вызовы: в этом примере gcc и clang не строят вызов, потому что они, вероятно, не выполняют достаточно статического анализа. Но вы можете им помочь. Например, с clang 3.3 (но не 3.2, а не gcc), объявляющим метод как const
и __attribute((pure))
выполняет задание. В gcc (4.8, pre-4.9) я попытался маркировать метод как final
и скомпилировать с помощью -fwhole-program
, но это не устранило вызов. Так что да в этом конкретном случае можно де виртуализировать, но не надежно. В общем, jitted компиляторы (С#, Java) лучше де виализуют, потому что они могут сделать лучшее предположение из информации о времени выполнения.