Dzięki C ++ 11 otrzymaliśmy std::function
rodzinę wrapperów funktorów. Niestety ciągle słyszę tylko złe rzeczy o tych nowych dodatkach. Najbardziej popularne jest to, że są strasznie powolne. Przetestowałem to i naprawdę są do niczego w porównaniu z szablonami.
#include <iostream>
#include <functional>
#include <string>
#include <chrono>
template <typename F>
float calc1(F f) { return -1.0f * f(3.3f) + 666.0f; }
float calc2(std::function<float(float)> f) { return -1.0f * f(3.3f) + 666.0f; }
int main() {
using namespace std::chrono;
const auto tp1 = system_clock::now();
for (int i = 0; i < 1e8; ++i) {
calc1([](float arg){ return arg * 0.5f; });
}
const auto tp2 = high_resolution_clock::now();
const auto d = duration_cast<milliseconds>(tp2 - tp1);
std::cout << d.count() << std::endl;
return 0;
}
111 ms w porównaniu z 1241 ms. Zakładam, że dzieje się tak dlatego, że szablony można ładnie wstawiać, podczas function
gdy wewnętrzne funkcje obejmują wirtualne połączenia.
Oczywiście szablony mają swoje problemy, tak jak je widzę:
- muszą być podane jako nagłówki, co nie jest czymś, czego możesz nie chcieć robić, udostępniając swoją bibliotekę jako zamknięty kod,
- mogą znacznie wydłużyć czas kompilacji, chyba że
extern template
zostanie wprowadzona taka polityka, - nie ma (przynajmniej mi znanego) czystego sposobu przedstawienia wymagań (pojęć, kogokolwiek?) szablonu, poza komentarzem opisującym, jakiego rodzaju funktora się oczekuje.
Czy mogę więc założyć, że function
s może być de facto stosowany jako standard funktorów mijających i tam, gdzie oczekuje się wysokiej wydajności, należy używać szablonów?
Edytować:
Mój kompilator to Visual Studio 2012 bez CTP.
c++
templates
c++11
std-function
Czerwony XIII
źródło
źródło
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).std::function
ani szablony”. Myślę, że tutaj problemem jest po prostu zawijanie lambdy,std::function
a nie zawijanie lambdystd::function
. W tej chwili twoje pytanie jest jak pytanie „czy wolę jabłko czy miskę?”Odpowiedzi:
Ogólnie rzecz biorąc, jeśli masz do czynienia z sytuacją projektową, która daje wybór, użyj szablonów . Podkreśliłem słowo design, ponieważ uważam, że należy się skupić na rozróżnieniu między przypadkami użycia
std::function
a szablonami, które są całkiem inne.Ogólnie rzecz biorąc, wybór szablonów jest tylko przykładem szerszej zasady: spróbuj określić jak najwięcej ograniczeń w czasie kompilacji . Uzasadnienie jest proste: jeśli uda Ci się wykryć błąd lub niezgodność typu, nawet przed wygenerowaniem programu, nie wyślesz klientowi błędnego programu.
Ponadto, jak słusznie zauważyłeś, wywołania funkcji szablonu są rozwiązywane statycznie (tj. W czasie kompilacji), więc kompilator ma wszystkie niezbędne informacje do optymalizacji i ewentualnie wbudowania kodu (co nie byłoby możliwe, gdyby wywołanie zostało wykonane za pomocą vtable).
Tak, to prawda, że obsługa szablonów nie jest doskonała, a C ++ 11 wciąż nie obsługuje pojęć; jednak nie widzę, jak
std::function
by cię uratować pod tym względem.std::function
nie jest alternatywą dla szablonów, ale raczej narzędziem w sytuacjach projektowych, w których nie można używać szablonów.Jeden taki przypadek użycia pojawia się, gdy trzeba rozwiązać wywołanie w czasie wykonywania , wywołując wywoływalny obiekt, który jest zgodny z określonym podpisem, ale którego konkretny typ jest nieznany w czasie kompilacji. Dzieje się tak zazwyczaj, gdy masz kolekcję wywołań zwrotnych potencjalnie różnych typów , ale które musisz wywołać w jednolity sposób ; typ i liczba zarejestrowanych wywołań zwrotnych jest określana w czasie wykonywania na podstawie stanu programu i logiki aplikacji. Niektóre z tych wywołań zwrotnych mogą być funktorami, niektóre mogą być zwykłymi funkcjami, a niektóre mogą być wynikiem powiązania innych funkcji z określonymi argumentami.
std::function
astd::bind
także oferują naturalny idiom umożliwiający programowanie funkcjonalne w C ++, w którym funkcje są traktowane jako obiekty i są naturalnie przetwarzane i łączone w celu wygenerowania innych funkcji. Chociaż ten rodzaj kombinacji można również osiągnąć za pomocą szablonów, podobna sytuacja projektowa zwykle występuje wraz z przypadkami użycia, które wymagają określenia typu połączonych wywoływalnych obiektów w czasie wykonywania.Wreszcie są inne sytuacje, w których
std::function
jest to nieuniknione, np. Jeśli chcesz napisać rekurencyjne lambdy ; jednak te ograniczenia są bardziej podyktowane ograniczeniami technologicznymi niż rozróżnieniami pojęciowymi, jak sądzę.Podsumowując, skup się na projektowaniu i spróbuj zrozumieć, jakie są koncepcyjne przypadki użycia tych dwóch konstrukcji. Jeśli porównasz je tak, jak to zrobiłeś, zmuszasz ich do wejścia na arenę, do której prawdopodobnie nie należą.
źródło
std::function
końcówkę pamięci i szablonFun
interfejsu”.unique_ptr<void>
Wywoływanie odpowiednich destruktorów nawet dla typów bez wirtualnych destruktorów).Andy Prowl ładnie omówił problemy projektowe. Jest to oczywiście bardzo ważne, ale uważam, że pierwotne pytanie dotyczy większej liczby problemów związanych z wydajnością
std::function
.Przede wszystkim krótka uwaga na temat techniki pomiaru: 11ms uzyskane dla
calc1
nie ma żadnego znaczenia. Rzeczywiście, patrząc na wygenerowany zestaw (lub debugowanie kodu asemblera), można zauważyć, że optymalizator VS2012 jest wystarczająco sprytny, aby zdać sobie sprawę, że wynik wywołaniacalc1
jest niezależny od iteracji i przenosi wywołanie poza pętlę:Ponadto zdaje sobie sprawę, że wywołanie
calc1
nie ma widocznego efektu i całkowicie je odrzuca. Dlatego 111 ms to czas potrzebny na uruchomienie pustej pętli. (Dziwię się, że optymalizator zachował pętlę). Dlatego uważaj na pomiary czasu w pętlach. To nie jest tak proste, jak mogłoby się wydawać.Jak już wspomniano, optymalizator ma więcej problemów ze zrozumieniem
std::function
i nie przenosi wywołania z pętli. Zatem 1241 ms to rzetelna miara dlacalc2
.Zauważ, że
std::function
jest w stanie przechowywać różne typy wywoływalnych obiektów. W związku z tym musi wykonać jakąś magię usuwania typów dla magazynu. Ogólnie oznacza to dynamiczną alokację pamięci (domyślnie przez wywołanienew
). Powszechnie wiadomo, że jest to dość kosztowna operacja.Standard (20.8.11.2.1 / 5) koduje implementacje, aby uniknąć dynamicznej alokacji pamięci dla małych obiektów, co, na szczęście, VS2012 tak robi (w szczególności dla oryginalnego kodu).
Aby zorientować się, o ile wolniej może się to stać w przypadku alokacji pamięci, zmieniłem wyrażenie lambda na trzy
float
sekundy. To sprawia, że wywoływalny obiekt jest zbyt duży, aby zastosować optymalizację małych obiektów:W przypadku tej wersji czas wynosi około 16000 ms (w porównaniu do 1241 ms dla oryginalnego kodu).
Na koniec zwróć uwagę, że czas życia lambda obejmuje czas trwania
std::function
. W takim przypadku zamiast przechowywać kopię lambda,std::function
można zapisać do niej „odniesienie”. Przez „odniesienie” rozumiem element,std::reference_wrapper
który można łatwo zbudować za pomocą funkcjistd::ref
istd::cref
. Dokładniej, używając:czas zmniejsza się do około 1860 ms.
Pisałem o tym jakiś czas temu:
http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059
Jak powiedziałem w artykule, argumenty nie do końca dotyczą VS2010 ze względu na jego słabą obsługę C ++ 11. W momencie pisania tego tekstu dostępna była tylko wersja beta VS2012, ale jej obsługa C ++ 11 była już wystarczająco dobra w tej kwestii.
źródło
calc1
można przyjąćfloat
argument, który byłby wynikiem poprzedniej iteracji. Coś jakx = calc1(x, [](float arg){ return arg * 0.5f; });
. Ponadto musimy zapewnić, żecalc1
używax
. Ale to jeszcze nie wystarczy. Musimy stworzyć efekt uboczny. Np. Po pomiarze wydrukx
na ekranie. Mimo to zgadzam się, że używanie kodów zabawek do pomiarów timimg nie zawsze może dać idealne wskazanie, co się stanie z rzeczywistym / produkcyjnym kodem.std::reference_wrapper
(do wymuszania szablonów; nie tylko do ogólnego przechowywania) i zabawne jest widzieć, że optymalizator VS nie odrzuca pustej pętli ... jak zauważyłem z tym błędem GCC ponownievolatile
.Dzięki Clang nie ma między nimi różnicy w wydajności
Używając clang (3.2, trunk 166872) (-O2 w Linuksie), pliki binarne z dwóch przypadków są w rzeczywistości identyczne .
-Wrócę, by dzwonić na końcu postu. Ale najpierw gcc 4.7.2:
Jest już wiele wglądu, ale chcę zwrócić uwagę, że wyniki obliczeń calc1 i calc2 nie są takie same, z powodu wbudowania itp. Porównaj na przykład sumę wszystkich wyników:
z calc2 staje się
podczas gdy z calc1 staje się
jest to współczynnik ~ 40 w przypadku różnicy prędkości i ~ 4 współczynnik w wartościach. Pierwsza to znacznie większa różnica niż opublikowana przez OP (przy użyciu programu Visual Studio). Właściwie wydrukowanie wartości a end jest również dobrym pomysłem, aby zapobiec usunięciu przez kompilator kodu bez widocznego wyniku (reguła as-if). Cassio Neri już to powiedział w swojej odpowiedzi. Zwróć uwagę, jak różne są wyniki - należy zachować ostrożność podczas porównywania współczynników szybkości kodów wykonujących różne obliczenia.
Ponadto, aby być uczciwym, porównywanie różnych sposobów wielokrotnego obliczania f (3.3) może nie jest takie interesujące. Jeśli wejście jest stałe, nie powinno być w pętli. (Optymalizator może łatwo zauważyć)
Jeśli dodam argument wartości podany przez użytkownika do funkcji calc1 i 2, współczynnik prędkości między calc1 i calc2 sprowadza się do współczynnika 5, z 40! W przypadku programu Visual Studio różnica jest zbliżona do współczynnika 2, a przy dźwiękach nie ma różnicy (patrz poniżej).
Ponieważ mnożenie jest szybkie, mówienie o czynnikach spowolnienia często nie jest tak interesujące. Bardziej interesującym pytaniem jest, jak małe są twoje funkcje i czy te wywołania są wąskim gardłem w prawdziwym programie?
Szczęk:
Clang (użyłem 3.2) faktycznie generował identyczne pliki binarne, kiedy przełączam się między calc1 i calc2 dla przykładowego kodu (zamieszczonego poniżej). W przypadku oryginalnego przykładu zamieszczonego w pytaniu oba są również identyczne, ale nie zajmują wcale czasu (pętle są po prostu całkowicie usuwane, jak opisano powyżej). W moim zmodyfikowanym przykładzie z -O2:
Liczba sekund do wykonania (najlepsza z 3):
Obliczone wyniki wszystkich plików binarnych są takie same, a wszystkie testy zostały wykonane na tej samej maszynie. Byłoby interesujące, gdyby ktoś z głębszą wiedzą na temat clang lub VS mógł skomentować, jakie optymalizacje zostały wprowadzone.
Mój zmodyfikowany kod testowy:
Aktualizacja:
Dodano vs2015. Zauważyłem również, że istnieją konwersje double-> float w calc1, calc2. Usunięcie ich nie zmienia wniosku dla Visual Studio (oba są o wiele szybsze, ale stosunek jest mniej więcej taki sam).
źródło
Różne to nie to samo.
Jest wolniejszy, ponieważ robi rzeczy, których szablon nie może zrobić. W szczególności umożliwia wywołanie dowolnej funkcji, którą można wywołać z podanymi typami argumentów i której typ zwracany jest konwertowany na podany typ zwracany z tego samego kodu .
Zauważ, że ten sam obiekt funkcji,,
fun
jest przekazywany do obu wywołań funkcjieval
. Pełni dwie różne funkcje.Jeśli nie musisz tego robić, to należy nie używać
std::function
.źródło
Masz już tutaj kilka dobrych odpowiedzi, więc nie zamierzam im zaprzeczać, w skrócie porównywanie std :: function do szablonów jest jak porównywanie funkcji wirtualnych do funkcji. Nigdy nie powinieneś „preferować” funkcji wirtualnych od funkcji, ale raczej używasz funkcji wirtualnych, gdy pasuje to do problemu, przenosząc decyzje z czasu kompilacji do czasu wykonania. Pomysł polega na tym, że zamiast rozwiązywać problem za pomocą niestandardowego rozwiązania (takiego jak tabela skoków), używasz czegoś, co daje kompilatorowi większą szansę na optymalizację dla Ciebie. Pomaga również innym programistom, jeśli używasz standardowego rozwiązania.
źródło
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:
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 --------------
----- drugi plik źródłowy -------------
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):
(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:
źródło
Wyniki są dla mnie bardzo interesujące, więc trochę poszperałem, aby zrozumieć, co się dzieje. Po pierwsze, jak powiedziało wielu innych, bez wpływu wyników obliczeń na stan programu, który kompilator po prostu zoptymalizuje. Po drugie, mając stałą wartość 3,3 podaną jako uzbrojenie do wywołania zwrotnego, podejrzewam, że będą miały miejsce inne optymalizacje. Mając to na uwadze, zmieniłem trochę kod twojego testu porównawczego.
Biorąc pod uwagę tę zmianę w kodzie, skompilowałem z gcc 4.8 -O3 i otrzymałem czas 330 ms dla calc1 i 2702 dla calc2. Więc użycie szablonu było 8 razy szybsze, ta liczba wydawała mi się podejrzana, prędkość o potędze 8 często wskazuje, że kompilator coś zwektoryzował. kiedy spojrzałem na wygenerowany kod dla wersji szablonów, był on wyraźnie zwektoryzowany
Gdzie nie było wersji std :: function. Ma to dla mnie sens, ponieważ z szablonem kompilator wie na pewno, że funkcja nigdy się nie zmieni w pętli, ale przy przekazaniu funkcji std :: może się zmienić, dlatego nie można jej wektoryzować.
To skłoniło mnie do wypróbowania czegoś innego, aby sprawdzić, czy uda mi się zmusić kompilator do wykonania tej samej optymalizacji w wersji std :: function. Zamiast przekazywać funkcję, tworzę std :: function jako zmienną globalną i wywołuję to.
W tej wersji widzimy, że kompilator teraz zwektoryzował kod w ten sam sposób i otrzymuję te same wyniki testów porównawczych.
Tak więc mój wniosek jest taki, że surowa prędkość funkcji std :: function w porównaniu z funktorem szablonowym jest prawie taka sama. Jednak to znacznie utrudnia pracę optymalizatora.
źródło
calc3
sprawa nie ma sensu; Calc3 jest teraz zakodowany na stałe do wywołania f2. Oczywiście można to zoptymalizować.