« Je peux soit restructurer le code ou utiliser une petite GOTO à la place. Tant pis, ça ne peut pas être si terrible que ça, goto main_sub3; »
Depuis les 1970s, les programmeurs modernes ont commencé à rejeter cette instruction, sous le motif qu’elle rend les programmes plus difficiles à comprendre et à maintenir (on parle dans ce cas de programmation spaghetti). Depuis lors, on a commencé à recourir à des structures comme les conditionnelles (if .. then .. else ..) ou les boucles (for, while, etc.) qui font partie intégrante de tous les langages de programmation impératifs modernes.
Edsger Dijkstra et Niklaus Wirth ont défendu l'idée selon laquelle l'instruction goto ne peut que mener à du code illisible. D'autres, comme Linus Torvalds ou Robert Love, ont fait remarquer que même si elle incite à produire du code spaghetti, l'instruction goto peut être appréciable et rendre au contraire le code plus lisible, lorsqu'elle est employée à bon escient.
Alors dans quel cas cette instruction est-elle encore utilisée ? Parmi les cas de figure évoqués existent la sortie d’une boucle imbriquée ce qui épargne le recours à plusieurs break, l’amélioration de la lisibilité du code et le traitement des erreurs ou encore l’optimisation manuelle du code pour améliorer les performances.
Dans les deux premiers cas, toute utilisation parcimonieuse semble correcte, mais pas dans le dernier cas selon Jeff Law et Jason Merril, tous les deux ingénieurs chez Red Hat et membres du comité du compilateur GCC. En effet, ils expliquent que l’optimisation manuelle du code n’est plus à l’ordre du jour, car les compilateurs modernes sont suffisamment développés pour se charger gracieusement de cette tâche, en transformant le code en entrée en une série de blocs de base et en se reposant sur l’utilisation d’un graphe de flot de contrôle (GFC). Résultat des courses aucune distinction entre un code bien structuré et un code en spaghetti (qui résulte d’une addiction au goto).
Maintenant dans le monde réel, RASTER montre comment l’instruction GOTO peut être utile quelques fois.
Code c : | Sélectionner tout |
1 2 3 4 5 6 7 8 | if (shared) lock(); if (data == INVALID) { log_error("blah %s:%i %s() -> %p\n", __FILE__, __LINE__ __FUNC__, data); return NULL; } // smallish body of code hunting through some nested tables using data if (shared) unlock(); return realdata; |
lock() et unlock() sont déclarées static inline pour faciliter la vérification d’erreurs et le logging et aussi pour faciliter la vie au développeur. Alors if(shared) lock(); devient :
Code c : | Sélectionner tout |
1 2 3 4 5 6 7 | if (shared) { if (lock == VALID) { if (!do_lock(lock)) { log_error("lock fail %s:%i %s() -> %p\n", __FILE__, __LINE__ __FUNC__, lock); } } } |
La même chose pour unlock().
Maintenant ce code devrait verrouiller une ressource partagée, aller chercher quelques données et possiblement les déverrouiller et les retourner tout en manipulant les erreurs au passage. Raster s’est rendu compte que ce segment de code consommait entre 6 et 7 % du CPU, ce qui fait un peu beaucoup.
Alors il a passé un peu de temps à réorganiser tout ça :
Code c : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if (shared) { lock(); if (data == INVALID) { log_error("blah %s:%i %s() -> %p\n", __FILE__, __LINE__ __FUNC__, data); return NULL; } // smallish body of code hunting through some nested tables using data unlock(); } else { if (data == INVALID) { log_error("blah %s:%i %s() -> %p\n", __FILE__, __LINE__ __FUNC__, data); return NULL; } // smallish body of code hunting through some nested tables using data } return realdata; |
Malgré cela, l'usage du CPU utilisé est resté le même (6-7 %). Raster s’est rendu compte que le locking est coûteux en ressources. Alors il a décidé de recourir à l’instruction GOTO pour gérer les exceptions qui sont rares, en transférant ces cas à la fin de la fonction, pour laisser place aux autres cas plus communs. Cette manoeuvre lui a permis de réduire les pertes de cache.
Code c : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | if (!shared) { if (data != INITIALIZED) goto doinit_shared; doinit_shared_back: if (data == INVALID) goto err_invalid; // smallish body of code hunting through some nested tables using data } else { lock(); if (data != INITIALIZED) goto doinit_shared; doinit_shared_back: if (data == INVALID) goto err_invalid; // smallish body of code hunting through some nested tables using data unlock(); return realdata; } return realdata; doinit_shared: // a few lines of initting data here goto doinit_shared_back; doinit: // a few lines of initting data here goto doinit_back; err_invalid: log_error("blah %s:%i %s() -> %p\n", __FILE__, __LINE__ __FUNC__, data); return NULL; |
Et voilà, l’usage du CPU est tombé à 2,5 % soit 2 à 3 fois la vitesse initiale.
En gros, Raster pense que rien n’est complètement méchant, bien sûr il ne faut pas utiliser GOTO pour remplacer les boucles et les structures de contrôle. Mais pour déplacer le code loin du hot path et manipuler les exceptions, Raster pense que c’est admissible et ça permet de rendre le code plus lisible et fournit surtout une performance accrue qui passe par dessus toute laideur perçue dans le code.
Source : Rasterman
Et vous ?
Qu'en pensez-vous ?