Newsgroup: it.comp.lang.c++
Titolo: Costo delle eccezioni

Debuggando con GDB con abilitato "catch throw" del codice non scritto da 
me, mi sono accorto che in un loop che veniva eseguito svariate migliaia 
di volte in una procedura di caricaemento generava x ogni loop 
un'eccezione, trappata al livello superiore, che di fatto era una 
semplice gestione di errore "che e' normale che capiti", ecco un piccolo 
esempio:
void myclass::mymethod(const std::string &test)
{
	try {
		handle_directory(test));
		// do other stuff oo the basis test is a directory
	}
	catch (not_directory_exception &e) {
		// do nothing
	}
}
Bene, ho modificato la funzione perche` ritornasse un booleano invece di 
sparare un'exception ed ho notato una VISIBILE riduzione dei tempi di 
esecuzione del programma...
Allora per eseguire un test piu` minimale ho scritto un programmino che 
verifica i numeri primi usando le exception per ritornare il fallimento 
o ritornando un booleano... ecco i risultati:
gabry@nevada:/tmp$ ./prime 100000
Esecuzione prime1... 100000 numeri in 1050ms numeri primi: 21052
Esecuzione prime2... 100000 numeri in 174ms numeri primi: 21052
gabry@nevada:/tmp$ ./prime 1000000
Esecuzione prime1... 1000000 numeri in 13400ms numeri primi: 170741
Esecuzione prime2... 1000000 numeri in 3951ms numeri primi: 170741
E' normnale che costi cosi` tanto una throw?
Ecco il sorgente:
#include <iostream>
#include <sstream>
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
#include <math.h>
struct timeval tv;
void reset_timer()
{
     gettimeofday(&tv, NULL);
}
std::string print_timer()
{
     struct timeval end;
     gettimeofday(&end, NULL);
     end.tv_sec -= tv.tv_sec;
     end.tv_usec -= tv.tv_usec;
     if (end.tv_usec < 0) {
         end.tv_usec += 1000000;
         end.tv_sec -= 1;
     }
     std::ostringstream os;
     os << (end.tv_sec * 1000 + end.tv_usec / 1000) << "ms";
     return os.str();
}
void is_prime1(int n)
{
     if ( (n%2) == 0 && (n%3) == 0)
         throw n;
     int maxdiv = (int)sqrt(n);
     for (int k = 5; k <= maxdiv; ++k)
         if ( (n%k) == 0 )
             throw n;
}
bool is_prime2(int n)
{
     if ( (n%2) == 0 && (n%3) == 0)
         return false;
     int maxdiv = (int)sqrt(n);
     for (int k = 5; k <= maxdiv; ++k)
         if ( (n%k) == 0 )
             return false;
     return true;
}
int main(int argc, char *argv[])
{
     if (argc != 2) {
         std::cerr << "Usare " << argv[0] << " [maxnum]\n";
         return 0;
     }
     int counts = atoi(argv[1]);
     std::cerr << "Esecuzione prime1... ";
     int prime = 0;
     reset_timer();
     for (int i = 6; i < counts; ++i) {
         try {
             is_prime1(i);
             prime++;
         }
         catch (...) {
             // do nothing
         }
     }
     std::cerr << counts << " numeri in "
               << print_timer() << " numeri primi: " << prime << "\n";
     std::cerr << "Esecuzione prime2... ";
     prime = 0;
     reset_timer();
     for (int i = 6; i < counts; ++i) {
         if (is_prime2(i))
             prime++;
     }
     std::cerr << counts << " numeri in "
               << print_timer() << " numeri primi: " << prime << "\n";
}

 E' normnale che costi cosi` tanto una throw?
Visto quel che c'è dietro si, ma non ho mai quantificato.

 }
is_prime() dovrebbe ritornare sempre' un bool. Un numero che non e' 
prime non e' eccezionale.
Ma se il programma non puo' continuare con un numero non prime, il 
codice dovrebbe essere  if( !is_prime() ) throw qualcosa();. Comunque, 
in generale, il fatto che un numero non e' prime non e' eccezionale, 
quindi l'exception non dovrebbe essere thrown nella funzione is_prime() 
per significare semplicemente "false".
MC

 per significare semplicemente "false".
