std :: function vs template


161

Grazie a C ++ 11 abbiamo ricevuto la std::functionfamiglia di wrapper functor. Sfortunatamente, continuo a sentire solo cose negative su queste nuove aggiunte. Il più popolare è che sono orribilmente lenti. L'ho provato e fanno davvero schifo rispetto ai modelli.

#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 contro 1241 ms. Presumo che ciò sia dovuto al fatto che i modelli possono essere ben incorporati, mentre functioni modelli interni coprono le chiamate virtuali.

Ovviamente i modelli hanno i loro problemi come li vedo io:

  • devono essere forniti come intestazioni che non è qualcosa che potresti non voler fare quando rilasci la tua libreria come codice chiuso,
  • possono allungare il tempo di compilazione molto più a meno che non extern templatevenga introdotta una politica simile,
  • non esiste un modo (almeno per me noto) chiaro di rappresentare i requisiti (concetti, chiunque?) di un modello, escludere un commento che descriva quale tipo di funzione ci si aspetta.

Posso quindi supporre che functions possa essere usato come standard di fatto per i passanti e in luoghi in cui si dovrebbero usare modelli ad alte prestazioni?


Modificare:

Il mio compilatore è Visual Studio 2012 senza CTP.


16
Utilizzare std::functionse e solo se in realtà è necessaria una raccolta eterogenea di oggetti richiamabili (ovvero non sono disponibili ulteriori informazioni discriminanti in fase di esecuzione).
Kerrek SB,

30
Stai confrontando le cose sbagliate. I modelli sono usati in entrambi i casi - non è " std::functiono modelli". Penso che qui il problema sia semplicemente avvolgere un lambda in std::functionvs non avvolgere un lambda in std::function. Al momento la tua domanda è come chiedere "dovrei preferire una mela o una ciotola?"
Razze di leggerezza in orbita

7
Sia 1ns o 10ns, entrambi non sono niente.
IPC

23
@ipc: il 1000% non è niente però. Quando l'OP si identifica, inizi a prenderti cura quando la scalabilità vi entra per qualsiasi scopo pratico.
Razze di leggerezza in orbita

18
@ipc È 10 volte più lento, il che è enorme. La velocità deve essere confrontata con la linea di base; inganna pensare che non importa solo perché sono nanosecondi.
Paul Manta,

Risposte:


170

In generale, se stai affrontando una situazione progettuale che ti dà una scelta, usa i modelli . Ho sottolineato la parola design perché penso che ciò su cui devi concentrarti sia la distinzione tra casi d'uso std::functione modelli, che sono piuttosto diversi.

In generale, la scelta dei modelli è solo un'istanza di un principio più ampio: provare a specificare quanti più vincoli possibile al momento della compilazione . La logica è semplice: se riesci a rilevare un errore o una mancata corrispondenza del tipo, anche prima che il tuo programma venga generato, non spedirai un programma difettoso al tuo cliente.

Inoltre, come hai giustamente sottolineato, le chiamate alle funzioni del modello vengono risolte staticamente (cioè al momento della compilazione), quindi il compilatore ha tutte le informazioni necessarie per ottimizzare e possibilmente incorporare il codice (cosa che non sarebbe possibile se la chiamata fosse eseguita attraverso un vtable).

Sì, è vero che il supporto del modello non è perfetto e in C ++ 11 manca ancora un supporto per i concetti; tuttavia, non vedo come std::functionti salverebbe a tale riguardo. std::functionnon è un'alternativa ai modelli, ma piuttosto uno strumento per situazioni di progettazione in cui i modelli non possono essere utilizzati.

Uno di questi casi d'uso si presenta quando è necessario risolvere una chiamata in fase di esecuzione invocando un oggetto richiamabile che aderisce a una firma specifica, ma il cui tipo concreto è sconosciuto al momento della compilazione. Questo è in genere il caso in cui si dispone di una raccolta di callback di tipi potenzialmente diversi , ma che è necessario richiamare in modo uniforme ; il tipo e il numero dei callback registrati vengono determinati in fase di esecuzione in base allo stato del programma e alla logica dell'applicazione. Alcuni di questi callback potrebbero essere funzionali, altri potrebbero essere semplici funzioni, altri potrebbero essere il risultato del vincolo di altre funzioni a determinati argomenti.

