Comment signaler les erreurs dans les bibliothèques scientifiques?

11

Il existe de nombreuses philosophies dans différentes disciplines du génie logiciel sur la façon dont les bibliothèques doivent faire face aux erreurs ou à d'autres conditions exceptionnelles. Quelques-uns de ceux que j'ai vus:

  1. Renvoie un code d'erreur avec le résultat renvoyé par un argument pointeur. C'est ce que fait PETSc.
  2. Renvoie les erreurs par une valeur sentinelle. Par exemple, malloc retourne NULL s'il ne peut pas allouer de mémoire, sqrtretournera NaN si vous passez un nombre négatif, etc. Cette approche est utilisée dans de nombreuses fonctions libc.
  3. Jetez des exceptions. Utilisé dans deal.II, Trilinos, etc.
  4. Renvoie un type de variante; par exemple, une fonction C ++ qui renvoie un objet de type Results'il s'exécute correctement et utilise un type Errorpour décrire comment il échouera std::variant<Error, Result>.
  5. Utilisez assert et crash. Utilisé dans p4est et certaines parties d'igraph.

Problèmes avec chaque approche:

  1. La vérification de chaque erreur introduit beaucoup de code supplémentaire. Les valeurs dans lesquelles un résultat sera stocké doivent toujours être déclarées en premier, introduisant de nombreuses variables temporaires qui ne peuvent être utilisées qu'une seule fois. Cette approche explique quelle erreur s'est produite, mais il peut être difficile de déterminer pourquoi ou, pour une pile d'appels profonds, où.
  2. Le cas d'erreur est facile à ignorer. En plus de cela, de nombreuses fonctions ne peuvent même pas avoir de valeur sentinelle significative si toute la gamme de types de sortie est un résultat plausible. Beaucoup des mêmes problèmes que # 1.
  3. Uniquement possible en C ++, Python, etc., pas en C ou en Fortran. Peut être imité en C en utilisant la sorcellerie setjmp / longjmp ou libunwind .
  4. Uniquement possible en C ++, Rust, OCaml, etc., pas en C ou en Fortran. Peut être imité en C en utilisant la macro sorcellerie.
  5. Sans doute le plus informatif. Mais si vous adoptez cette approche pour, disons, une bibliothèque C pour laquelle vous écrivez ensuite un wrapper Python, une erreur idiote comme passer un index hors limites à un tableau plantera l'interpréteur Python.

Une grande partie des conseils sur Internet concernant la gestion des erreurs est écrite du point de vue des systèmes d'exploitation, du développement intégré ou des applications Web. Les accidents sont inacceptables et vous devez vous soucier de la sécurité. Les applications scientifiques n'ont pas ces problèmes à peu près dans la même mesure, voire pas du tout.

Une autre considération est de savoir quels types d'erreurs sont récupérables ou non. Un échec de malloc n'est pas récupérable et, dans tous les cas, le tueur de mémoire insuffisante du système d'exploitation y parviendra avant vous. Un index hors limites pour une taille de tableau n'est pas récupérable non plus. Pour moi en tant qu'utilisateur, la meilleure chose qu'une bibliothèque puisse faire est de planter avec un message d'erreur informatif. D'autre part, l'échec, par exemple, d'un solveur linéaire itératif à converger pourrait être récupéré en utilisant un solveur à factorisation directe.

Comment les bibliothèques scientifiques doivent-elles signaler les erreurs et s'attendre à ce qu'elles soient traitées? Je réalise bien sûr que cela dépend de la langue dans laquelle la bibliothèque est implémentée. Mais pour autant que je sache, pour toute bibliothèque suffisamment utile, les gens voudront l'appeler à partir d'une autre langue que celle dans laquelle elle est implémentée.

Soit dit en passant, je pense que l'approche # 5 peut être considérablement améliorée pour une bibliothèque C si elle définit un pointeur de fonction de gestionnaire d'assertion global dans le cadre de l'API publique. Par défaut, le gestionnaire d'assertion signale le numéro de fichier / ligne et se bloque. Les liaisons C ++ pour cette bibliothèque définiraient un nouveau gestionnaire d'assertion qui lève à la place une exception C ++. De même, les liaisons Python définiraient un gestionnaire d'assertion qui utilise l'API CPython pour lever une exception Python. Mais je ne connais aucun exemple qui adopte cette approche.