Io lo so che non devo usare le exception per ritornare qualcosa che non 
sia eccezionale, purtroppo una buona fetta del mondo pensa il contrario 
e le usa per riportare qualunque tipo di errore...
Chesso`, giusto per fare un esempio tutta la gestione degli errori di 
OpenAL (versione OSX, sviluppata da apple) e` fatta con exception 
trappate prima dell'esposizione dell'interfaccia C.
Tra l'altro ho visto che su OSX il costo della throw e' anche maggiore 
che su linux:
Mini:/tmp gabry$ ./prime 1000000
Esecuzione prime1... 1000000 numeri in 9549ms numeri primi: 170741
Esecuzione prime2... 1000000 numeri in 2046ms numeri primi: 170741
Mi capita spesso di lavorare con codice scritto da altri e purtroppo 
l'uso delle eccezioni in casi non eccezionali, non e` affatto 
un'eccezione :)
Bye,
  Gabry

 e le usa per riportare qualunque tipo di errore...
Mah... non mi sento di biasimare quella "buona fetta". Il fatto è che
a volte è meglio avere un unico modo di gestire l'errore, che non due
modi distinti (e quindi due modi di sbagliare).
E' chiaro che poi bisogna intendersi su cosa si intende per "errore".

 E' normnale che costi cosi` tanto una throw?
Si. Considera che le performance variano sensibilmente da compilatore
a compilatore (ogni compilatore utilizza un metodo suo per gestire le
eccezioni), ma generalente e' sempre piuttosto costoso fare il catch
di un eccezione sollevata.
Francemente credo che in linea teorica si potrebbero avere delle
performance migliori, ma comunque:
(1) Ha poco senso visto che le eccezioni dovrebbero essere sollevate
solo in circostanze effettivamente eccezionali
(2) E' molto piu' importante minimizzare il costo di una clausula try
nel caso l'eccezione non venga sollevata
Saluti,
The Librarian

 E' chiaro che poi bisogna intendersi su cosa si intende per "errore".
Una ottima dissertazione proprio riguardo questo argomento è fornita dal
libro "Thinking in Java, 4th edition" di Bruce Eckel, nel capitolo
"Error handling with exceptions".
Il concetto di base è che "quando occorre un errore, potresti non sapere
come comportarti". Questo si intende per eccezione, rimandare l'errore
al punto in cui è giusto che venga gestito (v. paragrafo "concepts").
Nell'esempio del numero primo, ad esempio, è chiaro che l'eccezione è
usata per tornare un risultato, infatti non interrompe il flusso del
programma, ma semplicemente un risultato; diverso sarebbe se quella
parte di codice fosse inserita all'interno di una elaborazione più
complessa tale per cui l'evento impedisce la prosecuzione.
E', secondo me, la differenza tra una funzione che inverte una matrice e
una che controlla se la matrice è invertibile/singolare. Nel primo caso,
l'eccezione ci sta (il risultato è la matrice invertita), nel secondo no
(il risultato è il fatto che la matrice sia singolare o meno).
Ovvio che se condizioni di programma lo richiedono, è possibile o
necessario effettuare scelte diverse (ad esempio, tornare un booleano
false nella funzione di inversione in caso di non invertibilità, solo
per condizioni di performance).

 e le usa per riportare qualunque tipo di errore...