std::functione std::bindoffre anche un linguaggio naturale per consentire la programmazione funzionale in C ++, in cui le funzioni sono trattate come oggetti e vengono naturalmente cagliate e combinate per generare altre funzioni. Sebbene questo tipo di combinazione possa essere realizzato anche con modelli, una situazione progettuale simile si presenta normalmente con casi d'uso che richiedono di determinare il tipo di oggetti richiamabili combinati in fase di esecuzione.

Infine, ci sono altre situazioni in cui std::functionè inevitabile, ad esempio se si desidera scrivere lambda ricorsive ; tuttavia, queste restrizioni sono più dettate dalle limitazioni tecnologiche che dalle distinzioni concettuali che credo.

Per riassumere, concentrati sul design e cerca di capire quali sono i casi d'uso concettuali per questi due costrutti. Se li metti a confronto come hai fatto, li stai forzando in un'arena a cui probabilmente non appartengono.


23
Penso "Questo è in genere il caso in cui si dispone di una raccolta di callback di tipi potenzialmente diversi, ma che è necessario invocare in modo uniforme;" è la parte importante. La mia regola empirica è: "Preferisci std::functionsul lato di archiviazione e modello Funsull'interfaccia".
R. Martinho Fernandes,

2
Nota: la tecnica di nascondere i tipi di calcestruzzo si chiama cancellazione dei tipi (da non confondere con la cancellazione dei tipi nelle lingue gestite). È spesso implementato in termini di polimorfismo dinamico, ma è più potente (ad es. unique_ptr<void>Chiamare distruttori appropriati anche per tipi senza distruttori virtuali).
ecatmur

2
@ecatmur: sono d'accordo sulla sostanza, anche se siamo leggermente disallineati sulla terminologia. Il polimorfismo dinamico significa per me "assumere forme diverse in fase di esecuzione", al contrario del polimorfismo statico che interpreto come "assumere forme diverse in fase di compilazione"; quest'ultimo non può essere raggiunto attraverso modelli. Per me, la cancellazione del tipo è, dal punto di vista del design, una sorta di precondizione per essere in grado di raggiungere il polimorfismo dinamico: hai bisogno di un'interfaccia uniforme per interagire con oggetti di diversi tipi e la cancellazione del tipo è un modo per astrarre il tipo- informazioni specifiche.
Andy Prowl,

2
@ecatmur: Quindi in un certo senso il polimorfismo dinamico è il modello concettuale, mentre la cancellazione del tipo è una tecnica che consente di realizzarlo.
Andy Prowl

2
@Downvoter: Sarei curioso di sapere cosa hai trovato di sbagliato in questa risposta.
Andy Prowl,

89

Andy Prowl ha ben affrontato problemi di progettazione. Questo è, ovviamente, molto importante, ma credo che la domanda originale riguardi più problemi di prestazioni correlati std::function.

Prima di tutto, una breve osservazione sulla tecnica di misurazione: gli 11ms ottenuti per calc1non hanno alcun significato. Infatti, guardando l'assembly generato (o eseguendo il debug del codice assembly), si può vedere che l'ottimizzatore di VS2012 è abbastanza intelligente da rendersi conto che il risultato della chiamata calc1è indipendente dall'iterazione e sposta la chiamata fuori dal ciclo:

for (int i = 0; i < 1e8; ++i) {
}
calc1([](float arg){ return arg * 0.5f; });

Inoltre, si rende conto che la chiamata calc1non ha alcun effetto visibile e abbandona del tutto la chiamata. Pertanto, 111ms è il tempo impiegato per l'esecuzione del ciclo vuoto. (Sono sorpreso che l'ottimizzatore abbia mantenuto il ciclo.) Quindi, fai attenzione con le misurazioni del tempo in loop. Questo non è così semplice come potrebbe sembrare.

