Come dovrebbero essere riportati gli errori nelle biblioteche scientifiche?


11

Esistono molte filosofie in diverse discipline di ingegneria del software su come le biblioteche dovrebbero affrontare errori o altre condizioni eccezionali. Alcuni di quelli che ho visto:

  1. Restituisce un codice di errore con il risultato restituito da un argomento pointer. Questo è ciò che fa PETSc.
  2. Restituisce errori per un valore sentinella. Ad esempio, malloc restituisce NULL se non è in grado di allocare memoria, sqrtrestituirà NaN se si passa un numero negativo, ecc. Questo approccio viene utilizzato in molte funzioni libc.
  3. Genera eccezioni. Utilizzato nell'affare.II, trilinos, ecc.
  4. Restituisce un tipo di variante; ad esempio una funzione C ++ che restituisce un oggetto di tipo Resultse viene eseguito correttamente e utilizza un tipo Errorper descrivere come restituirebbe l'errore std::variant<Error, Result>.
  5. Usa assert e crash. Utilizzato in p4est e in alcune parti dell'igrafo.

Problemi con ciascun approccio:

  1. Il controllo di ogni errore introduce un sacco di codice aggiuntivo. I valori in cui verrà archiviato un risultato devono sempre essere dichiarati per primi, introducendo molte variabili temporanee che potrebbero essere utilizzate una sola volta. Questo approccio spiega quale errore si è verificato, ma può essere difficile determinare perché o, per uno stack di chiamate profonde, dove.
  2. Il caso di errore è facile da ignorare. Inoltre, molte funzioni non possono nemmeno avere un valore sentinella significativo se l'intero intervallo di tipi di output è un risultato plausibile. Molti degli stessi problemi del n. 1.
  3. Possibile solo in C ++, Python, ecc., Non in C o Fortran. Può essere imitato in C usando setjmp / longjmp stregoneria o libunwind .
  4. Possibile solo in C ++, Rust, OCaml, ecc., Non in C o Fortran. Può essere imitato in C usando la stregoneria macro.
  5. Probabilmente il più informativo. Ma se adotti questo approccio, diciamo, per una libreria C per la quale scrivi un wrapper Python, un errore sciocco come passare un indice fuori limite a un array causerà il crash dell'interprete Python.

Gran parte dei consigli su Internet sulla gestione degli errori sono scritti dal punto di vista dei sistemi operativi, dello sviluppo integrato o delle applicazioni web. Gli arresti anomali sono inaccettabili e devi preoccuparti della sicurezza. Le applicazioni scientifiche non hanno questi problemi quasi nella stessa misura, se non del tutto.

Un'altra considerazione è quali tipi di errori sono recuperabili o meno. Un errore di malloc non è recuperabile e, in ogni caso, il killer di memoria insufficiente del sistema operativo ci riuscirà prima di farlo. Neanche un indice fuori limite per una dimensione dell'array è recuperabile. Per me come utente, la cosa più bella che una libreria può fare è andare in crash con un messaggio di errore informativo. D'altra parte, il fallimento di, per esempio, un solutore lineare iterativo a convergere potrebbe essere recuperato usando un risolutore a fattorizzazione diretta.

In che modo le biblioteche scientifiche dovrebbero segnalare gli errori e aspettarsi che vengano gestiti? Mi rendo conto ovviamente che dipende da quale lingua è implementata la biblioteca. Ma per quanto posso dire, per qualsiasi biblioteca sufficientemente utile, la gente vorrà chiamarla da una lingua diversa da quella in cui è implementata.

A parte questo, penso che l'approccio n. 5 possa essere sostanzialmente migliorato per una libreria C se definisce un puntatore alla funzione di gestore di asserzioni globali come parte dell'API pubblica. Il gestore dell'asserzione imposterebbe per impostazione predefinita la segnalazione del numero di file / riga e l'arresto anomalo. I binding C ++ per questa libreria definiscono un nuovo gestore di asserzioni che genera invece un'eccezione C ++. Allo stesso modo, i collegamenti Python definiscono un gestore di asserzioni che utilizza l'API CPython per generare un'eccezione Python. Ma non conosco esempi che adottino questo approccio.


Un'altra considerazione sono le ramificazioni delle prestazioni. In che modo questi vari metodi influenzano la velocità del software? Dovremmo usare una diversa gestione degli errori nelle parti di "controllo" del codice (ad es. Elaborazione dei file di input) rispetto ai "motori" computazionalmente costosi?
LedHead,

Tieni presente che la risposta migliore varia in base alla lingua.
Chrylis

Risposte:


10

Ti darò la mia prospettiva, che è codificata nel progetto deal.II a cui fai riferimento.