Boost.Optional e' spesso un'alternativa preferibile, in particolare
per quei casi dove non e' possibile individuare un valore
convenzionale per indicare il caso particolare. Ad esempio:
#include <stdexcept>
double div(double a, double b) {
  if ( b == 0.0 ) throw std::out_of_range():
  return a/b;
#include <boost/optional.hpp>
boost::optional<double> div(double a, double b) {
  boost::optional<double> res;
  if ( b != 0.0 ) res = a/b;
  return res;
}
Ciao,
Nicola

Gabriele Greco ha scritto:
  > E' normnale che costi cosi` tanto una throw?
Direi che e' normale che costi cosi' poco.
Ho provato a eseguire lo stesso programma con il compilatore Visual C++ 
7.1, e, dopo aver sostituito le chiamate alle routine reset_timer e 
print_timer con altrettante chiamate a clock, e dopo aver castato a 
double l'argomento di sqrt, ho ottenuto i seguenti risultati:
prime 100000
Esecuzione prime1... 100000 numeri in 875 ms, numeri primi: 21052
Esecuzione prime2... 100000 numeri in 187 ms, numeri primi: 21052
prime 1000000
Esecuzione prime1... 1000000 numeri in 11734 ms, numeri primi: 170741
Esecuzione prime2... 1000000 numeri in 4516 ms, numeri primi: 170741
Risulta che il sollevamento e cattura di ogni eccezione richiede solo 
circa 7 microsecondi, e dato che e' ovviamente sbagliato sollevare piu' 
di una eccezione ogni millisecondo, questo significa che l'overhead per 
un programma ben progettato (cioe' che sollevi meno di una eccezione 
ogni millisecondo) e' inferiore all'1%.
Viene sollevata un'eccezione quando si commette un'errore. Chi e' che 
sbaglia piu' di mille volte al secondo?
-- 
     Carlo Milanesi
 
http://digilander.libero.it/carlmila

 Chi e' che sbaglia piu' di mille volte al secondo?
Un programmatore che usa throw al posto di return, ad esempio...

Un programmatore che usa throw al posto di return, ad esempio...
In moltissimi casi in cui è correttissimo sollevare un'eccezione.
Stanno lì apposta.

 convenzionale per indicare il caso particolare.
Quanto mi ricorda Haskell...
-- 
blog:  
http://www.akropolix.net/rik0/blogs
 | Uccidete i filosofi,
site:  
http://www.akropolix.net/rik0/
      | tenetevi riso e
forum: 
http://www.akropolix.net/forum/
     | bacchette per voi.

 al punto in cui è giusto che venga gestito (v. paragrafo "concepts").
Io continuo a preferire la strategia EAFP a quella LBYL.
Proprio da un punto di vista *logico*, di *pulizia* del codice. 
Il fatto che in C++ si fa spesso a sproposito e non quando si dovrebbe.
if(cond)
        do_ok();
else
        do_fail();
costa 
t(cond) + P(ok) * t(do_ok) + P(fail) * t(do_fail)
D'altra parte 
try
        do_ok();
catch(exc)
        do_fail();
costa
P(ok) * t(do_ok) + P(fail) * [t(do_fail) + t(throw)]
Il punto è che a questo punto bisogna valutare se
t(cond) >< P(fail) * + t(throw)
Non sono *affatto* pochi i casi in cui cond è nell'ordine di grandezza
di do_ok, mentr do_fail è sostanzialmente poco costoso.
Non solo. Se siamo in ambiente multithread, devi *spesso* aggiungere il
costo di lock e unlock, perchè il check della condizione e il do_ok()
devono essere atomici rispetto ai dati controllati e manipolati. Ancora
una volta, se cond è costoso questo è diverso da lockare solo per il
do_ok();
Poi ci sono una serie di casi dove in buona sostanza *non* puoi lockare,
per esempio perchè i dati su cui operi non sono dati ma cose esterne al
programma. Ah, questo anche in ambiente non multithread, ma
semplicemente per il fatto che agisci in un sistema operativo
multitasking.
In questi casi bisogna aggiungere gestione dell'errore aggiuntiva che
tratti il caso in cui cond vada bene e poi do_ok non sia eseguibile
perchè le cose che rendevano vero cond sono nel frattempo cambiate. Non
farlo è esporsi a bachi *estremamente* difficili da tracciarsi.
Aggiungo inoltre che do_ok() deve essere scritta in modo che tutto
quello che fa prima di accorgersi che in effetti non sarebbe dovuta
partire è rollbackabile.
Insomma... come vedi il discorso è *notevolmente* più complesso.


 Insomma... come vedi il discorso è *notevolmente* più complesso.
Assolutamente.

Questi acronimi mi sfuggono e, ormai lo si è capito, son pigro :-)
"it' (E)asy to (A)sk (F)orgiveness than (P)ermission"
"(L)ook (B)efore (Y)ou (L)eap"
http://mail.python.org/pipermail/python-list/2003-May/205182.html

 difficili da gestire e risolvere.
La differenza è che posso farmi i vari lock logici (lascia stare che sia
a livello di thread o di multiprocesso su un sistema) *dentro* i metodi.
Tipicamente posso aspettarmi che la check() e la do_ok() gestiscano
correttamente il locking. E *voglio* che la semantica di locking sia
*dentro* l'oggetto, non fuori.
Per rendere atomico il tutto invece devo o mettere le condizioni di
locking esplicite fuori (ma attenzione, io utente dell'oggetto potrei
non sapere *esattamente* cosa debba essere lockato, come, rispetto a
cosa). Oppure chi fa la libreria mi deve dare pure una funzione atomica
check_and_do(). E così via. Con poi il problema che se do_ok() ritorna
qualcosa non è ben chiaro cosa debba ritornare check_and_do() -- o
meglio, è chiaro, vedi il post di Nicola --.
Tipicamente tutti i casi in cui una feature di più alto livello mi
permette di scrivere codice più corretto per me vale la pena usarla. Se
e quando si dimostrerà che questo porta a performance inaccettabili, si
cambierà.


 "(L)ook (B)efore (Y)ou (L)eap"
Esatto. :)


Esatto. :)
Gli acronimi, a volte, mi sembrano tanto un modo per chiudere
ermeticamente la conoscenza delle cose :-)

 ermeticamente la conoscenza delle cose :-)
forse hai ragione. è che io li davo per scontati e non ci ho pensato.
