2015-07-18 13 views
14

Ho scritto un test per misurare il costo delle eccezioni C++ con i thread.Il costo delle eccezioni C++ e setjmp/longjmp

#include <cstdlib> 
#include <iostream> 
#include <vector> 
#include <thread> 

static const int N = 100000; 

static void doSomething(int& n) 
{ 
    --n; 
    throw 1; 
} 

static void throwManyManyTimes() 
{ 
    int n = N; 
    while (n) 
    { 
     try 
     { 
      doSomething(n); 
     } 
     catch (int n) 
     { 
      switch (n) 
      { 
      case 1: 
       continue; 
      default: 
       std::cout << "error" << std::endl; 
       std::exit(EXIT_FAILURE); 
      } 
     } 
    } 
} 

int main(void) 
{ 
    int nCPUs = std::thread::hardware_concurrency(); 
    std::vector<std::thread> threads(nCPUs); 
    for (int i = 0; i < nCPUs; ++i) 
    { 
     threads[i] = std::thread(throwManyManyTimes); 
    } 
    for (int i = 0; i < nCPUs; ++i) 
    { 
     threads[i].join(); 
    } 
    return EXIT_SUCCESS; 
} 

Ecco la versione C che ho inizialmente scritto per divertimento.

#include <stdio.h> 
#include <stdlib.h> 
#include <setjmp.h> 
#include <glib.h> 

#define N 100000 

static GPrivate jumpBuffer; 

static void doSomething(volatile int *pn) 
{ 
    jmp_buf *pjb = g_private_get(&jumpBuffer); 

    --*pn; 
    longjmp(*pjb, 1); 
} 

static void *throwManyManyTimes(void *p) 
{ 
    jmp_buf jb; 
    volatile int n = N; 

    (void)p; 
    g_private_set(&jumpBuffer, &jb); 
    while (n) 
    { 
     switch (setjmp(jb)) 
     { 
     case 0: 
      doSomething(&n); 
     case 1: 
      continue; 
     default: 
      printf("error\n"); 
      exit(EXIT_FAILURE); 
     } 
    } 
    return NULL; 
} 

int main(void) 
{ 
    int nCPUs = g_get_num_processors(); 
    GThread *threads[nCPUs]; 
    int i; 

    for (i = 0; i < nCPUs; ++i) 
    { 
     threads[i] = g_thread_new(NULL, throwManyManyTimes, NULL); 
    } 
    for (i = 0; i < nCPUs; ++i) 
    { 
     g_thread_join(threads[i]); 
    } 
    return EXIT_SUCCESS; 
} 

La versione C++ corre molto lento rispetto alla versione C.

$ g++ -O3 -g -std=c++11 test.cpp -o cpp-test -pthread 
$ gcc -O3 -g -std=c89 test.c -o c-test `pkg-config glib-2.0 --cflags --libs` 
$ time ./cpp-test 

real 0m1.089s 
user 0m2.345s 
sys  0m1.637s 
$ time ./c-test 

real 0m0.024s 
user 0m0.067s 
sys  0m0.000s 

Così ho eseguito il profiler del callgrind.

Per cpp-test, __cxz_throw è stato chiamato esattamente 400.000 volte con auto-costo di 8.000.032.

Per c-test, __longjmp_chk è stato chiamato esattamente 400.000 volte con auto-costo di 5.600.000.

L'intero costo di cpp-test è 4.048.441.756.

L'intero costo di c-test è 60.417.722.


immagino qualcosa di molto di più che semplicemente salvare lo stato del salto-point e ripresa in seguito viene fatto con eccezioni C++. Non ho potuto testare con più grande N perché il profiler callgrind verrà eseguito per sempre per il test C++.

Qual è il costo aggiuntivo delle eccezioni C++ che lo rende molto più lento della coppia setjmp/longjmp almeno in questo esempio?

+0

È possibile confrontare il codice macchina risultante? –

+0

@KerrekSB Scusate, il disassemblaggio per entrambi è troppo complicato per me per ottenere qualcosa di significativo, posso incollare l'intero asm da qualche parte se ne avete bisogno. – xiver77

+0

Tutti i tipi di informazioni groovy qui: http://stackoverflow.com/questions/13835817/are-exceptions-in-c-really-slow – user4581301

risposta

16

Questo è di progettazione.

Le eccezioni C++ sono previste eccezionali in natura e sono ottimizzate in tal modo. Il programma è compilato per essere più efficiente quando non si verifica un'eccezione.

È possibile verificare ciò commentando l'eccezione dai test.

In C++:

//throw 1; 

$ g++ -O3 -g -std=c++11 test.cpp -o cpp-test -pthread 

$ time ./cpp-test 

real 0m0.003s 
user 0m0.004s 
sys  0m0.000s 

In C:

/*longjmp(*pjb, 1);*/ 

$ gcc -O3 -g -std=c89 test.c -o c-test `pkg-config glib-2.0 --cflags --libs` 

$ time ./c-test 

real 0m0.008s 
user 0m0.012s 
sys  0m0.004s 

Qual è il costo aggiuntivo coinvolti in C++ eccezioni che rende molte volte più lento rispetto alla coppia di setjmp/longjmp almeno in questo esempio?

g ++ implementa eccezioni modello a costo zero, che non hanno sovraccarico efficace * quando un'eccezione viene non torta. Il codice macchina viene prodotto come se non ci fosse il blocco try/catch.

Il costo di questo zero sovraccarico è che una ricerca tabella deve essere effettuata sul contatore di programma quando un'eccezione viene generata , per determinare un salto al codice appropriato per eseguire la rimozione dello stack. Ciò pone l'intera implementazione del blocco /catch all'interno del codice che esegue un throw.

Il costo aggiuntivo è una ricerca tabella.

* Potrebbe verificarsi un temporaneo voodoo, in quanto la presenza di una tabella di ricerca PC potrebbe influire sul layout della memoria, il che potrebbe influire sui problemi di cache della CPU.