Daniel Shapero
la source
Une autre considération est les ramifications de performances. Comment ces différentes méthodes affectent-elles la vitesse du logiciel? Devrions-nous utiliser une gestion des erreurs différente dans les parties "de contrôle" du code (par exemple, le traitement des fichiers d'entrée) par rapport aux "moteurs" coûteux en calcul?
LedHead
Notez que la meilleure réponse variera selon la langue.
chrylis -on strike-

Réponses:

10

Je vais vous donner ma perspective, qui est encodée dans le projet deal.II que vous référencez.

Tout d'abord, il existe deux types de conditions d'erreur: les erreurs récupérables et les erreurs non récupérables.

  • Le premier est, par exemple, si un fichier d'entrée ne peut pas être lu - par exemple si vous lisez des informations d'un fichier tel que $HOME/.dealiicelui-ci peut ou non exister. La fonction de lecture doit simplement revenir à la fonction appelante pour que celle-ci sache quoi faire. Il se peut également qu'une ressource ne soit pas disponible pour le moment mais peut l'être à nouveau dans une minute (un système de fichiers monté à distance).

  • Ce dernier est, par exemple, si vous essayez d'ajouter un vecteur de taille 10 à un vecteur de taille 20: Essayez comme vous pourriez, il n'y a rien qui puisse être fait à ce sujet - il y a un bogue dans le code qui a conduit à le point où nous avons essayé de faire l'ajout.

Ces deux conditions doivent être traitées différemment, quel que soit le langage de programmation que vous utilisez:

  • Dans le deuxième cas, puisqu'il n'y a pas de recours, mettez fin au programme. Vous pouvez le faire en lançant une exception ou en renvoyant un code d'erreur qui indique à l'appelant que rien ne peut être fait, mais vous pourriez aussi bien abandonner le programme immédiatement car cela facilite grandement la tâche du programmeur pour déboguer le problème.

  • Dans le premier cas, une situation exceptionnelle s'est produite qui pourrait être traitée. Même si C et Fortran n'avaient aucun moyen d'exprimer cela, tous les langages raisonnables qui sont venus plus tard ont incorporé des moyens dans la norme linguistique pour traiter de tels retours "exceptionnels" en fournissant, eh bien, des "exceptions". Utilisez-les - c'est pour cela qu'ils sont là; ils sont également conçus de telle manière que vous ne pouvez pas oublier de les ignorer (si vous le faites, l'exception se propage juste un niveau plus haut).

En d'autres termes, ce que je préconise ici (et ce que fait deal.II) est un mélange de vos stratégies 3 et 5, selon le contexte. Il est vrai que 3 ne fonctionne pas dans des langues comme C ou Fortran - dans ce cas, on peut affirmer que c'est une bonne raison de ne pas utiliser de langues qui rendent difficile l'expression de ce que vous voulez faire.

Je noterai que certains systèmes ne devraient tout simplement pas tomber en panne, même dans les cas où les erreurs ne sont pas récupérables. Par exemple, un ensemble de fonctions est appelé à plusieurs reprises pour un certain nombre de requêtes, par exemple pour évaluer une fonction de vraisemblance pour des entrées données dans un schéma d'échantillonnage statistique. Peut-être que l'évaluateur ne peut pas traiter les valeurs négatives car le problème n'a aucun sens dans cette situation (par exemple, évaluer la rigidité d'une plaque métallique d'épaisseurx), mais comme l'évaluateur doit être appelé à plusieurs reprises, il ne doit pas simplement planter, mais simplement lever une exception. Dans de tels cas, même si le passage d'une valeur négative n'est pas récupérable, il convient de lever une exception plutôt que d'interrompre le programme. J'étais en désaccord avec cette position il y a quelques années, mais j'ai changé d'avis après que les directives du logiciel de la communauté xSDK ont codé l'exigence selon laquelle les programmes ne devraient jamais planter (ou du moins devraient avoir un moyen de passer du crash à l'exception - alors accord. II a maintenant la possibilité de faire Assertlever une exception au lieu d'appeler abort().)

Wolfgang Bangerth
la source
Je recommanderais simplement le contraire: lever une exception lorsque la situation ne peut pas être gérée et renvoyer un code d'erreur lorsqu'elle peut être gérée. Le problème est que traiter les exceptions levées est délicat: le programmeur d'application doit connaître le type de toutes les exceptions possibles pour les intercepter et les gérer, sinon le programme va juste planter. Le plantage est correct et même bienvenu pour les situations qui ne peuvent pas être gérées, car le point de plantage est signalé par défaut avec python, par exemple, mais pour les situations qui peuvent être gérées, il n'est (généralement) pas le bienvenu.
cdalitz
@cdalitz: C'est une faille de conception de C ++ que vous pouvez lancer des objets de tout type. Mais tout logiciel raisonnable (à l'exception de Trilinos) ne génère que des exceptions dérivées std::exception, et celles-ci peuvent être capturées par référence sans connaître le type dérivé.
Wolfgang Bangerth du
1
Mais je suis fortement en désaccord avec le retour d'un code d'erreur pour les raisons décrites dans la question d'origine: (i) Les codes d'erreur sont trop souvent ignorés et, par conséquent, les erreurs ne sont pas traitées du tout; (ii) dans de nombreux cas, il n'y a tout simplement pas de valeur exceptionnelle pouvant être raisonnablement renvoyée étant donné que le type de retour de la fonction est fixe; (iii) les fonctions ont différents types de retour, et il faudrait définir dans chaque cas séparément quelle serait la valeur "exceptionnelle" qui représente une erreur.
Wolfgang Bangerth
WB a écrit (désolé, l'astuce '@' ne fonctionne pas pour une raison quelconque et le nom d'utilisateur est supprimé par StackExchage pour une raison quelconque): "Les codes d'erreur sont trop souvent ignorés". Cela vaut encore plus pour la capture d'exceptions: peu de développeurs de logiciels prennent la peine de mettre entre crochets chaque appel de fonction dans un bloc try / catch. Mais c'est surtout une question de goût: tant que la documentation indique clairement si et quelles exceptions une fonction lève, je peux la gérer. Mais encore une fois, on pourrait dire: le devoir d'écrire de la documentation est trop souvent ignoré ;-)
cdalitz
Mais le fait est que si vous oubliez d'attraper une exception, il n'y a pas de problème en aval: le programme est simplement abandonné. Il sera facile de trouver où le problème s'est produit. Si vous oubliez de vérifier le code d'erreur, votre programme peut se bloquer ultérieurement à cause d'un état interne indéfini - mais où le problème d'origine était totalement incertain. Il est extrêmement difficile de trouver ce genre de bogues.
Wolfgang Bangerth