Ta odpowiedź ma na celu wnieść do zestawu istniejących odpowiedzi coś, co uważam za bardziej znaczący punkt odniesienia dla kosztów wywołań std :: function w czasie wykonywania.
Mechanizm std :: function powinien być rozpoznawany ze względu na to, co zapewnia: Każda wywoływalna jednostka może zostać przekonwertowana na funkcję std :: z odpowiednim podpisem. Załóżmy, że masz bibliotekę, która dopasowuje powierzchnię do funkcji zdefiniowanej przez z = f (x, y), możesz napisać ją tak, aby akceptowała a std::function<double(double,double)>
, a użytkownik biblioteki może łatwo przekonwertować dowolną wywoływalną jednostkę na tę; czy to zwykła funkcja, metoda instancji klasy, lambda, czy cokolwiek, co jest obsługiwane przez std :: bind.
W przeciwieństwie do podejść opartych na szablonach, działa to bez konieczności ponownej kompilacji funkcji biblioteki dla różnych przypadków; odpowiednio, trochę dodatkowego skompilowanego kodu jest potrzebne dla każdego dodatkowego przypadku. Zawsze było to możliwe, ale wymagało to pewnych niewygodnych mechanizmów, a użytkownik biblioteki prawdopodobnie musiałby skonstruować adapter wokół swojej funkcji, aby działała. std :: function automatycznie konstruuje dowolny adapter potrzebny do uzyskania wspólnego interfejsu wywołań w czasie wykonywania dla wszystkich przypadków, co jest nową i bardzo potężną funkcją.
Moim zdaniem jest to najważniejszy przypadek użycia std :: function, jeśli chodzi o wydajność: interesuje mnie koszt wielokrotnego wywoływania std :: function po jej skonstruowaniu i musi być sytuacją, w której kompilator nie jest w stanie zoptymalizować wywołania, znając faktycznie wywoływaną funkcję (tj. musisz ukryć implementację w innym pliku źródłowym, aby uzyskać odpowiedni test porównawczy).
Zrobiłem test poniżej, podobny do PO; ale główne zmiany to:
- Każdy przypadek jest zapętlony 1 miliard razy, ale obiekty std :: function są konstruowane tylko raz. Patrząc na kod wyjściowy, odkryłem, że 'operator new' jest wywoływany podczas konstruowania rzeczywistych wywołań std :: function (może nie, gdy są zoptymalizowane).
- Test jest podzielony na dwa pliki, aby zapobiec niepożądanej optymalizacji
- Moje przypadki to: (a) funkcja jest wbudowana (b) funkcja jest przekazywana przez zwykły wskaźnik funkcji (c) funkcja jest zgodną funkcją opakowaną jako std :: function (d) funkcja jest niekompatybilną funkcją zgodną ze standardem std :: bind, opakowany jako std :: function
Wyniki, które otrzymuję to:
case (a) (inline) 1,3 nsec
wszystkie inne przypadki: 3,3 ns.
Przypadek (d) wydaje się być nieco wolniejszy, ale różnica (około 0,05 ns) jest pochłaniana przez hałas.
Wniosek jest taki, że funkcja std :: jest porównywalna narzutem (w czasie wywołania) do używania wskaźnika funkcji, nawet jeśli istnieje prosta adaptacja „bind” do rzeczywistej funkcji. Inline jest 2 ns szybsze niż inne, ale jest to oczekiwany kompromis, ponieważ inline jest jedynym przypadkiem, który jest `` podłączony na stałe '' w czasie wykonywania.
Kiedy uruchamiam kod Johana-Lundberga na tej samej maszynie, widzę około 39 nsec na pętlę, ale w pętli jest o wiele więcej, w tym rzeczywisty konstruktor i destruktor funkcji std ::, która jest prawdopodobnie dość wysoka ponieważ obejmuje nowe i usuń.
-O2 gcc 4.8.1, do celu x86_64 (core i5).
Uwaga, kod jest podzielony na dwa pliki, aby uniemożliwić kompilatorowi rozszerzenie funkcji, w których są wywoływane (z wyjątkiem jednego przypadku, w którym jest przeznaczony).
----- pierwszy plik źródłowy --------------
#include <functional>
// simple funct
float func_half( float x ) { return x * 0.5; }
// func we can bind
float mul_by( float x, float scale ) { return x * scale; }
//
// func to call another func a zillion times.
//
float test_stdfunc( std::function<float(float)> const & func, int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with a function pointer
float test_funcptr( float (*func)(float), int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func(x);
}
return y;
}
// same thing with inline function
float test_inline( int nloops ) {
float x = 1.0;
float y = 0.0;
for(int i =0; i < nloops; i++ ){
y += x;
x = func_half(x);
}
return y;
}
----- drugi plik źródłowy -------------
#include <iostream>
#include <functional>
#include <chrono>
extern float func_half( float x );
extern float mul_by( float x, float scale );
extern float test_inline( int nloops );
extern float test_stdfunc( std::function<float(float)> const & func, int nloops );
extern float test_funcptr( float (*func)(float), int nloops );
int main() {
using namespace std::chrono;
for(int icase = 0; icase < 4; icase ++ ){
const auto tp1 = system_clock::now();
float result;
switch( icase ){
case 0:
result = test_inline( 1e9);
break;
case 1:
result = test_funcptr( func_half, 1e9);
break;
case 2:
result = test_stdfunc( func_half, 1e9);
break;
case 3:
result = test_stdfunc( std::bind( mul_by, std::placeholders::_1, 0.5), 1e9);
break;
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
std::cout << result<< std::endl;
}
return 0;
}
Dla zainteresowanych, oto adapter, który kompilator zbudował tak, aby „mul_by” wyglądał jak float (float) - jest to „nazywane”, gdy wywoływana jest funkcja utworzona jako bind (mul_by, _1,0.5):
movq (%rdi), %rax ; get the std::func data
movsd 8(%rax), %xmm1 ; get the bound value (0.5)
movq (%rax), %rdx ; get the function to call (mul_by)
cvtpd2ps %xmm1, %xmm1 ; convert 0.5 to 0.5f
jmp *%rdx ; jump to the func
(więc mogłoby być trochę szybciej, gdybym napisał 0.5f w bind ...) Zauważ, że parametr 'x' pojawia się w% xmm0 i po prostu tam pozostaje.
Oto kod w obszarze, w którym konstruowana jest funkcja, przed wywołaniem testu_stdfunc - uruchom przez c ++ filt:
movl $16, %edi
movq $0, 32(%rsp)
call operator new(unsigned long) ; get 16 bytes for std::function
movsd .LC0(%rip), %xmm1 ; get 0.5
leaq 16(%rsp), %rdi ; (1st parm to test_stdfunc)
movq mul_by(float, float), (%rax) ; store &mul_by in std::function
movl $1000000000, %esi ; (2nd parm to test_stdfunc)
movsd %xmm1, 8(%rax) ; store 0.5 in std::function
movq %rax, 16(%rsp) ; save ptr to allocated mem
;; the next two ops store pointers to generated code related to the std::function.
;; the first one points to the adaptor I showed above.
movq std::_Function_handler<float (float), std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_invoke(std::_Any_data const&, float), 40(%rsp)
movq std::_Function_base::_Base_manager<std::_Bind<float (*(std::_Placeholder<1>, double))(float, float)> >::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation), 32(%rsp)
call test_stdfunc(std::function<float (float)> const&, int)
std::function
wtedy i tylko wtedy, gdy faktycznie potrzebujesz heterogenicznego zbioru wywoływalnych obiektów (tj. Żadne dalsze informacje rozróżniające nie są dostępne w czasie wykonywania).