Innanzitutto, esistono due tipi di condizioni di errore: errori da cui è possibile recuperare e errori da cui non è possibile recuperare.

  • Il primo è, ad esempio, se un file di input non può essere letto, ad esempio se stai leggendo informazioni da un file come quello $HOME/.dealiiche potrebbe esistere o meno. La funzione di lettura dovrebbe semplicemente tornare alla funzione di chiamata affinché quest'ultima capisca cosa fare. È possibile che al momento una risorsa non sia disponibile ma potrebbe essere di nuovo tra un minuto (un file system montato in remoto).

  • Quest'ultimo è, ad esempio, se stai cercando di aggiungere un vettore di dimensione 10 a un vettore di dimensione 20: prova come potresti, non c'è nulla che si possa fare al riguardo - c'è un bug nel codice che ha portato a il punto in cui abbiamo tentato di fare l'aggiunta.

Queste due condizioni dovrebbero essere trattate in modo diverso, indipendentemente dal linguaggio di programmazione che stai utilizzando:

  • Nel secondo caso, poiché non vi è alcun ricorso, chiudere il programma. Potresti farlo lanciando un'eccezione o restituendo un codice di errore che indica al chiamante che non è possibile fare nulla, ma potresti anche interrompere immediatamente il programma poiché ciò rende molto più facile per il programmatore il debug del problema.

  • Nel primo caso, si è verificata una situazione eccezionale che potrebbe essere gestita. Anche se C e Fortran non avevano mezzi per esprimerlo, tutte le lingue ragionevoli che sono arrivate in seguito hanno incorporato modalità nello standard linguistico per gestire tali ritorni "eccezionali" fornendo, beh, "eccezioni". Usa questi: ecco a cosa servono; sono inoltre progettati in modo tale da non dimenticare di ignorarli (se lo fai, l'eccezione si propaga solo di un livello superiore).

In altre parole, quello che sto sostenendo qui (e che affare.II fa) è una miscela delle tue strategie 3 e 5, a seconda del contesto. È vero che 3 non funziona in lingue come C o Fortran - nel qual caso si potrebbe sostenere che questa è una buona ragione per non usare semplicemente le lingue che rendono difficile esprimere ciò che si desidera fare.

X), ma poiché il valutatore deve essere chiamato ripetutamente, non dovrebbe solo bloccarsi, ma generare un'eccezione. In tali casi, anche se non è possibile recuperare un valore negativo, è consigliabile generare un'eccezione anziché interrompere il programma. Non ero d'accordo con questa posizione un paio d'anni fa, ma ho cambiato idea dopo che le linee guida del software della comunità xSDK hanno codificato il requisito che i programmi non dovrebbero mai andare in crash (o almeno dovrebbero avere un modo per passare da crash a eccezione). II ora ha la possibilità di fare Assertun'eccezione invece di chiamare abort().)


Vorrei solo raccomandare il contrario: generare un'eccezione quando la situazione non può essere gestita e restituire un codice di errore quando può essere gestito. Il problema è che gestire le eccezioni generate è complicato: il programmatore dell'applicazione deve conoscere il tipo di tutte le possibili eccezioni per catturarle e gestirle, altrimenti il ​​programma andrà in crash. L'arresto anomalo è corretto e persino gradito per le situazioni che non possono essere gestite, poiché il punto di arresto anomalo viene segnalato immediatamente con Python, ad esempio, ma per le situazioni che possono essere gestite, (per lo più) non è il benvenuto.
cdalitz,

@cdalitz: è un difetto di progettazione del C ++ che puoi lanciare oggetti di qualsiasi tipo. Ma qualsiasi software ragionevole (Trilinos escluso) genera solo eccezioni che derivano std::exceptione queste possono essere rilevate per riferimento senza conoscere il tipo derivato.
Wolfgang Bangerth,

1
Ma non sono assolutamente d'accordo con la restituzione di un codice di errore per i motivi indicati nella domanda originale: (i) i codici di errore vengono ignorati troppo spesso e, di conseguenza, gli errori non vengono gestiti affatto; (ii) in molti casi, semplicemente non esiste un valore eccezionale che possa ragionevolmente essere restituito dato che il tipo di ritorno della funzione è fisso; (iii) le funzioni hanno tipi di restituzione diversi e dovresti definire in ogni caso separatamente quale sia il valore "eccezionale" che rappresenta un errore.
Wolfgang Bangerth,

WB ha scritto (scusate, il trucco '@' non funziona per qualche motivo e il nome utente viene rimosso da StackExchage per qualche motivo): "I codici di errore vengono ignorati troppo spesso". Questo vale ancora di più per la rilevazione delle eccezioni: non molti sviluppatori di software si prendono la briga di raggruppare ogni chiamata di funzione in un blocco try / catch. Ma è soprattutto una questione di gusti: finché la documentazione indica chiaramente se e quali eccezioni genera una funzione, posso gestirla. Ma ancora si potrebbe dire: il dovere di scrivere la documentazione viene ignorato troppo spesso ;-)
cdalitz,

Ma il punto è che se si dimentica di catturare un'eccezione, non ci sono problemi a valle: il programma si interrompe. Sarà facile trovare dove si è verificato il problema. Se si dimentica di controllare il codice di errore, il programma potrebbe bloccarsi in un secondo momento a causa di uno stato interno indefinito, ma dove il problema originale era del tutto non chiaro. È estremamente difficile trovare questo tipo di bug.
Wolfgang Bangerth,
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.