Почему в данном случае вызов std::function такой медленный?
Ниже пример моей реализации. Не важно, что именно делают функции класса MyClass. Задача понять, насколько данная реализация хороша в плане производительности.
class MyFunctor
{
public:
//MyFunctor(double (*func)()) : _func(func) {}; // can't do it
MyFunctor(std::function<double(void)> func): _func(func) {};
~MyFunctor() {};
double operator()() { return _func(); };
private:
//double (*_func)();
std::function<double(void)> _func;
/*rest implementation*/
};
class MyClass1
{
double _b{};
double _c{};
double _a{};
vector<MyFunctor> _functors{};
double func() { return (std::sin(_a) + std::cos(_b)); };
double anotherFunc() { return _a * _a + _b * _b; };
public:
MyClass1(double a) : _a(a), _b(a), _c(a)
{
_functors.push_back(MyFunctor{ [this]() {return func(); } });
_functors.push_back(MyFunctor{ [this]() {return func() * func(); } });
_functors.push_back(MyFunctor{ [this]() {return _a*anotherFunc(); } });
_functors.push_back(MyFunctor{ [this]() {return _a + _b + _c; } });
}
vector<MyFunctor> getFunctors() { return _functors; }
// FOR PERFORMANCE COMPARISON (do the same as functors)
double F1() { return func(); };
double F2() { return func() * func(); };
double F3() { return _a*anotherFunc(); };
double F4() { return _a + _b + _c; };
~MyClass1() {}
};
/*
* .. i have many classes MyClass2, MyClass3, ... with getFunctors() function
*/
int main()
{
using std::chrono::duration_cast;
using clock_t = std::chrono::high_resolution_clock;
using millisecond_t = std::chrono::duration<double, std::milli>;
std::chrono::time_point<clock_t> tic;
double elapsed;
constexpr int N = 1000'000;
MyClass1 C{ 1 };
MyClass1 C2{ 2 };
//MyClass2 D{ 3 }; // etc..
auto functors = C.getFunctors(); // can also contain functors of other objects
/* TEST FUNCTORS */
double sum{};
sum = 0.0;
tic = clock_t::now();
for (size_t i = 0; i < N; i++)
{
sum = 0;
auto iter = functors.begin();
while (iter != functors.end())
{
sum += (*iter)();
++iter;
}
}
elapsed = duration_cast<millisecond_t>(clock_t::now() - tic).count();
std::cout << "\t(functors) elapsed: " << elapsed << " millisec\t";
std::cout << sum << "\n";
/* TEST MEMBERS */
sum = 0.0;
tic = clock_t::now();
for (size_t i = 0; i < N; i++)
{
sum = C.F1() + C.F2() + C.F3() + C.F4();
}
elapsed = duration_cast<millisecond_t>(clock_t::now() - tic).count();
std::cout << "\t(members) elapsed: " << elapsed << " millisec\t";
std::cout << sum << "\n";
return 0;
}
Вывод:
(functors) elapsed: 117.828 millisec 8.29107
(members) elapsed: 1.4552 millisec 8.29107
В данном случае вызов функторов в ~100 раз медленнее, чем вызов напрямую методов класса.
msvc and gcc дают похожие результаты.
В моем приложении каждый функтор имеет несколько флагов, и в зависимости от значений этих флагов надо получить вектор значений функторов, удовлетворяющих некоторым условиям. Например, в функторах есть флаг, enable, и надо сформировать вектор double-ов - состоящий из значений функторов, для которых enable = true, или false. Получать такие векторы надо очень быстро после изменения флагов (на самом деле флаги - это состояния state machine).
Меня пугает такая разница в производительности, т.к. именно вычисление подобных функторов - основная нагрузка на производительность. А производительность на первом месте.
Стоит ли избегать в таких случаях std::function? Если да, то можно ли реализовать такую идею другим способом? Или проблема не в std::function, а в моей криворукости? Т.к. опыта в C++ я только набираюсь, хотелось бы услышать советы от экспертов.
Спасибо.
UPD: добавляю полный текст примера
// CMakeProject1.cpp : Defines the entry point for the application.
#include <functional>
#include <iostream>
#include <chrono>
using std::vector;
using std::string;
class MyFunctor
{
public:
//Eq(double (*func)()) : _func(func) {}; // can't do it
MyFunctor(std::function<double(void)> func): _func(func) {};
~MyFunctor() {};
double operator()() { return _func(); };
private:
//double (*_func)();
std::function<double(void)> _func;
/*rest implementation*/
};
class MyClass1
{
double _b{};
double _c{};
double _a{};
vector<MyFunctor> _functors{};
double func() { return (std::sin(_a) + std::cos(_b)); };
double anotherFunc() { return _a * _a + _b * _b; };
public:
MyClass1(double a) : _a(a), _b(a), _c(a)
{
_functors.push_back(MyFunctor{ [this]() {return func(); } });
_functors.push_back(MyFunctor{ [this]() {return func() * func(); } });
_functors.push_back(MyFunctor{ [this]() {return anotherFunc(); } });
_functors.push_back(MyFunctor{ [this]() {return _a + _b + _c; } });
}
vector<MyFunctor> getFunctors() { return _functors; }
double F1() { return func(); };
double F2() { return func() * func(); };
double F3() { return anotherFunc(); };
double F4() { return _a + _b + _c; };
~MyClass1() {}
};
/*
* .. i have many classes MyClass2, MyClass3 ...
*/
int main()
{
using std::chrono::duration_cast;
using clock_t = std::chrono::high_resolution_clock;
using millisecond_t = std::chrono::duration<double, std::milli>;
std::chrono::time_point<clock_t> tic;
double elapsed;
constexpr int N = 1000'000;
MyClass1 C{ 1 };
MyClass1 C2{ 2 };
//MyClass2 D{ 3 }; // etc..
auto functors = C.getFunctors(); // also contains functors of other objects
/* TEST FUNCTORS */
double sum{};
sum = 0.0;
tic = clock_t::now();
for (size_t i = 0; i < N; i++)
{
sum = 0;
//for (auto& fu : functors)
// sum += fu();
//sum = functors[0]() + functors[1]() + functors[2]() + functors[3]();
auto iter = functors.begin();
while (iter != functors.end())
{
sum += (*iter)();
++iter;
}
}
elapsed = duration_cast<millisecond_t>(clock_t::now() - tic).count();
std::cout << "\t(functors) elapsed: " << elapsed << " millisec\t";
std::cout << sum << "\n";
/* TEST MEMBERS */
sum = 0.0;
tic = clock_t::now();
for (size_t i = 0; i < N; i++)
{
sum = C.F1() + C.F2() + C.F3() + C.F4();
}
elapsed = duration_cast<millisecond_t>(clock_t::now() - tic).count();
std::cout << "\t(members) elapsed: " << elapsed << " millisec\t";
std::cout << sum << "\n";
return 0;
}
CMakeLists.txt
# CMakeList.txt : CMake project for CMakeProject1, include source and define
# project specific logic here.
#
cmake_minimum_required (VERSION 3.8)
project ("CMakeProject1")
set(CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_FLAGS "-O3")
# Add source to this project's executable.
add_executable (CMakeProject1
"CMakeProject1.cpp"
#"CMakeProject1.h"
#"mvsignal.h"
#"timer.h"
)
Ответы (1 шт):
В медленном варианте вы вызываете функции не прямо, а из массива указателей на функции. Так как этот массив функций может в любой момент поменять значения указателей на функции оптимизатор ничего не может ускорить, а выполняет то что вы написали. Ассемблер :
.L135:
movq 24(%rsp), %rbx # MEM[(struct MyFunctor * const &)&functors], ivtmp.321
# functory.cpp:84: sum = 0;
movq $0x000000000, (%rsp) #, %sfp
.L134:
# functory.cpp:90: while (iter != functors.end())
cmpq %rbx, 32(%rsp) # ivtmp.321, MEM[(struct MyFunctor * const &)&functors + 8]
je .L132 #,
# /usr/include/c++/7/bits/std_function.h:704: if (_M_empty())
cmpq $0, 16(%rbx) #, MEM[base: iter$_M_current_40, offset: 16B]
jne .L133 #,
.LEHB26:
# /usr/include/c++/7/bits/std_function.h:705: __throw_bad_function_call();
call _ZSt25__throw_bad_function_callv #
.L133:
# /usr/include/c++/7/bits/std_function.h:706: return _M_invoker(_M_functor, std::forward<_ArgTypes>(__args)...);
movq %rbx, %rdi # ivtmp.321,
call *24(%rbx) # MEM[base: iter$_M_current_40, offset: 24B]
# functory.cpp:92: sum += (*iter)();
addsd (%rsp), %xmm0 # %sfp, _88
addq $32, %rbx #, ivtmp.321
movsd %xmm0, (%rsp) # _88, %sfp
jmp .L134 #
.L132:
# functory.cpp:82: for (size_t i = 0; i < N; i++)
decq %rbp # ivtmp_43
jne .L135 #,
Быстрый вариант :
# functory.cpp:49: double F1() { return func(); };
leaq 48(%rsp), %rdi #, tmp194
# functory.cpp:103: tic = clock_t::now();
movq %rax, %rbx #, tic
# functory.cpp:49: double F1() { return func(); };
call _ZN8MyClass14funcEv #
# functory.cpp:36: double anotherFunc() { return _a * _a + _b * _b; };
movsd 64(%rsp), %xmm1 # C._a, _97
movsd 48(%rsp), %xmm3 # C._b, _99
movaps %xmm1, %xmm2 # _97, tmp155
movaps %xmm3, %xmm4 # _99, tmp156
mulsd %xmm1, %xmm2 # _97, tmp155
# functory.cpp:52: double F4() { return _a + _b + _c; };
addsd %xmm3, %xmm1 # _99, tmp161
# functory.cpp:36: double anotherFunc() { return _a * _a + _b * _b; };
mulsd %xmm3, %xmm4 # _99, tmp156
# functory.cpp:52: double F4() { return _a + _b + _c; };
addsd 56(%rsp), %xmm1 # C._c, tmp162
# functory.cpp:36: double anotherFunc() { return _a * _a + _b * _b; };
addsd %xmm4, %xmm2 # tmp156, tmp157
# functory.cpp:50: double F2() { return func() * func(); };
movaps %xmm0, %xmm4 # _63, tmp158
mulsd %xmm0, %xmm4 # _63, tmp158
# functory.cpp:106: sum = C.F1() + C.F2() + C.F3() + C.F4();
addsd %xmm4, %xmm0 # tmp158, tmp159
addsd %xmm2, %xmm0 # tmp157, tmp160
addsd %xmm1, %xmm0 # tmp162, tmp160
movsd %xmm0, (%rsp) # tmp160, %sfp
Все математические операции выполняются прямо по тексту, не смотря на адреса функций (они вызываются статически). И самое главное: оптимизатор миллион раз передумал всё вычислять (бессмысленно). Оставил только одну итерацию, без цикла.
Пугаться скорости не надо. Если надо, чтобы массив функций был динамическим, меняя адреса функций, то так и быть. Пользуйтесь вектором.