Come è stato sottolineato, l'ottimizzatore ha più problemi da capire std::functione non sposta la chiamata fuori dal ciclo. Quindi 1241ms è una misura corretta per calc2.

Si noti che std::functionè in grado di memorizzare diversi tipi di oggetti richiamabili. Quindi, deve eseguire un po 'di magia di cancellazione del tipo per la memorizzazione. In genere, ciò implica un'allocazione dinamica della memoria (per impostazione predefinita tramite una chiamata a new). È noto che si tratta di un'operazione piuttosto costosa.

Lo standard (20.8.11.2.1 / 5) incorpora le implementazioni per evitare l'allocazione dinamica della memoria per piccoli oggetti che, per fortuna, VS2012 fa (in particolare, per il codice originale).

Per avere un'idea di quanto può essere più lento quando è coinvolta l'allocazione di memoria, ho modificato l'espressione lambda per acquisire tre floatsecondi. Ciò rende l'oggetto richiamabile troppo grande per applicare l'ottimizzazione dell'oggetto piccolo:

float a, b, c; // never mind the values
// ...
calc2([a,b,c](float arg){ return arg * 0.5f; });

Per questa versione, il tempo è di circa 16000 ms (rispetto ai 1241ms per il codice originale).

Infine, nota che la durata della lambda racchiude quella della std::function. In questo caso, anziché archiviare una copia del lambda, è std::functionpossibile memorizzare un "riferimento" ad esso. Per "riferimento" intendo un std::reference_wrapperche è facilmente costruito da funzioni std::refe std::cref. Più precisamente, usando:

auto func = [a,b,c](float arg){ return arg * 0.5f; };
calc2(std::cref(func));

il tempo diminuisce a circa 1860 ms.

Ne ho scritto qualche tempo fa:

http://www.drdobbs.com/cpp/efficient-use-of-lambda-expressions-and/232500059

Come ho detto nell'articolo, gli argomenti non valgono per VS2010 a causa del suo scarso supporto a C ++ 11. Al momento della stesura di questo articolo, era disponibile solo una versione beta di VS2012, ma il suo supporto per C ++ 11 era già abbastanza buono per questa questione.


Lo trovo davvero interessante, voler provare una velocità del codice usando esempi di giocattoli che vengono ottimizzati dal compilatore perché non hanno effetti collaterali. Direi che raramente si può scommettere su questo tipo di misurazioni, senza un codice reale / di produzione.
Ghita,

@ Ghita: in questo esempio, per impedire l'ottimizzazione del codice, si calc1potrebbe prendere un floatargomento che sarebbe il risultato della precedente iterazione. Qualcosa del genere x = calc1(x, [](float arg){ return arg * 0.5f; });. Inoltre, dobbiamo garantire che gli calc1usi x. Ma questo non è ancora abbastanza. Dobbiamo creare un effetto collaterale. Ad esempio, dopo la misurazione, stampa xsullo schermo. Anche se, sono d'accordo sul fatto che l'uso di codici giocattolo per le misurazioni di timimg non può sempre fornire un'indicazione perfetta di ciò che accadrà con il codice reale / di produzione.
Cassio Neri,

Mi sembra anche che il benchmark costruisca l'oggetto std :: function all'interno del ciclo e chiami calc2 nel ciclo. Indipendentemente dal fatto che il compilatore possa o meno ottimizzarlo (e che il costruttore potrebbe essere semplice come memorizzare un vptr), sarei più interessato a un caso in cui la funzione viene costruita una volta e passata a un'altra funzione che chiama in un ciclo. Vale a dire l'overhead della chiamata anziché il tempo di costruzione (e la chiamata di 'f' e non di calc2). Inoltre sarebbe interessato se chiamare f in un ciclo (in calc2), piuttosto che una volta, trarrebbe beneficio da qualsiasi sollevamento.
Greggo

