Подтвердить что ты не робот

Как измерить накладные расходы на вызовы функций?

Я хотел измерить и сравнить накладные расходы на различные вызовы функций. Разные в смысле двух альтернативных способов борьбы с расширением класса при минимизации модификации кода:

  • с использованием абстрактного базового класса и обеспечения реализаций в виртуальных функциях-членах
  • с использованием класса хоста политики и определения различных политик со статическими и функциями-членами

Оба этих параметра сравниваются с вызовом никакой функции вообще. Я также знаю, что 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 += ...

с той же диаграммой, что и для исходного кода.

4b9b3361

Ответ 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) лучше де виализуют, потому что они могут сделать лучшее предположение из информации о времени выполнения.