Bella risposta. 2 cose: un bell'esempio di uso valido per std::reference_wrapper(per forzare i template; non è solo per l'archiviazione generale), ed è divertente vedere l'ottimizzatore di VS che non riesce a scartare un ciclo vuoto ... come ho notato con questo bug di GCCvolatile .
underscore_d

37

Con Clang non c'è differenza di prestazioni tra i due

Usando clang (3.2, trunk 166872) (-O2 su Linux), i binari dei due casi sono in realtà identici .

-Tornerò per suonare alla fine del post. Ma prima, gcc 4.7.2:

C'è già molta intuizione in corso, ma voglio sottolineare che il risultato dei calcoli di calc1 e calc2 non è lo stesso, a causa di in-lining ecc. Confronta ad esempio la somma di tutti i risultati:

float result=0;
for (int i = 0; i < 1e8; ++i) {
  result+=calc2([](float arg){ return arg * 0.5f; });
}

con calc2 che diventa

1.71799e+10, time spent 0.14 sec

mentre con calc1 diventa

6.6435e+10, time spent 5.772 sec

questo è un fattore di ~ 40 nella differenza di velocità e un fattore di ~ 4 nei valori. La prima è una differenza molto più grande di ciò che OP ha pubblicato (usando Visual Studio). In realtà, stampare il valore alla fine è anche una buona idea per impedire al compilatore di rimuovere il codice senza risultati visibili (regola as-if). Cassio Neri ha già detto questo nella sua risposta. Nota quanto diversi sono i risultati: bisogna fare attenzione quando si confrontano i fattori di velocità dei codici che eseguono calcoli diversi.

Inoltre, per essere onesti, confrontare vari modi di calcolare ripetutamente f (3.3) non è forse così interessante. Se l'ingresso è costante, non dovrebbe essere in un ciclo. (È facile notare l'ottimizzatore)

Se aggiungo un argomento fornito dall'utente a calc1 e 2, il fattore di velocità tra calc1 e calc2 scende fino a un fattore 5, da 40! Con Visual Studio la differenza è vicina a un fattore 2 e con clang non c'è differenza (vedi sotto).

Inoltre, poiché le moltiplicazioni sono veloci, parlare di fattori di rallentamento spesso non è così interessante. Una domanda più interessante è: quanto sono piccole le tue funzioni e queste chiamate sono il collo di bottiglia in un vero programma?

clang:

Clang (ho usato 3.2) ha effettivamente prodotto binari identici quando passo da calc1 a calc2 per il codice di esempio (pubblicato di seguito). Con l'esempio originale pubblicato nella domanda entrambi sono identici ma non richiedono tempo (i loop vengono rimossi completamente come descritto sopra). Con il mio esempio modificato, con -O2:

Numero di secondi da eseguire (meglio di 3):

clang:        calc1:           1.4 seconds
clang:        calc2:           1.4 seconds (identical binary)

gcc 4.7.2:    calc1:           1.1 seconds
gcc 4.7.2:    calc2:           6.0 seconds

VS2012 CTPNov calc1:           0.8 seconds 
VS2012 CTPNov calc2:           2.0 seconds 

VS2015 (14.0.23.107) calc1:    1.1 seconds 
VS2015 (14.0.23.107) calc2:    1.5 seconds 

MinGW (4.7.2) calc1:           0.9 seconds
MinGW (4.7.2) calc2:          20.5 seconds 

I risultati calcolati di tutti i file binari sono gli stessi e tutti i test sono stati eseguiti sulla stessa macchina. Sarebbe interessante se qualcuno con un clang più profondo o una conoscenza VS potesse commentare quali ottimizzazioni potrebbero essere state fatte.

Il mio codice di prova modificato:

#include <functional>
#include <chrono>
#include <iostream>

template <typename F>
float calc1(F f, float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

float calc2(std::function<float(float)> f,float x) { 
  return 1.0f + 0.002*x+f(x*1.223) ; 
}

int main() {
    using namespace std::chrono;

    const auto tp1 = high_resolution_clock::now();

    float result=0;
    for (int i = 0; i < 1e8; ++i) {
      result=calc1([](float arg){ 
          return arg * 0.5f; 
        },result);
    }
    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;
}

Aggiornare:

Aggiunto vs2015. Ho anche notato che ci sono conversioni double-> float in calc1, calc2. Rimuoverli non cambia la conclusione per Visual Studio (entrambi sono molto più veloci ma il rapporto è più o meno lo stesso).


8
Che probabilmente dimostra solo che il benchmark è sbagliato. IMHO il caso d'uso interessante è dove il codice chiamante riceve un oggetto funzione da qualche altra parte, quindi il compilatore non conosce l'origine della funzione std :: durante la compilazione della chiamata. Qui, il compilatore conosce esattamente la composizione della funzione std :: quando lo chiama, espandendo calc2 inline in main. Facilmente risolto rendendo calc2 "esterno" in settembre. file sorgente. Stai quindi confrontando le mele con le arance; calc2 sta facendo qualcosa che calc1 non può. E il loop potrebbe essere all'interno di calc (molte chiamate a f); non attorno al ctor dell'oggetto funzione.
Greggo

1
Quando posso arrivare a un compilatore adatto. Per ora posso dire che (a) ctor per una vera std :: function chiama 'new'; (b) la chiamata stessa è piuttosto snella quando il target ha una funzione reale corrispondente; (c) nei casi con associazione, c'è un pezzo di codice che esegue l'adattamento, selezionato da un codice ptr nella funzione obj, e che raccoglie i dati (parametri associati) dalla funzione obj (d) la funzione 'legata' può essere integrato in quell'adattatore, se il compilatore può vederlo.
greggo

Nuova risposta aggiunta con l'impostazione descritta.
Greggo

3
A proposito, il benchmark non è sbagliato, la domanda ("std :: function vs template") è valida solo nell'ambito della stessa unità di compilazione. Se si sposta la funzione su un'altra unità, il modello non è più possibile, quindi non c'è nulla da confrontare.
Rustyx,

13

Diverso non è lo stesso.

È più lento perché fa cose che un modello non può fare. In particolare, ti consente di chiamare qualsiasi funzione che può essere chiamata con i tipi di argomento indicati e il cui tipo di ritorno è convertibile nel tipo di ritorno dato dallo stesso codice .

void eval(const std::function<int(int)>& f) {
    std::cout << f(3);
}

int f1(int i) {
    return i;
}

float f2(double d) {
    return d;
}

int main() {
    std::function<int(int)> fun(f1);
    eval(fun);
    fun = f2;
    eval(fun);
    return 0;
}

Si noti che lo stesso oggetto funzione fun, viene passato a entrambe le chiamate eval. Contiene due diverse funzioni.

Se non hai bisogno di farlo, allora dovresti non usare std::function.


2
Voglio solo sottolineare che quando 'fun = f2' è finito, l'oggetto 'fun' finisce per indicare una funzione nascosta che converte int in double, chiama f2 e converte il doppio risultato in int. (Nell'esempio reale , 'f2' potrebbe essere integrato in quella funzione). Se si assegna un divertente std :: bind, l'oggetto 'fun' può finire con contenere i valori da usare per i parametri associati. per supportare questa flessibilità, un'assegnazione a "fun" (o init of) può comportare l'allocazione / deallocazione della memoria e può richiedere più tempo dell'overhead della chiamata effettiva.
Greggo

8

Hai già delle buone risposte qui, quindi non le contraddirò, in breve paragonare std :: function ai template è come confrontare le funzioni virtuali con le funzioni. Non dovresti mai "preferire" le funzioni virtuali alle funzioni, ma piuttosto usi le funzioni virtuali quando si adatta al problema, spostando le decisioni dal tempo di compilazione al tempo di esecuzione. L'idea è che invece di dover risolvere il problema utilizzando una soluzione su misura (come una tabella di salto) si utilizza qualcosa che offre al compilatore una migliore possibilità di ottimizzazione per te. Aiuta anche altri programmatori, se si utilizza una soluzione standard.


6

Questa risposta intende contribuire, all'insieme delle risposte esistenti, a quello che ritengo sia un punto di riferimento più significativo per il costo di runtime delle chiamate std :: function.

Il meccanismo std :: function dovrebbe essere riconosciuto per ciò che fornisce: Qualsiasi entità richiamabile può essere convertita in una funzione std :: della firma appropriata. Supponiamo di avere una libreria che si adatta a una superficie a una funzione definita da z = f (x, y), puoi scriverla per accettare a std::function<double(double,double)>e l'utente della libreria può facilmente convertire qualsiasi entità chiamabile in quella; sia una normale funzione, un metodo di un'istanza di classe, o un lambda, o qualsiasi cosa sia supportata da std :: bind.

A differenza degli approcci modello, questo funziona senza dover ricompilare la funzione di libreria per diversi casi; di conseguenza, per ogni caso aggiuntivo è necessario un piccolo codice compilato. È sempre stato possibile farlo accadere, ma richiedeva alcuni meccanismi imbarazzanti e l'utente della biblioteca avrebbe probabilmente bisogno di costruire un adattatore attorno alla sua funzione per farlo funzionare. std :: function costruisce automaticamente qualunque adattatore sia necessario per ottenere un'interfaccia di chiamata di runtime comune per tutti i casi, che è una funzionalità nuova e molto potente.

A mio avviso, questo è il caso d'uso più importante per std :: function per quanto riguarda le prestazioni: sono interessato al costo di chiamare una std :: function molte volte dopo che è stata costruita una volta, e deve essere una situazione in cui il compilatore non è in grado di ottimizzare la chiamata conoscendo la funzione effettivamente chiamata (cioè è necessario nascondere l'implementazione in un altro file sorgente per ottenere un benchmark adeguato).

Ho fatto il test di seguito, simile ai PO; ma i principali cambiamenti sono:

  1. Ogni caso viene ripetuto 1 miliardo di volte, ma gli oggetti std :: function vengono costruiti una sola volta. Ho scoperto guardando il codice di output che viene chiamato 'operatore nuovo' quando si costruiscono chiamate std :: function (forse non quando sono ottimizzate).
  2. Il test è diviso in due file per impedire l'ottimizzazione indesiderata
  3. I miei casi sono: (a) la funzione è in linea (b) la funzione è passata da un normale puntatore a funzione (c) la funzione è una funzione compatibile avvolta come std :: funzione (d) è una funzione incompatibile resa compatibile con una std :: bind, avvolto come std :: function

I risultati che ottengo sono:

  • case (a) (inline) 1.3 nsec

  • tutti gli altri casi: 3.3 nsec.

Il caso (d) tende ad essere leggermente più lento, ma la differenza (circa 0,05 nsec) viene assorbita dal rumore.

La conclusione è che la funzione std :: è un sovraccarico paragonabile (al momento della chiamata) all'utilizzo di un puntatore a funzione, anche quando c'è un semplice adattamento 'bind' alla funzione reale. L'Inline è 2 ns più veloce degli altri, ma questo è un compromesso atteso poiché l'Inline è l'unico caso che è "cablato" in fase di esecuzione.

Quando eseguo il codice di johan-lundberg sulla stessa macchina, vedo circa 39 nsec per loop, ma c'è molto di più nel loop lì, incluso l'attuale costruttore e distruttore della funzione std ::, che è probabilmente abbastanza alto poiché comporta un nuovo ed elimina.

-O2 gcc 4.8.1, al target x86_64 (core i5).

Nota, il codice è suddiviso in due file, per impedire al compilatore di espandere le funzioni in cui sono chiamati (tranne nel caso in cui è previsto).

----- primo file sorgente --------------

#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;
}

----- secondo file sorgente -------------

#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;
}

Per coloro che sono interessati, ecco l'adattatore creato dal compilatore per far sembrare 'mul_by' un float (float) - questo viene chiamato quando viene chiamata la funzione creata come 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

(quindi potrebbe essere stato un po 'più veloce se avessi scritto 0,5f nel bind ...) Nota che il parametro' x 'arriva in% xmm0 e rimane lì.

Ecco il codice nell'area in cui è costruita la funzione, prima di chiamare test_stdfunc - eseguire 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)

1
Con clang 3.4.1 x64 i risultati sono: (a) 1.0, (b) 0.95, (c) 2.0, (d) 5.0.
Rustyx,

4

Ho trovato i tuoi risultati molto interessanti, quindi ho fatto un po 'di ricerche per capire cosa sta succedendo. Prima di tutto, come molti altri hanno detto senza avere i risultati dell'effetto di calcolo sullo stato del programma, il compilatore lo ottimizzerà semplicemente via. In secondo luogo, avendo una costante 3.3 data come un armamento al callback, sospetto che ci saranno altre ottimizzazioni in corso. Con questo in mente ho cambiato un po 'il tuo codice di riferimento.

template <typename F>
float calc1(F f, float i) { return -1.0f * f(i) + 666.0f; }
float calc2(std::function<float(float)> f, float i) { return -1.0f * f(i) + 666.0f; }
int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc2([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

Data questa modifica al codice che ho compilato con gcc 4.8 -O3 e ho ottenuto un tempo di 330 ms per calc1 e 2702 per calc2. Quindi usare il template è stato 8 volte più veloce, questo numero mi è sembrato sospetto, la velocità di una potenza di 8 indica spesso che il compilatore ha vettorizzato qualcosa. quando ho guardato il codice generato per la versione dei modelli era chiaramente vectoreized

.L34:
cvtsi2ss        %edx, %xmm0
addl    $1, %edx
movaps  %xmm3, %xmm5
mulss   %xmm4, %xmm0
addss   %xmm1, %xmm0
subss   %xmm0, %xmm5
movaps  %xmm5, %xmm0
addss   %xmm1, %xmm0
cvtsi2sd        %edx, %xmm1
ucomisd %xmm1, %xmm2
ja      .L37
movss   %xmm0, 16(%rsp)

Dove non era la versione std :: function. Questo ha senso per me, dal momento che con il modello il compilatore sa per certo che la funzione non cambierà mai durante il ciclo ma con la funzione std :: al suo interno potrebbe cambiare, quindi non può essere vettorializzata.

Questo mi ha portato a provare qualcos'altro per vedere se riuscivo a fare in modo che il compilatore eseguisse la stessa ottimizzazione sulla versione std :: function. Invece di passare una funzione faccio una std :: function come una var globale, e questa viene chiamata.

float calc3(float i) {  return -1.0f * f2(i) + 666.0f; }
std::function<float(float)> f2 = [](float arg){ return arg * 0.5f; };

int main() {
    const auto tp1 = system_clock::now();
    for (int i = 0; i < 1e8; ++i) {
        t += calc3([&](float arg){ return arg * 0.5f + t; }, i);
    }
    const auto tp2 = high_resolution_clock::now();
}

Con questa versione vediamo che il compilatore ha ora vettorializzato il codice allo stesso modo e ottengo gli stessi risultati di benchmark.

  • modello: 330ms
  • std :: funzione: 2702ms
  • global std :: function: 330ms

Quindi la mia conclusione è che la velocità pura di una funzione std :: funzione rispetto a una funzione template è praticamente la stessa. Tuttavia rende il lavoro dell'ottimizzatore molto più difficile.


1
L'intero punto è passare un funzione come parametro. Il tuo calc3caso non ha senso; calc3 è ora hardcoded per chiamare f2. Certo che può essere ottimizzato.
Rustyx,

anzi, questo è quello che stavo cercando di mostrare. Quel calc3 è equivalente al modello, e in quella situazione è effettivamente un costrutto in fase di compilazione proprio come un modello.
Joshua Ritterman,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.