C’est d’abord et avant tout une question d’usage et d’éviter d’appliquer des règles de façon dogmatique.
La première et plus importante question, c’est : Quelle doit être la principale caractéristique du code ?
Est-ce qu’on privilégie d’abord la sécurité, la stabilité à long terme, les possibilités d’évolutions rapides, la fiabilité, les performances… ? Quels impacts on s’autorise sur les autres aspects du code ? Dans quel mesure ?
Et donc oui, privilégier d’abord autre chose que les performances peut mener à… ben, du code peu performant. Ça ne devrait être une surprise pour personne.
Là où ça devient un problème, c’est quand on érige une règle en dogme et qu’on l’applique par réflexe, sans se demander si elle est pertinente ; ou pire, là où elle est d’évidence contre-productive.
Donc, oui, exiger d’un code dont le but principal est « être le plus performant possible » qu’il soit développé en respectant à la lettre des règles qui sont là pour garantir un souplesse d’évolution à toute épreuve, c’est complètement stupide. Mais le problème c’est pas les règles elles-mêmes, c’est leur domaine d’application.
exiger d’un code dont le but principal est « être le plus performant possible » qu’il soit développé en respectant à la lettre des règles qui sont là pour garantir un souplesse d’évolution à toute épreuve,
Moui. Perso je dirais que l'abus d'héritage mène plus rapidement encore à un plat de spaghetti que l'usage de goto par contre, donc la maintenabilité et la souplesse quand on abuse de la POO et des dogmes, ben… je ne suis pas trop d'accord :)
Mon propos, c’est qu’il ne faut jamais appliquer des règles de façon dogmatique, mais toujours de façon pragmatique. Quelle que soit la règle (et c’est valable partout, pas seulement en développement informatique).
Par exemple, si ton héritage (qui soit dit en passant est presque toujours une mauvaise pratique, oui, même en Java, mieux vaut privilégier de la composition) mène à un code impossible à maintenir alors qu’on a cherché la maintenabilité, alors soit la règle est mauvaise, soit elle est mal appliquée, soit il y a un problème de conception plus général.
Posté par arnaudus .
Évalué à 5.
Dernière modification le 10 mars 2023 à 10:40.
Le peu que j'ai appris sur ces histoires d'optimisation, c'est
1) Laisser le compilateur faire son job (donc, coder très clairement)
2) Utiliser les paradigmes du langage
Pour le 1 par exemple, je n'arrive toujours pas à bien réaliser à quel point les branchements sont coûteux par rapport aux calculs. Pendant longtemps j'ai écrit des trucs du style:
double ans = 0.0;
if (a != 0.0)
ans = a*log(truc)/(sin(bidule)-pow(machin, 3.7));
et ça s'est avéré désastreux en termes de perfs, parce les processeurs modernes sont justes très, très, très rapides pour les calculs.
Pour le 2, ça me gonflera toujours autant d'entendre 'tel langage est lent'. Il n'y a pas de langages intrinsèquement lent (évidemment, les trucs interprétés sont toujours moins performants, mais pour la plupart des applications même un facteur 10 ne se voit pas). Par contre, il y a des langages qui ont des paradigmes différents, et si on code tout comme en C, on va voir apparaitre des ralentissements insupportables ; ça n'est pas un problème du langage.
et ça s'est avéré désastreux en termes de perfs, parce les processeurs modernes sont justes très, très, très rapides pour les calculs.
Ils sont aussi très bon en prédiction de branchement et du coup ça me parait pas si débile que ça, le calcul étant en parallèle du test de branchement et le tout aussi optimisé (surtout si tu sais les probas et met un likely/unlikely qui va bien).
OK, je n'ai pas testé, mais tu as de tête le niveau de désastre?
Le test est mauvais dans les 2 sens, que ce soit pour détecter un nombre non nul ou un nul. Si le nombre devrait être nul et que tu le crois non nul parce que seulement très petit, tu risques d'obtenir un résultat bizarre en l'utilisant comme diviseur.
Vous avez tous les trois raison.
Après, il se trouve qu'on est en train de débattre d'un mauvais exemple (certainement un truc plus ou moins de mémoire, en tout cas a n'est pas dans le contexte approprié pour apprécier la pertinence du test… De même, on ne peut pas affirmer être dans un contexte où le nombre risque d'être très petit puisque visiblement on travaille à l'ordre du dixième j'ai l'impression)
Un point que l'exemple illustre, à mon avis, est que les langages comme C (mais pas que) sont piégeux car nécessitant d'avoir plein de considérations (d'architecture machine et aussi d'arithmétique flottante IEEE ici) alors en Ada on aurait défini un décimal d'une précision d'un chiffre et l'algorithme aurait été aussi simple sans devoir se préoccuper du compilateur et autres nœuds au cerveau. Fin de l'instant pub.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Si, si, exemple lu, et c'est pour ça que je dis qu'on n'a pas tout le contexte (si ça se trouve y a une division plus loin ou alors c'est juste pas la bonne formule) ; pour un résultat zéro cela a été répondu plus tôt en parlant des prédictions de branchement ;-) (ne p;s se croire plus malin que les compilateurs modernes)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Si, si, exemple lu, et c'est pour ça que je dis qu'on n'a pas tout le contexte (si ça se trouve y a une division plus loin ou alors c'est juste pas la bonne formule)
Il parle d'optimisation pas de validité du calcul.
Le point c'est simplement qu'il pensait qu'une condition plus un saut était plus efficace qu'une opération mathématiques qui peu paraître complexe.
S'il s'agissait d'éviter une division par 0, retirer la condition est moins triviale et n'est pas toujours possible ou souhaitable.
Comme tu dis, « peu paraitre complexe » …Sauf que l'exemple tel que donné est juste du « X×Y÷Z » (si c'est la bonne formule, c'est pas si complexe pour nombre de compilos)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
Merci d'avoir lu ce que j'avais écrit, je commençais à me demander si on parlait la même langue…
Je voulais juste donner un exemple d'optimisation prématurée qui ne fonctionnait pas, et insister d'une manière générale sur la difficulté de prédire les perfs d'un compilateur et d'un processeur moderne dans un contexte non-trivial (branchement vs calcul). Donc au final, coder clairement et laisser le compilateur gérer, c'est exactement le message que je voulais passer.
Posté par barmic 🦦 .
Évalué à 3.
Dernière modification le 12 mars 2023 à 22:57.
Pour le prendre d'un point de vu humoristique, il faut le voir autrement les gens sont tellement totalement en mode automatique. Ils voient x != 0.0, ils ne cherchent plus à comprendre quoi que ce soit et ressortent les commentaires random déjà ressorti pleins de fois.
Par contre, je suis curieux est-ce que le cas arrivait dans la vrai vie ? Parce que même si le calcul était compliqué (imaginons le même mais sur des nombres de taille arbitraire par exemple), si les cas où l'optimisation se déclenche était trop rare par rapport au reste, ça resterait une optimisation discutable.
Dans ce cas, a était (indirectement) un paramètre (0.0 par défaut). Du coup, il était tout à fait crédible que a soit exactement 0, et ça avait du sens de gérer cette situation. Je préfère largement ça à un paramétrage redondant (style un switch booléen, et une variable numérique qui n'a de sens que si le switch est activé).
J'aurais eu plus de doutes pour une variable différente de 0, style 1.0. En théorie, si on est carré sur les types, j'imagine que c'est possible d'avoir un test exact, parce que la représentation binaire ne devrait pas changer (si le fichier de paramètres contient MAVAR=1.0, qu'on lit ça avec >> pour mettre dans un type à virgule flottante, qu'on ne transtype pas et qu'on finit par tester == 1.0, j'ai l'intuition que ça devrait marcher). Mais avec 0.0 ça doit forcément marcher, sauf s'il y a une gestion vraiment déconnante des types à virgule flottante.
Le pire dans cette histoire, c'est que même si le test ne fonctionnait pas comme prévu (si a était le résultat d'un calcul numérique, style a = 3.0 - sqrt(9.0)), l'algo restait tout à fait fonctionnel (il aurait multiplié par 1e-32 et aurait obtenu quelque chose du même ordre de grandeur). Il n'y avait donc aucune raison de partir en vrille.
si les cas où l'optimisation se déclenche était trop rare par rapport au reste, ça resterait une optimisation discutable.
J'imagine qu'en théorie la prédiction de branches permet justement de faire en sorte que ça marche. Mais j'ai eu l'impression (ce n'est qu'une impression, je n'ai pas analysé plus que ça) que dans tous les cas les deux branches étaient calculées (peut-être parce que le nombre de cycles nécessaire était inférieur à un critère que je ne connais pas), et que la version avec if() nécessitait de revenir en arrière alors que le coût du calcul était de toutes manières déja passé.
J'ai aussi à mon actif quelques cas de mémoïsation ratés, parce que ça coûtait plus cher d'aller chercher un résultat dans une table que de calculer une fonction non-triviale. C'est probablement des cas très classiques pour des gens dont le métier est d'optimiser des algorithmes (typiquement, des algos en O(1) en pratique plus lents que des O(2) à cause d'un coût initial), mais quand on n'est confrontés que sporadiquement à des problèmes d'optimisation, c'est toujours destabilisant.
J'ai aussi à mon actif quelques cas de mémoïsation ratés, parce que ça coûtait plus cher d'aller chercher un résultat dans une table que de calculer une fonction non-triviale. C'est probablement des cas très classiques pour des gens dont le métier est d'optimiser des algorithmes (typiquement, des algos en O(1) en pratique plus lents que des O(2) à cause d'un coût initial), mais quand on n'est confrontés que sporadiquement à des problèmes d'optimisation, c'est toujours destabilisant.
Je comprends tout à fait le problème. J'ai fais face à un cas comme ça avec l'Advent of Code où pour trouver une solution efficace il fallait voir qu'un parcourt de graphe était plus chère qu'un produit cartésien. Non seulement je suis pas habitué à faire des parcours d'arbre, mais en plus il est rare que j'utilise au quotidien des algo de complexité au delà de linéaire. Du coup utiliser un algo en n² pour aller plus vite n'est pas intuitif pour moi
Il me semble qu'un bon compilateur est capable de convertir de nombreux branchements en opérations arithmétiques. Par exemple :
if (condition)
output = something
else
output = whatever
peut être converti en :
output = int(condition) * something + int(! condition) * whatever
Si tu as constaté des effets désastreux sur les perf, c'est soit que ton langage est lent compilateur ne fait pas bien son boulot, soit que tu lui as donné des spaghettis à manger.
L'auteur de l'article (re)découvre que l'abstraction a un coût et que du pur C (qu'il oppose au "clean code") est plus rapide que de l'abstraction réalisé via des classes en C++. Ensuite, il (re)découvre qu'une lookup table est plus rapide qu'un switch case. Au passage, je ne connais pas la règle qui empêche d'utiliser des lookup tables dans le "clean code". Si j'ai compris sa troisième remarque semble être que plus on fait de choses, plus la différence est importante. Ca me parait logique, si une construction de code est moins performante, l'ajout de calculs également rendre le code encore moins performant relativement à ce qu'il était avant.
Je pense que l'article est intéressant car il rappelle que les choix ont un impact sur les performances et il y a certaines choses qu'on oublie facilement.
Il me semble pourtant que depuis "quelques" années, l'héritage est considéré, à raison, comme étant un truc à éviter (sauf cas particuliers) parce que, entres autres:
c'est lent (oui les vtables ont un coût)
ce n'est pas aussi maintenable que ça en a l'air de prime abord: non, remonter une pile de 10 classes pour comprendre ce que fait une fonction, ce n'est pas cool
Après, il faut reconnaître que les exemples habituels pour la POO, et notamment celui des formes, sont catastrophiques.
Je ne crois pas qu'il soit vraiment utile de rappeler, par exemple, que les variables ça sert à quelque chose?
Donc, l'idée de faire deux classes différentes pour le carré et le rectangle? C'est débile. La seule raison que je vois, c'est si on a besoin de stocker une immense quantité de carrés, tellement immense que les 4 octets (voire moins) requis pour stocker la 2nd dimension aurait un réel impact sur les performances. Ce n'est pas franchement le cas le plus commun, perso ça ne m'est jamais arrivé. Je dirais donc que c'est débile de faire ça d'entrée de jeu, et que la raison la plus probable, c'est de faire de la merde pour filer un exemple bancal. Un carré est un rectangle, c'est tout. Pas la peine de chercher midi à quatorze heures comme on dit.
D'ailleurs, on peut vraiment décrire un triangle avec juste une hauteur et une largeur? Bien sûr que non! C'est encore un truc débile.
Un triangle requiers plus de données que ça.
Bref, en se basant sur des exemples débiles, c'est effectivement facile de démonter la POO. Ceci dit, je suis d'accord sur le fond: on a traversé plusieurs décennies ou la POO était présentée comme la panacée, et malheureusement on ne peut pas mettre à jour toutes les ressources du monde pour dire qu'en fait non, faire 5 couches d'héritage pour le fun, ce n'est pas une bonne architecture.
Perso, je vois l'héritage comme le goto: c'est utile, mais si on peut s'en passer, autant s'en passer. L'utiliser partout ou le bannir absolument, c'est du dogmatisme, et ça ne mène à rien de bon. C'est bien pour ça que j'apprécie C++ d'ailleurs: je fais de l'objet… si je veux, et si j'ai pas envie, j'en fait pas. En pratique, j'en utilise souvent, mais je n'ai pas souvenir d'avoir un seul programme qui soit de l'objet pur… j'ai toujours des fonctions qui se baladent, du code qui pourrait être mis en POO mais ça serait juste se faire chier pour rien, qui côtoie des objets avec plus ou moins (plutôt moins) de hiérarchie.
D'ailleurs, on peut vraiment décrire un triangle avec juste une hauteur et une largeur? Bien sûr que non! C'est encore un truc débile.
Bien sur que si : un triangle rectangle. Et on peut décrire un triangle equilateral avec une seule dimansion. Un triangle rectangle isocèle se décrit avec juste la mesure de l'un des deux côtés constituant l'angle droit.
Mais comme tu le soulignes, ce sont des triangles particuliers, comme les carrés sont des rectangles particuliers. Maintenant fau-il une classe par type de forme ? Ca doit dépendre du besoin.
Perso, je vois l'héritage comme le goto: c'est utile, mais si on peut s'en passer, autant s'en passer. L'utiliser partout ou le bannir absolument, c'est du dogmatisme, et ça ne mène à rien de bon.
En pratique, j'en utilise souvent, mais je n'ai pas souvenir d'avoir un seul programme qui soit de l'objet pur…
Ta remarque me fait penser à quelque chose : Comment modéliser ce type de choses en UML ?
J'étend la question à un sujet qui me trottine dans la tête depuis un moment : UML est un modèle qui est assez "orienté" objet (il ne devrait d'ailleurs pas s'appeler "Uml" mais OML". Je le trouve inadapté pour modéliser des concepts issus des langages fonctionnels ou il n'y a pas forcément d'objet (je pense notamment à Erlang qui a la notion de process que l'on voit un peu partout - avec OTP et le concept de worker/superviseur ou de behavior). N'étant ni un spécialiste UML, ni un spécialiste Erlang, je me demande encore si UML est adapté à la modélisation Erlang (je pense que certaines choses peuvent se représenter en UML, mais qu'il manque quelque chose à UML pour pouvoir modéliser correctement ces concepts - mais c'est peut-être de la méconnaissance).
Ta remarque me fait penser à quelque chose : Comment modéliser ce type de choses en UML ?
Très simple: on ne fait pas. Enfin, je suppose qu'on peut faire un workaround en créant une "classe" qui n'a pas de nom. C'est moche, mais bon, UML est quand même super dogmatique, donc ça force a faire des trucs moches (comme en java ou il faut créer une classe avec une méthode statique pour le main(), parce que sinon spas de l'objet pur… heureusement que le rididule ne tue pas!)
Je pense que pour des langages non objet, UML est assez peu utile en effet. En tout cas, moi ce que j'utilise le plus dans UML, ce sont les diagrammes de classe, qui n'ont évidemment aucun intérêt dans ce type de situation. Par contre je suppose que tu dois pouvoir utiliser d'autres diagrammes (diagramme d'état-transition peut-être?) pour ce genre de choses: je suppose que ça peut servir de hack potable… mais bon, franchement, je n'en vois pas plus que ça l'intérêt.
C'est d'ailleurs valable pour UML de manière générale, je vois ça comme un truc pour faire des jolis dessins pour le chef, histoire qu'il ait l'impression de comprendre ce qu'il se passe.
Comme je l'ai dis, je n'utilise que les diagramme de classe pour ma part, et encore (sauf pour amuser la galerie, dans ces cas la je trouve le diagramme de cas d'utilisation utile, ça remplit les slides…), je ne fait jamais de diagramme complet, ça ne me sers qu'a avoir une vue de haut niveau, pratique pour prendre du recul, réfléchir à l'architecture, ce genre de trucs, mais ça finis souvent désynchronisé de toute façon, et comme on ne peux pas y mettre les fonctions, ben, je m'emmerde pas avec ça.
Après, je ne suis pas non plus un expert UML… je préfère passer mon temps à coder voire écrire de la doc, je trouve ça plus utile (et c'est dans la doc qu'UML à un petit intérêt, mais juste petit).
La semaine dernière, je n'avais pas assez de temps pour développer ma pensée et revenir sur le fond de l'article du lien; aujourd'hui je le peux.
Je voudrais d'abord revenir sur un point qu'il rejette : l'encapsulation ou les types de données abstraits, résumés sous le principe "Code should not know about the internals of objects it’s working with". C'est là un principe que je ne renierai jamais, et dont les développeurs OCaml font un usage abondant : on s'en fout, la plupart du temps, de la représentation concrète d'un type, ce que l'on veut connaître c'est son interface ou API, c'est-à-dire ce que l'on peut faire avec.
J'ai toujours aimé la présentation qu'en faisait John C. Reynolds dans son article Types, abstraction and parametric polymorphism. Il présente l'idée sous la forme d'une fable où des étudiants d'université suivent des cours sur les nombres complexes. Certains étudiants ont pour professeur Descartes qui les définit comme une paire de nombres réels (partie réelle et partie imaginaire), tandis qu'une autre partie des étudiants a pour professeur Bessel qui les définit par leurs coordonnées polaires. Chacun définit ensuite les notions usuels sur les complexes. Puis au cours du semestre, les deux sections sont interchangées, et pourtant les étudiants ne rencontre aucune difficultés bien que les représentations concrètes choisies par chacun des enseignants soient distinctes. C'est cela l'essence des types de donnés abstraits.
Bon revenons à nos moutons, et à l'article qui critique ce principe pour des raisons de performances. Dans un premier temps, il ne fait pas que suivre ce principe mais applique toute la panoplie de la POO. Ensuite, au lieu de faire une hiérarchie de classes, il va définir un type somme de quatre figures et faire un switch statement pour comparer les performances. Ce qui revient à implémenter quelque chose du genre :
Maintenant, constatant que les formules de calculs de surfaces ont des similarité, il tente son optimisation ultime. En Ocaml cela ressemblerait à cela :
Il y a déjà un point problématique dans son code s'il ne cache pas la représentation de son type à l'extérieur : comment garantir l'invariant qu'un carré a nécessairement une largeur égale à sa hauteur ?
Ensuite au niveau des interfaces, chacun des deux modules peuvent se voir donner exactement la même :
moduletypeForme_simple=sigtypet(* constructeurs de formes *)valsquare:float->tvalrectangle:float->float->tvaltriangle:flaot->float->tvalcircle:float->t(* méthodes sur les formes *)valarea:t->floatend
Et maintenant, il y a même mieux entre ces deux modules : algébriquement, ces deux structures sont totalement isomorphes (comme pour Descartes et Bessel dans la fable de Reynolds). Autrement dit, elles sont complétement équivalentes pour l'utilisateur et, en tant qu'implémenteur, je peux passer de l'une à l'autre sans perturber aucunement le code client, à la condition de respecter le principe Code should not know about the internals of objects it’s working with. ;-)
Enfin, si l'on revient à mon graphe de la catégorie des structures algébriques correspondantes, comme pour le type Top il n'y a pas qu'une implémentation possible pour la somme de quatre structures. Ce qu'il a constaté c'est qu'en choisissant une implémentation adaptée, on peut avoir un code plus performant. En revanche, si on ne veut pas perturber le code client de nos modules, il faut cacher les détails d'implémentation. D'ailleurs, si seule la méthode area l'intéresse, je peux lui proposer un choix encore plus drastique pour la représentation du type t, à savoir type t = float :-P Autrement dit, on calcule une bonne fois pour toute la surface à la construction (ce qui ne marche réellement que si les valeurs sont immuables ;-)
Une dernière petite remarque pour la route : ces deux modules de formes simples ne sont, par contre, pas isomorphes à la structure Top de la catégorie (celle que l'on manipule en faisant de la POO). Elles n'en sont que des sous-structures, car elles ne contiennent pas toutes les formes possibles et imaginables mais seulement quatre. Ce qui signifie que toutes le formes simples sont des formes, notion qui n'a absolument rien à voir avec l'héritage et une hiérarchie de classes (mais c'est là une toute autre histoire… ;-).
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
Euh… si tu le dis. Je t'avoue que tu m'a largué dès que tu as dégainé OCaml, ce qui est assez habituel me concernant (être largué quand je vois du code de language fonctionnel).
A priori, sur le principe, je suis d'accord avec toi, mais… bon: je ne vois pas le rapport avec la performance, qui est l'un des point principaux de l'article que l'on commente.
Bon, j'ai quand même vaguement compris que tu défends l'idée que les interfaces c'est bien pour la compréhension. Je suis d'accord avec ça.
Par contre, ton commentaire ne parle en rien de performance, je vais donc me permettre de compléter avec mon opinion:
Moi, ça me paraît évident, en fait, et je pense même qu'avec des interfaces suffisamment claires, on récupère de la performance tout simplement parce qu'on va éviter de faire plusieurs fois la même opération pour rien, naturellement.
Autrement dit, je pense que d'avoir des algo super performants dans chaque fonction, ben, je m'en fout. De toute façon moi, je suis nul en algo.
Je préfère de loin me concentrer sur l'architecture: ça permets de faire la maintenance plus facilement, et si besoin est, de remplacer, justement, les algo par d'autres mieux adaptés en fonction du contexte.
Dans son exemple, si on a besoin plusieurs fois de la surface d'un carré, on va la calculer a chaque fois. Bonjour la performance… alors qu'avec des classes, aussi pourries soient-elles dans l'exemple qu'il a effectivement piqué à un «défenseur de la POO» (la POO a des soucis a se faire avec ce type de défenseurs…) il serait possible à posteriori de modifier les classes pour ne faire le calcul que lors des modifications des côtés du carrés, rectangle, … et ce, sans devoir passer au peigne fin quelques dizaines de milliers de lignes de code. Pour les petits projets. On parle bien vite de centaines de milliers, après tout…
Et maintenant, il y a même mieux entre ces deux modules : algébriquement, ces deux structures sont totalement isomorphes (comme pour Descartes et Bessel dans la fable de Reynolds). Autrement dit, elles sont complétement équivalentes pour l'utilisateur et, en tant qu'implémenteur, je peux passer de l'une à l'autre sans perturber aucunement le code client, à la condition de respecter le principe Code should not know about the internals of objects it’s working with. ;-)
C'était ma compréhension naïve de la POO, mais le contact avec la STL de C++ a mis mes certitudes à rude épreuve. Par exemple, conceptuellement, ça m'a beaucoup gêné de ne pas avoir accès à des fonctions inefficaces pour certains containers. Par exemple, vector::push_front(). C'est une opération inefficace sur un vecteur, certes, mais ça empêche l'abstraction (en particulier, ça rend le benchmarking très difficile, alors qu'il y a des algos pour lesquels les performances relatives de différents containers mériterait d'être testé empiriquement). En gros, avec les containers de la STL, tu es obligé de savoir comment ils sont implémentés, parce que tu n'as accès qu'à une liste restreinte de méthodes en fonction de l'efficacité algorithmique des opérations. Est-ce qu'il n'y a pas là des principes qui se contredisent, et que les concepteurs de la STL ont choisi de trancher dans un sens sans laisser l'utilisateur décider?
En fait le cas de la STL est plutôt intéressant je trouve. A l'origine, c'était effectivement "tout dans les méthodes", "parce que les class c'est la classe". Bon, j'exagère peut-être un peu, mais probablement pas tant que ça. Voir par exemple std::string qui est selon moi une très mauvaise interface.
Hors, de nos jours l'idée est plutôt d'utiliser au maximum #include <algorithm> qui par exemple fournit std::back_inserter, et qui devrais être ce que tu veux. Evidemment, ça n'a pas de sense avec un std::set ou std::map, puisque c'est trié… mais bon, ça permets effectivement de pouvoir remplacer aisément un conteneur par un autre, tant que l'opération est légitime pour le nouveau. Cela dis, je trouve la syntaxe "un peu" lourde, c'est surtout utile quand on écrit des template pour moi.
Je le trouve inadapté pour modéliser des concepts issus des langages fonctionnels
Ça ne sert effectivement à rien. Un jour j'ai demandé à quelqu'un de m'expliquer l'UML, au bout de dix minutes je lui ai dit d'arrêter : je n'avais rien à apprendre d'une personne qui ne sait pas structurer logiquement sa pensée de manière adéquate. En revanche j'use abondamment de diagrammes : ceux de la théorie des catégories. Bon après, j'ai une gros rejet de la POO, non du concept d'objet de la POO, mais du paradigme dans lequel tout est objet (à la Java). C'est pour moi un paradigme d'asile d'aliéné, ou l'art de faire de l'algèbre en marchant sur la tête. Je m'explique en reprenant le cas d'étude de l'article.
Qu'un carré, un rectangle ou un cercle soient des formes, il n'y a là aucun doute. Mais cette propriété n'a rien à foutre dans leur définition ! Ce qui est inévitablement le cas si on les définit comme des objets. Personnellement je les définit ainsi :
Ce qui définit chacun des types, ce sont les enregistrements et la propriété qu'ils ont d'avoir une surface se traduit par l'existence d'une fonction area sur chacun de ces types. Après le concept d'un objet, au sens de la POO, c'est le type suprême de toutes les formes, celui dans lequel on peut injecter n'importe quel forme. Ce que ne semble pas avoir compris l'auteur de l'article du lien, puisqu'il ne considère que le type somme de quatre formes possibles, alors qu'il y a aussi n'importe quel polygone, des ellipses…
Et voilà comment on définit, algébriquement, un tel type :
moduletypeShape=sigmoduletypeS=sigtypetvalarea:t->floatendtype'ameth=(moduleSwithtypet='a)(* les valeurs de type `t` sont des formes *)typet(* toute valeur d'un type `'a` qui possède une méthode `area` est une forme *)valmake:'ameth->'a->tvalarea:t->floatend
Je n'ai donné que l'interface (ou API) d'un module qui définit ce genre suprême, sans préciser les détails d'implémentation ("Code should not know about the internals of objects it’s working with" ;-). Pour l'implémentation du type Shape.t je peux prendre aux choix :
un objet : type t = < area : unit -> float >
une clôture : type t = unit -> float
un gadt : type t = Shape : {meth : 'a meth; self : 'a} -> t
et la liste n'est pas exhaustive.
Et c'est là qu'interviennent les diagrammes de la théorie des catégories. Une forme est caractérisée par l'existence d'une fonction area sur son type. Algébriquement cela se traduit par l'étude des structures algébriques ayant la signature Shape.S :
moduletypeS=typetvalarea:t->floatend
On peut faire un graphe orienté de toutes ces structures : chaque nœud du graphe est un module satisfaisant cette signature, et chaque arête est une fonction qui transforme un nœud en un autre (par exemple, la proposition tous les carrés sont des rectangles est représentée par une telle arrête). Mais dans ce graphe (qui, en soit, contient une infinité de nœuds), il y a un nœud particulier T (ou plutôt une famille de nœud). Ce dernier à la propriété que pour tout autre nœud N du graphe il existe une et une seule arrête qui va de N à T. Ce nœud T est le type Top de toutes ces structures algébriques, celui qui les contient toutes (les carrés, les rectangles, les cercles…). En réalité, il y a une infinité de tels nœuds Top, mais ils sont tous équivalents et ne diffèrent que par leurs détails d'implémentation (j'en ai donné 3 possibles au-dessus). La fonction centrale de cette structure c'est de faire du dynamic dispacth ou du polymorphisme ad-hoc : il applique la fonction area adéquate à la forme qu'il contient. Mais la stratégie à base d'objets et de vtable n'est qu'une solution parmi d'autre pour faire cela. On trouve par exemple, dans des langages existant :
les modules (ou les objets) en OCaml ;
les objets dans tout langage orienté POO ;
les interface en Golang ;
les type class en Haskell ;
les template en C++ (c'est l'équivalent des modules OCaml) ;
les traits en Rust ;
et bien d'autres encore.
Mais pour définir une fonction sur une liste de formes, ou sur une forme quelconque, c'est-à-dire pour faire du polymorphisme ad-hoc, il n'est absolument pas nécessaire de les injecter dans le type Top (qui est le type des objets de la POO), et c'est ce que je reproche à la POO : elle réduit la catégorie à son seul objet terminal (ce type Top), ce qui est une manière plus que bancal de faire de l'algèbre. D'autant que de nombreuses catégories de structures algébriques n'ont pas d'objet terminal, à commencer par toutes celles qui ont des opérateurs binaires (addition, multiplication), où il devient alors vraiment ridicule de suivre le paradigme du tout objet.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
Lecture intéressante. Je trouve qu'il y a beaucoup de mauvaise foi chez ce Casey; en fait, il a une idée fixe, et il essaye de torturer les réponses pour faire rentrer son idée fixe. La discussion est pleine de sous-entendus fallacieux, ç'en est perturbant. Par exemple, Casey essaye de systématiquement donner des exemples de logiciels dont le manque de réactivité est problématique (Microsoft Word, des IDE, etc), comme si ce manque de réactivité était une conséquence directe du fait qu'ils étaient codés selon les principes du Clean Code.
Sur le fond, je trouve que Martin est beaucoup plus convainquant. Les bloatwares sont lents parce qu'ils sont mal conçus, et qu'ils seraient probablement mieux conçus s'ils suivaient le principe du Clean Code. Le coût des appels de fonctions et des tables virtuelles ne peut qu'être totalement négligeable pour de tels logiciels, leur lenteur ne peux être qu'un problème de conception et/ou de ressources externes (biliothèques graphiques?).
Je partage cet intérêt pour la lecture et je partage également l'opinion sur les deux intervenants. Ce que je retiens surtout c'est que les besoins de très grandes performances sont limitées à certains logiciels et certaines parties de ces logiciels. Un autre argument qui est avancé est :
the kind of efficiency the compile engine of an IDE needs is more algorithmic than cycle-lean. Cycle-lean code can increase efficiency by an order of magnitude; but the right choice of algorithm can increase efficiency by many orders of magnitude
Cela pourrait se traduire par : "si tu souhaites améliorer les performances de ton code, regarde tes algorithmes avant de regarder ton architecture".
Posté par freem .
Évalué à 2.
Dernière modification le 10 mars 2023 à 15:58.
Le coût des appels de fonctions et des tables virtuelles ne peut qu'être totalement négligeable pour de tels logiciels
Pourquoi? Qu'est-ce qui empêcherait ce coût d'être élevé en cas d'abus d'héritage (allez, disons qu'il y a 10 niveaux d'héritages, 5 fonctions virtuelles à chaque fois, et que chaque couche a plus de classes que sa couche parente…), de fonctions virtuelles, de dynamic_cast?
Je suis d'accord que le polymorphisme n'est pas à jeter, loin de la, mais de la a dire que ça ne peut avoir qu'un coût négligeable en toute situation, il y a un pas que je ne franchirai pas.
Ca me rappelle le problème des libs qui exportent trop de symboles: ça rend l'OS lent à faire la résolution de nom, augmentant drastiquement le temps de démarrage des applications. Il y avait un article d'un type de RedHat sur ce sujet, je dois avoir ça chez moi dans mes favoris, je vais essayer de le retrouver ce week-end… enfin, si je n'oublie pas d'ici la :D
Pourquoi? Qu'est-ce qui empêcherait ce coût d'être élevé en cas d'abus d'héritage (allez, disons qu'il y a 10 niveaux d'héritages, 5 fonctions virtuelles à chaque fois, et que chaque couche a plus de classes que sa couche parente…), de fonctions virtuelles, de dynamic_cast?
Je suis d'accord que ça n'est pas totalement impossible. Mais on parle de nanosecondes. Donc oui, on va peut-être perdre des millions de nanosecondes. Il n'empêche que ça reste imperceptible, parce que le temps d'exécution, en réalité, c'est des cache miss, des accès disques, des appels système, etc. C'est assez perturbant de voir à quel point les processeurs sont rapides, en fait.
Ça me rappelle une discussion avec notre ingé système, qui n'était pas content quand on lançait 300 exécutables sur un serveur de 100 cœurs. Ça fait péter le load average (et ça fait clignoter ses alarmes), et en théorie les context switch sont coûteux. Et en effet, ça fait des dizaines de milliers de context switch… qui font perdre quelques dizièmes de microsecondes chacun. Donc au final, moins d'une seconde sur des jobs qui tournent sur 3 jours. C'est toujours une histoire d'ordre de grandeur, faire des millions de fois des choses inutiles qui durent 10 milliardièmes de secondes, c'est complètement instantané. Par contre, faire un milliard de fois quelque chose qui prend une microseconde, ça fait planter la machine.
C'est intéressant que tu lui trouves de la mauvaise foi, je trouve au contraire qu'il est de bonne volonté :) Il me semble bien documenté, a clairement lu le livre, et il a regardé les présentations d'Uncle Bob. Il y a beaucoup d'aller-retours au début de la discussion avec des listes de logiciels mais non pas parce qu'ils sont lents, plutôt pour s'assurer que les deux parlent du même type de logiciels. La question étant : qu'est-ce qui a besoin d'un niveau de performance à la nanoseconde, a la milliseconde, ou plus. J'aime beaucoup l'approche d'Uncle Bob aussi, qui distingue le niveau de perf en fonction de la fonctionnalité (i.e. la fenêtre de préférences de Chrome n'a pas besoin d'une réactivité folle, mais le rendu d'une page web doit être le plus efficace possible).
Je ne sens pas de fort désaccord entre les deux. L'un comme l'autre considère que la perf est importante, tant d'un point de vue algorithmique que d'implémentation, et j'en ressors plutôt que l'intention d'Uncle Bob avec Clean Code était plutôt d'améliorer la clarté des parties non impactantes sur les perfs, pour les programmeurs de tous les jours. C'est un peu dommage que ça ait été interprété comme la méthode de développement à appliquer par défaut.
C'est un peu dommage que ça ait été interprété comme la méthode de développement à appliquer par défaut.
C'est bien plus philosophique que ça. Y a t'il une qualité par défaut qu'un logiciel doit avoir ? Et si oui le quel est-ce ?
Certains diront qu'un logiciel doit être le plus rapide possible sauf cas particulier.
D'autres diront qu'il doit être le plus lisible possible sauf cas particulier.
Et on parle là que de 2 qualités, mais il y en a pleins d'autres. La subjectivité de chacun donnera des réponses différentes.
Y a t'il une qualité par défaut qu'un logiciel doit avoir ?
Oui.
Et si oui le quel est-ce ?
Avoir le moins de bugs possible.
Le reste, la lisibilité du code, la vitesse, c'est accessoire. La priorité, c'est que le logiciel fasse sans erreurs ce qu'il a été conçu pour faire, et c'est un équilibre à trouver, toujours.
Les diverses optimisations de performance:
réseau
mémoire vive
stockage
temps CPU
ainsi le coût de maintenance, ce sont des points importants, certes, mais si le programme maintenable; super rapide, mais qu'il ne fait pas ce qu'il devrais, je pense que l'utilisateur va juste le mettre très vite à la poubelle.
Très bien mais du coup ça veut dire quoi ? Il faut passer par de la preuve logiciel ? Il faut s'assurer d'une couverture MC/DC ? Avec test par mutation ?
ainsi le coût de maintenance, ce sont des points importants, certes, mais si le programme maintenable; super rapide, mais qu'il ne fait pas ce qu'il devrais, je pense que l'utilisateur va juste le mettre très vite à la poubelle.
Tu as l'impression que c'est ce qu'il se passe ? Que les logiciels utilisés n'ont pas de bugs ?
Toutes les qualités peuvent avoir ce genre de phrases :
si un logiciel met 1h à se lancer, il va juste être mis à la poubelle
s'il est impossible de relire le code, il va vitre être mis à la poubelle pour être remplacé
Toutes les qualités sont un équilibre entre ce qui est perçu du logiciel, son usage, la subjectivité/appétence/compétences des développeurs, l'investissement dans le logiciel, etc
Personnellement j'ai l'intuition qu'en l'absence de critères permettant de décider, il faut privilégier la lisibilité car elle permet le plus facilement d'améliorer les autres qualités quand on découvrira de quel côté on pèche. Mais c'est un avis. Tout a fait discutable.
Ca veut dire tendre vers 0. Je ne comprend pas pourquoi je dois expliquer ce que "essayer d'avoir moins de bugs" entends.
La méthode diffère selon la situation, les gens, et leurs compétences.
La preuve de coq est une possibilité (il paraît, je ne jamais vu ça mis en place), mais ce n'est pas la seule. On peut aussi utiliser des outils d'analyse statique, les tests unitaires, la programmation par contrat, … il y a plein de méthodes pour essayer de réduire le nombre de bugs. Aucune à ma connaissance n'est parfaite, mais la méthode joue clairement.
De toute façon, l'absence de bugs est impossible (oui, ça contredit les mathématiciens, qui sont pourtant plus intelligents que moi) avec juste du code: on est toujours exposé aux problèmes "environnementaux": avoir une machine qui plante parce qu'un appareil à proximité émets des ondes magnétiques trop fortes, ça arrive, et tu peux prendre l'algo que tu veux, l'archi que tu veux, la vérification formelle que tu veux, ça ne changera rien.
Bon, évidemment, ce n'est pas le type de bugs le plus fréquent, loin de la et heureusement (parce que c'est la merde a trouver, promis) et ça peut sembler extrême, mais j'ai déjà vu un appel téléphonique faire redémarrer un PC…
Tu as l'impression que c'est ce qu'il se passe ? Que les logiciels utilisés n'ont pas de bugs ?
Toutes les qualités peuvent avoir ce genre de phrases :
Tout à fait, et c'est pour ça que j'ai écrit:
La priorité, c'est que le logiciel fasse sans erreurs ce qu'il a été conçu pour faire, et c'est un équilibre à trouver, toujours.
que j'ai du corriger via un nouveau message en:
La priorité, c'est que le logiciel fasse sans erreurs ce qu'il a été conçu pour faire, et le reste, c'est un équilibre à trouver, toujours.
Mais c'est un avis. Tout a fait discutable.
Que je rejoins globalement, mais je suis quand même intrigué: tu as déjà vu des logiciels dont les utilisateurs ne connaissent pas l'utilité? Après, il est aussi vrai que parfois certains bugs sont mués en fonctionnalités, et vice versa.
En tant qu'utilisateur, je me contrefout que le code de linux soit écrit en C, en Rust ou en brainfuck. Ce que je vois (ou pas), c'est quand ça bug. En tant qu'utilisateur, c'est bien la seule chose que je remarque la plupart du temps :D (et en tant que codeur, je sais que c'est difficile a digérer)
Ca veut dire tendre vers 0. Je ne comprend pas pourquoi je dois expliquer ce que "essayer d'avoir moins de bugs" entends.
Comme je l'ai dis tendre vers 0 ne veut rien dire en soit. Quel effort tu met derrière ce tendre vers 0 ? À moins de penser qu'il y a du sabotage, personne n'ajoute de bug volontairement.
L'effort c'est une autre façon de présenter la même question : sur quoi est-ce qu'il faut mettre de l'effort ? (performance/lisibilité/correction/sécurité/…)
Ce ne sont que des qualité personne ne te dira osef, dire que l'on tend vers le mieux possible c'est facile mais ça ne donne pas de réel indications. Je ne serais pas surpris que les logiciels les plus bugués soient ceux qui sortent avec le moins de bug connus par exemple.
La priorité, c'est que le logiciel fasse sans erreurs ce qu'il a été conçu pour faire, et c'est un équilibre à trouver, toujours.
Oui et la seconde partie où je voulais aller. C'est un équilibre qui est arbitraire, subjectif et contextuel. Pour reboucler avec le message au quel je répondais :
Je ne sens pas de fort désaccord entre les deux. L'un comme l'autre considère que la perf est importante, tant d'un point de vue algorithmique que d'implémentation, et j'en ressors plutôt que l'intention d'Uncle Bob avec Clean Code était plutôt d'améliorer la clarté des parties non impactantes sur les perfs, pour les programmeurs de tous les jours. C'est un peu dommage que ça ait été interprété comme la méthode de développement à appliquer par défaut.
Favoriser la clarté quand on ne connaît pas de contraintes particulière, ça n'est pas quelque chose qui paraît déconnant. En particulier pour moi, ça me semble simplifier le refactoring vers autre chose en cas de besoin.
Que je rejoins globalement, mais je suis quand même intrigué: tu as déjà vu des logiciels dont les utilisateurs ne connaissent pas l'utilité?
Dont les utilisateurs ne connaissent pas l'utilité ? Quel est le rapport avec des bugs ?
Je connais par contre un paquet de logiciels bugués et pourtant très utilisés, apprécié voir vénérés par leurs utilisateurs dans le jeux vidéo par exemple. Sans parler de cas particulier comme Star Citizen, beaucoup ont des bugs juste suffisamment peu pour que le gameplay ne soit pas trop embêté. Tu vend ça 45 à 80 € la licence d'utilisation et les clients sont globalement content.
Ce que je vois (ou pas), c'est quand ça bug.
Et c'est assez fréquent. Bon cette semaine j'ai eu un déréférencement de pointeur null au boot sur la dernière version stable, ce qui n'es pas fréquent par contre un driver du noyau qui marche pas si bien ça existe.
Dont les utilisateurs ne connaissent pas l'utilité ? Quel est le rapport avec des bugs ?
Il me paraît très difficile de déclarer qu'un comportement est un bug (sauf pour les crash, forcément… encore que?) si on ne connaît pas le rôle du programme.
par contre un driver du noyau qui marche pas si bien ça existe.
Bien sûr. Les pilotes graphiques sont par exemple plutôt buggués. Je pense que les pilotes wifi sont aussi assez remarquables de ce point de vue. (être remarquable n'est pas forcément une bonne chose)
Posté par arnaudus .
Évalué à 5.
Dernière modification le 15 mars 2023 à 10:55.
Avoir le moins de bugs possible.
Bah déja, des perfs problématiques peuvent être un bug. Si ton logiciel qui gère l'ESP d'une voiture réagit quand la bagnole est déja dans le fossé, c'est une sorte de bug.
Ensuite, je ne suis même pas d'accord. Il y a des logiciels pour lesquels une certaine catégorie de bugs est largement acceptable. Typiquement, c'est le cas des logiciels de rendu: quand Latex te sort des oversize box et te fait dépasser le texte dans la marge, c'est un bug; quand lilypond to sort un truc bof bof, c'est un bug (le rendu s'améliore d'ailleurs de version en version). En tout cas, ce genre de dysfonctionnement est inhérent à la complexité de certaines tâches, et c'est acceptable d'en avoir un certain nombre par rapport à d'autres critères à privilégier (par exemple, le temps d'exécution, ou la clarté des algorithmes). On pourrait aussi citer l'optimisation par les compilateurs par exemple, où il y a toujours un compromis entre l'efficacité et le temps de compilation. Des heuristiques de pathfinding, qui ne te donnent pas forcément le chemin optimal, mais qui font un compromis avec les perfs. Il semble y avoir une infinité d'exemples où une tâche peut ne pas être parfaitement exécutée et où un tel compromis est acceptable.
Du coup, "minimiser les bugs" me semble être un critère parmi d'autres à optimiser, et le poids respectif de ces critères dépend du contexte.
Est-ce que ça existe seulement, un logiciel qui n'a pas de "contrainte particulière"? Tout logiciel a un contexte d'utilisation, qui pondère l'importance des différents critères: clarté du code, performance RAM, performance CPU, évolutivité, expérience utilisateur, fiabilité, etc. Je ne vois pas ce qui ferait qu'automatiquement, le nombre de bugs ouverts serait le critère à optimiser en priorité. Même, au contraire, quel type de logiciel devrait avoir ce critère prioritaire? Les systèmes embarqués ou les systèmes d'aide à la décision, quand des vies humaines, du matériel qui coûte cher, ou des gros capitaux sont en jeu? Mais sur les logiciels courants (traitement de texte, IDE, navigateur, jeux vidéo, compilateurs…), si les bugs ne nuisent pas à l'utilisation (par exemple s'ils sont facilement contournables ou qu'ils concernent des fonctionnalités avancées dont on peut se passer), il est difficile d'imaginer une raison pour laquelle les ressources devraient être consacrées exclusivement à la correction des bugs.
J'aurais donc tendance à considérer que la priorité aux bugs relève de "contraintes particulières". D'ailleurs, beaucoup des logiciels qu'on utilise sont buggés, sans que les équipes de développement s'en émeuvent particulièrement.
Est-ce que ça existe seulement, un logiciel qui n'a pas de "contrainte particulière"?
Un POC ? Un logiciel avec très peu ou pas d'utilisateurs (son résultat est utilisé mais de temps en temps) exécuté sur des machines sur dimensionnées (parce que c'est ce qu'on a) ? Ou simplement tu n'es pas en mesure au moment où tu commence à travailler à déterminer quelle contrainte prime.
Pour le reste c'est tout mon point qu'il est difficile de répondre à cette question et donc (pour reboucler avec le contexte) qu'appliquer clean code par défaut relève plus d'un choix subjectif de développeur que d'un manquement problématique car ça peut amener à des performances désastreuses.
Pour moi un logiciel qui ne fait pas son travail dans des temps acceptables, c'est effectivement un bug.
Et je suis d'accord que parfois, il faut choisir entre plusieurs problèmes le plus urgent à corriger, et il arrive que le plus urgent soit la rapidité d'exécution plutôt que la précision du travail.
Posté par totof2000 .
Évalué à 2.
Dernière modification le 24 mars 2023 à 10:19.
Pour moi un logiciel qui ne fait pas son travail dans des temps acceptables, c'est effectivement un bug.
Le problème c'est que, mis à part dans les logiciels de type temps-réel, jamais on ne spécifie de durée d'exécution pour une tache à exécuter. Certes il existe des "conventions" qui indiquent par exemple qu'une interface de site web doit répondre en moins de x secondes si on ne veut pas perdre un client, mais mis à part les interfaces de logiciels "commerciaux - c'est à dire conçus pour générer de l'argent" (et encore … il y a pléthore d'apps sur android qui ne respectent pas ces règles), on s'asseoit vite sur ces règles lorsque par exemple il s'agit de développer un progiciel propre à une entreprise (on s'en fout que l'utilisateur doit attendre 10 à 15 secondes entre deux clics, ou que le parcours de l'interface soit contre-productif : la santé mentale des employés passe après les économies "de bout de chanfdelle"). Et la façon de faire de l'agilité en rance n'a pas arrangé ces problèmes.
# Usage, dogmatisme et pragmatisme
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 8.
C’est d’abord et avant tout une question d’usage et d’éviter d’appliquer des règles de façon dogmatique.
La première et plus importante question, c’est : Quelle doit être la principale caractéristique du code ?
Est-ce qu’on privilégie d’abord la sécurité, la stabilité à long terme, les possibilités d’évolutions rapides, la fiabilité, les performances… ? Quels impacts on s’autorise sur les autres aspects du code ? Dans quel mesure ?
Et donc oui, privilégier d’abord autre chose que les performances peut mener à… ben, du code peu performant. Ça ne devrait être une surprise pour personne.
Là où ça devient un problème, c’est quand on érige une règle en dogme et qu’on l’applique par réflexe, sans se demander si elle est pertinente ; ou pire, là où elle est d’évidence contre-productive.
Donc, oui, exiger d’un code dont le but principal est « être le plus performant possible » qu’il soit développé en respectant à la lettre des règles qui sont là pour garantir un souplesse d’évolution à toute épreuve, c’est complètement stupide. Mais le problème c’est pas les règles elles-mêmes, c’est leur domaine d’application.
La connaissance libre : https://zestedesavoir.com
[^] # Re: Usage, dogmatisme et pragmatisme
Posté par freem . Évalué à 5.
Moui. Perso je dirais que l'abus d'héritage mène plus rapidement encore à un plat de spaghetti que l'usage de goto par contre, donc la maintenabilité et la souplesse quand on abuse de la POO et des dogmes, ben… je ne suis pas trop d'accord :)
[^] # Re: Usage, dogmatisme et pragmatisme
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 5.
Mon propos, c’est qu’il ne faut jamais appliquer des règles de façon dogmatique, mais toujours de façon pragmatique. Quelle que soit la règle (et c’est valable partout, pas seulement en développement informatique).
Par exemple, si ton héritage (qui soit dit en passant est presque toujours une mauvaise pratique, oui, même en Java, mieux vaut privilégier de la composition) mène à un code impossible à maintenir alors qu’on a cherché la maintenabilité, alors soit la règle est mauvaise, soit elle est mal appliquée, soit il y a un problème de conception plus général.
La connaissance libre : https://zestedesavoir.com
# Paradigme
Posté par arnaudus . Évalué à 5. Dernière modification le 10 mars 2023 à 10:40.
Le peu que j'ai appris sur ces histoires d'optimisation, c'est
1) Laisser le compilateur faire son job (donc, coder très clairement)
2) Utiliser les paradigmes du langage
Pour le 1 par exemple, je n'arrive toujours pas à bien réaliser à quel point les branchements sont coûteux par rapport aux calculs. Pendant longtemps j'ai écrit des trucs du style:
et ça s'est avéré désastreux en termes de perfs, parce les processeurs modernes sont justes très, très, très rapides pour les calculs.
Pour le 2, ça me gonflera toujours autant d'entendre 'tel langage est lent'. Il n'y a pas de langages intrinsèquement lent (évidemment, les trucs interprétés sont toujours moins performants, mais pour la plupart des applications même un facteur 10 ne se voit pas). Par contre, il y a des langages qui ont des paradigmes différents, et si on code tout comme en C, on va voir apparaitre des ralentissements insupportables ; ça n'est pas un problème du langage.
[^] # Re: Paradigme
Posté par freem . Évalué à 5.
Je ne vois pas ou est le problème de dire que le go est lent, pourtant?
[^] # Re: Paradigme
Posté par arnaudus . Évalué à 4.
SAS est drôle, pourtant.
[^] # Re: Paradigme
Posté par Zenitram (site web personnel) . Évalué à 4.
Ils sont aussi très bon en prédiction de branchement et du coup ça me parait pas si débile que ça, le calcul étant en parallèle du test de branchement et le tout aussi optimisé (surtout si tu sais les probas et met un likely/unlikely qui va bien).
OK, je n'ai pas testé, mais tu as de tête le niveau de désastre?
[^] # Re: Paradigme
Posté par Tonton Th (Mastodon) . Évalué à 6.
Toi, tu n'as pas encore expérimenté la fourbitude des nombres flottants…
[^] # Re: Paradigme
Posté par Pol' uX (site web personnel) . Évalué à 3.
La référence à lire sur la chose : https://pages.cs.wisc.edu/~david/courses/cs552/S12/handouts/goldberg-floating-point.pdf
Adhérer à l'April, ça vous tente ?
[^] # Re: Paradigme
Posté par Jean-Baptiste Faure . Évalué à 3.
Oui, enfin, n'importe quel cours de calcul scientifique depuis 50 ans explique pourquoi qu'il ne faut jamais faire ce genre de test.
[^] # Re: Paradigme
Posté par Dr BG . Évalué à 3.
Dans son cas il s'agit juste d'éviter la division par 0, pas vraiment de tester le 0 en soit.
[^] # Re: Paradigme
Posté par Jean-Baptiste Faure . Évalué à 6.
Le test est mauvais dans les 2 sens, que ce soit pour détecter un nombre non nul ou un nul. Si le nombre devrait être nul et que tu le crois non nul parce que seulement très petit, tu risques d'obtenir un résultat bizarre en l'utilisant comme diviseur.
[^] # Re: Paradigme
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 2.
Vous avez tous les trois raison.
Après, il se trouve qu'on est en train de débattre d'un mauvais exemple (certainement un truc plus ou moins de mémoire, en tout cas
a
n'est pas dans le contexte approprié pour apprécier la pertinence du test… De même, on ne peut pas affirmer être dans un contexte où le nombre risque d'être très petit puisque visiblement on travaille à l'ordre du dixième j'ai l'impression)Un point que l'exemple illustre, à mon avis, est que les langages comme C (mais pas que) sont piégeux car nécessitant d'avoir plein de considérations (d'architecture machine et aussi d'arithmétique flottante IEEE ici) alors en Ada on aurait défini un décimal d'une précision d'un chiffre et l'algorithme aurait été aussi simple sans devoir se préoccuper du compilateur et autres nœuds au cerveau. Fin de l'instant pub.
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: Paradigme
Posté par Dr BG . Évalué à 5.
Bah en fait non, c'est même pas une question de division par 0. J'avoue ne pas avoir vraiment regardé l'exemple. Mais vous non plus 😅
En fait il voulait juste éviter de faire un calcul qui donnerait 0 en résultat en pensant qu'evaluer la condition irait plus vite.
[^] # Re: Paradigme
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 2.
Si, si, exemple lu, et c'est pour ça que je dis qu'on n'a pas tout le contexte (si ça se trouve y a une division plus loin ou alors c'est juste pas la bonne formule) ; pour un résultat zéro cela a été répondu plus tôt en parlant des prédictions de branchement ;-) (ne p;s se croire plus malin que les compilateurs modernes)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: Paradigme
Posté par barmic 🦦 . Évalué à 3.
Il parle d'optimisation pas de validité du calcul.
Le point c'est simplement qu'il pensait qu'une condition plus un saut était plus efficace qu'une opération mathématiques qui peu paraître complexe.
S'il s'agissait d'éviter une division par 0, retirer la condition est moins triviale et n'est pas toujours possible ou souhaitable.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Paradigme
Posté par Gil Cot ✔ (site web personnel, Mastodon) . Évalué à 2.
Comme tu dis, « peu paraitre complexe » …Sauf que l'exemple tel que donné est juste du « X×Y÷Z » (si c'est la bonne formule, c'est pas si complexe pour nombre de compilos)
“It is seldom that liberty of any kind is lost all at once.” ― David Hume
[^] # Re: Paradigme
Posté par barmic 🦦 . Évalué à 2.
(soit un log, un sinus et une puissance)
C'est tout l'objet de son commentaire.
Vous vous rendez compte de l'inefficacité de la discussion pour parler d'efficacité ?
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Paradigme
Posté par arnaudus . Évalué à 5.
Merci d'avoir lu ce que j'avais écrit, je commençais à me demander si on parlait la même langue…
Je voulais juste donner un exemple d'optimisation prématurée qui ne fonctionnait pas, et insister d'une manière générale sur la difficulté de prédire les perfs d'un compilateur et d'un processeur moderne dans un contexte non-trivial (branchement vs calcul). Donc au final, coder clairement et laisser le compilateur gérer, c'est exactement le message que je voulais passer.
[^] # Re: Paradigme
Posté par barmic 🦦 . Évalué à 3. Dernière modification le 12 mars 2023 à 22:57.
Pour le prendre d'un point de vu humoristique, il faut le voir autrement les gens sont tellement totalement en mode automatique. Ils voient
x != 0.0
, ils ne cherchent plus à comprendre quoi que ce soit et ressortent les commentaires random déjà ressorti pleins de fois.Par contre, je suis curieux est-ce que le cas arrivait dans la vrai vie ? Parce que même si le calcul était compliqué (imaginons le même mais sur des nombres de taille arbitraire par exemple), si les cas où l'optimisation se déclenche était trop rare par rapport au reste, ça resterait une optimisation discutable.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Paradigme
Posté par arnaudus . Évalué à 3.
Dans ce cas, a était (indirectement) un paramètre (0.0 par défaut). Du coup, il était tout à fait crédible que a soit exactement 0, et ça avait du sens de gérer cette situation. Je préfère largement ça à un paramétrage redondant (style un switch booléen, et une variable numérique qui n'a de sens que si le switch est activé).
J'aurais eu plus de doutes pour une variable différente de 0, style 1.0. En théorie, si on est carré sur les types, j'imagine que c'est possible d'avoir un test exact, parce que la représentation binaire ne devrait pas changer (si le fichier de paramètres contient MAVAR=1.0, qu'on lit ça avec >> pour mettre dans un type à virgule flottante, qu'on ne transtype pas et qu'on finit par tester == 1.0, j'ai l'intuition que ça devrait marcher). Mais avec 0.0 ça doit forcément marcher, sauf s'il y a une gestion vraiment déconnante des types à virgule flottante.
Le pire dans cette histoire, c'est que même si le test ne fonctionnait pas comme prévu (si a était le résultat d'un calcul numérique, style a = 3.0 - sqrt(9.0)), l'algo restait tout à fait fonctionnel (il aurait multiplié par 1e-32 et aurait obtenu quelque chose du même ordre de grandeur). Il n'y avait donc aucune raison de partir en vrille.
J'imagine qu'en théorie la prédiction de branches permet justement de faire en sorte que ça marche. Mais j'ai eu l'impression (ce n'est qu'une impression, je n'ai pas analysé plus que ça) que dans tous les cas les deux branches étaient calculées (peut-être parce que le nombre de cycles nécessaire était inférieur à un critère que je ne connais pas), et que la version avec if() nécessitait de revenir en arrière alors que le coût du calcul était de toutes manières déja passé.
J'ai aussi à mon actif quelques cas de mémoïsation ratés, parce que ça coûtait plus cher d'aller chercher un résultat dans une table que de calculer une fonction non-triviale. C'est probablement des cas très classiques pour des gens dont le métier est d'optimiser des algorithmes (typiquement, des algos en O(1) en pratique plus lents que des O(2) à cause d'un coût initial), mais quand on n'est confrontés que sporadiquement à des problèmes d'optimisation, c'est toujours destabilisant.
[^] # Re: Paradigme
Posté par barmic 🦦 . Évalué à 2.
Je comprends tout à fait le problème. J'ai fais face à un cas comme ça avec l'Advent of Code où pour trouver une solution efficace il fallait voir qu'un parcourt de graphe était plus chère qu'un produit cartésien. Non seulement je suis pas habitué à faire des parcours d'arbre, mais en plus il est rare que j'utilise au quotidien des algo de complexité au delà de linéaire. Du coup utiliser un algo en n² pour aller plus vite n'est pas intuitif pour moi
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Paradigme
Posté par sobriquet . Évalué à 4.
Il me semble qu'un bon compilateur est capable de convertir de nombreux branchements en opérations arithmétiques. Par exemple :
peut être converti en :
output = int(condition) * something + int(! condition) * whatever
Si tu as constaté des effets désastreux sur les perf, c'est soit que ton
langage est lentcompilateur ne fait pas bien son boulot, soit que tu lui as donné des spaghettis à manger.# Redécouverte
Posté par nico4nicolas . Évalué à 5.
L'auteur de l'article (re)découvre que l'abstraction a un coût et que du pur C (qu'il oppose au "clean code") est plus rapide que de l'abstraction réalisé via des classes en C++. Ensuite, il (re)découvre qu'une lookup table est plus rapide qu'un switch case. Au passage, je ne connais pas la règle qui empêche d'utiliser des lookup tables dans le "clean code". Si j'ai compris sa troisième remarque semble être que plus on fait de choses, plus la différence est importante. Ca me parait logique, si une construction de code est moins performante, l'ajout de calculs également rendre le code encore moins performant relativement à ce qu'il était avant.
Je pense que l'article est intéressant car il rappelle que les choix ont un impact sur les performances et il y a certaines choses qu'on oublie facilement.
# l'héritage et les exemples pourris
Posté par freem . Évalué à 8.
Il me semble pourtant que depuis "quelques" années, l'héritage est considéré, à raison, comme étant un truc à éviter (sauf cas particuliers) parce que, entres autres:
Après, il faut reconnaître que les exemples habituels pour la POO, et notamment celui des formes, sont catastrophiques.
Je ne crois pas qu'il soit vraiment utile de rappeler, par exemple, que les variables ça sert à quelque chose?
Donc, l'idée de faire deux classes différentes pour le carré et le rectangle? C'est débile. La seule raison que je vois, c'est si on a besoin de stocker une immense quantité de carrés, tellement immense que les 4 octets (voire moins) requis pour stocker la 2nd dimension aurait un réel impact sur les performances. Ce n'est pas franchement le cas le plus commun, perso ça ne m'est jamais arrivé. Je dirais donc que c'est débile de faire ça d'entrée de jeu, et que la raison la plus probable, c'est de faire de la merde pour filer un exemple bancal. Un carré est un rectangle, c'est tout. Pas la peine de chercher midi à quatorze heures comme on dit.
D'ailleurs, on peut vraiment décrire un triangle avec juste une hauteur et une largeur? Bien sûr que non! C'est encore un truc débile.
Un triangle requiers plus de données que ça.
Bref, en se basant sur des exemples débiles, c'est effectivement facile de démonter la POO. Ceci dit, je suis d'accord sur le fond: on a traversé plusieurs décennies ou la POO était présentée comme la panacée, et malheureusement on ne peut pas mettre à jour toutes les ressources du monde pour dire qu'en fait non, faire 5 couches d'héritage pour le fun, ce n'est pas une bonne architecture.
Perso, je vois l'héritage comme le goto: c'est utile, mais si on peut s'en passer, autant s'en passer. L'utiliser partout ou le bannir absolument, c'est du dogmatisme, et ça ne mène à rien de bon. C'est bien pour ça que j'apprécie C++ d'ailleurs: je fais de l'objet… si je veux, et si j'ai pas envie, j'en fait pas. En pratique, j'en utilise souvent, mais je n'ai pas souvenir d'avoir un seul programme qui soit de l'objet pur… j'ai toujours des fonctions qui se baladent, du code qui pourrait être mis en POO mais ça serait juste se faire chier pour rien, qui côtoie des objets avec plus ou moins (plutôt moins) de hiérarchie.
[^] # Re: l'héritage et les exemples pourris
Posté par totof2000 . Évalué à 4.
Bien sur que si : un triangle rectangle. Et on peut décrire un triangle equilateral avec une seule dimansion. Un triangle rectangle isocèle se décrit avec juste la mesure de l'un des deux côtés constituant l'angle droit.
Mais comme tu le soulignes, ce sont des triangles particuliers, comme les carrés sont des rectangles particuliers. Maintenant fau-il une classe par type de forme ? Ca doit dépendre du besoin.
Ta remarque me fait penser à quelque chose : Comment modéliser ce type de choses en UML ?
J'étend la question à un sujet qui me trottine dans la tête depuis un moment : UML est un modèle qui est assez "orienté" objet (il ne devrait d'ailleurs pas s'appeler "Uml" mais OML". Je le trouve inadapté pour modéliser des concepts issus des langages fonctionnels ou il n'y a pas forcément d'objet (je pense notamment à Erlang qui a la notion de process que l'on voit un peu partout - avec OTP et le concept de worker/superviseur ou de behavior). N'étant ni un spécialiste UML, ni un spécialiste Erlang, je me demande encore si UML est adapté à la modélisation Erlang (je pense que certaines choses peuvent se représenter en UML, mais qu'il manque quelque chose à UML pour pouvoir modéliser correctement ces concepts - mais c'est peut-être de la méconnaissance).
[^] # Re: l'héritage et les exemples pourris
Posté par freem . Évalué à 2.
Très simple: on ne fait pas. Enfin, je suppose qu'on peut faire un workaround en créant une "classe" qui n'a pas de nom. C'est moche, mais bon, UML est quand même super dogmatique, donc ça force a faire des trucs moches (comme en java ou il faut créer une classe avec une méthode statique pour le main(), parce que sinon spas de l'objet pur… heureusement que le rididule ne tue pas!)
Je pense que pour des langages non objet, UML est assez peu utile en effet. En tout cas, moi ce que j'utilise le plus dans UML, ce sont les diagrammes de classe, qui n'ont évidemment aucun intérêt dans ce type de situation. Par contre je suppose que tu dois pouvoir utiliser d'autres diagrammes (diagramme d'état-transition peut-être?) pour ce genre de choses: je suppose que ça peut servir de hack potable… mais bon, franchement, je n'en vois pas plus que ça l'intérêt.
C'est d'ailleurs valable pour UML de manière générale, je vois ça comme un truc pour faire des jolis dessins pour le chef, histoire qu'il ait l'impression de comprendre ce qu'il se passe.
Comme je l'ai dis, je n'utilise que les diagramme de classe pour ma part, et encore (sauf pour amuser la galerie, dans ces cas la je trouve le diagramme de cas d'utilisation utile, ça remplit les slides…), je ne fait jamais de diagramme complet, ça ne me sers qu'a avoir une vue de haut niveau, pratique pour prendre du recul, réfléchir à l'architecture, ce genre de trucs, mais ça finis souvent désynchronisé de toute façon, et comme on ne peux pas y mettre les fonctions, ben, je m'emmerde pas avec ça.
Après, je ne suis pas non plus un expert UML… je préfère passer mon temps à coder voire écrire de la doc, je trouve ça plus utile (et c'est dans la doc qu'UML à un petit intérêt, mais juste petit).
[^] # Re: l'héritage et les exemples pourris
Posté par kantien . Évalué à 4.
La semaine dernière, je n'avais pas assez de temps pour développer ma pensée et revenir sur le fond de l'article du lien; aujourd'hui je le peux.
Je voudrais d'abord revenir sur un point qu'il rejette : l'encapsulation ou les types de données abstraits, résumés sous le principe "Code should not know about the internals of objects it’s working with". C'est là un principe que je ne renierai jamais, et dont les développeurs OCaml font un usage abondant : on s'en fout, la plupart du temps, de la représentation concrète d'un type, ce que l'on veut connaître c'est son interface ou API, c'est-à-dire ce que l'on peut faire avec.
J'ai toujours aimé la présentation qu'en faisait John C. Reynolds dans son article Types, abstraction and parametric polymorphism. Il présente l'idée sous la forme d'une fable où des étudiants d'université suivent des cours sur les nombres complexes. Certains étudiants ont pour professeur Descartes qui les définit comme une paire de nombres réels (partie réelle et partie imaginaire), tandis qu'une autre partie des étudiants a pour professeur Bessel qui les définit par leurs coordonnées polaires. Chacun définit ensuite les notions usuels sur les complexes. Puis au cours du semestre, les deux sections sont interchangées, et pourtant les étudiants ne rencontre aucune difficultés bien que les représentations concrètes choisies par chacun des enseignants soient distinctes. C'est cela l'essence des types de donnés abstraits.
Bon revenons à nos moutons, et à l'article qui critique ce principe pour des raisons de performances. Dans un premier temps, il ne fait pas que suivre ce principe mais applique toute la panoplie de la POO. Ensuite, au lieu de faire une hiérarchie de classes, il va définir un type somme de quatre figures et faire un switch statement pour comparer les performances. Ce qui revient à implémenter quelque chose du genre :
Maintenant, constatant que les formules de calculs de surfaces ont des similarité, il tente son optimisation ultime. En Ocaml cela ressemblerait à cela :
Il y a déjà un point problématique dans son code s'il ne cache pas la représentation de son type à l'extérieur : comment garantir l'invariant qu'un carré a nécessairement une largeur égale à sa hauteur ?
Ensuite au niveau des interfaces, chacun des deux modules peuvent se voir donner exactement la même :
Et maintenant, il y a même mieux entre ces deux modules : algébriquement, ces deux structures sont totalement isomorphes (comme pour Descartes et Bessel dans la fable de Reynolds). Autrement dit, elles sont complétement équivalentes pour l'utilisateur et, en tant qu'implémenteur, je peux passer de l'une à l'autre sans perturber aucunement le code client, à la condition de respecter le principe Code should not know about the internals of objects it’s working with. ;-)
Enfin, si l'on revient à mon graphe de la catégorie des structures algébriques correspondantes, comme pour le type Top il n'y a pas qu'une implémentation possible pour la somme de quatre structures. Ce qu'il a constaté c'est qu'en choisissant une implémentation adaptée, on peut avoir un code plus performant. En revanche, si on ne veut pas perturber le code client de nos modules, il faut cacher les détails d'implémentation. D'ailleurs, si seule la méthode
area
l'intéresse, je peux lui proposer un choix encore plus drastique pour la représentation du typet
, à savoirtype t = float
:-P Autrement dit, on calcule une bonne fois pour toute la surface à la construction (ce qui ne marche réellement que si les valeurs sont immuables ;-)Une dernière petite remarque pour la route : ces deux modules de formes simples ne sont, par contre, pas isomorphes à la structure Top de la catégorie (celle que l'on manipule en faisant de la POO). Elles n'en sont que des sous-structures, car elles ne contiennent pas toutes les formes possibles et imaginables mais seulement quatre. Ce qui signifie que toutes le formes simples sont des formes, notion qui n'a absolument rien à voir avec l'héritage et une hiérarchie de classes (mais c'est là une toute autre histoire… ;-).
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: l'héritage et les exemples pourris
Posté par freem . Évalué à 2.
Euh… si tu le dis. Je t'avoue que tu m'a largué dès que tu as dégainé OCaml, ce qui est assez habituel me concernant (être largué quand je vois du code de language fonctionnel).
A priori, sur le principe, je suis d'accord avec toi, mais… bon: je ne vois pas le rapport avec la performance, qui est l'un des point principaux de l'article que l'on commente.
Bon, j'ai quand même vaguement compris que tu défends l'idée que les interfaces c'est bien pour la compréhension. Je suis d'accord avec ça.
Par contre, ton commentaire ne parle en rien de performance, je vais donc me permettre de compléter avec mon opinion:
Moi, ça me paraît évident, en fait, et je pense même qu'avec des interfaces suffisamment claires, on récupère de la performance tout simplement parce qu'on va éviter de faire plusieurs fois la même opération pour rien, naturellement.
Autrement dit, je pense que d'avoir des algo super performants dans chaque fonction, ben, je m'en fout. De toute façon moi, je suis nul en algo.
Je préfère de loin me concentrer sur l'architecture: ça permets de faire la maintenance plus facilement, et si besoin est, de remplacer, justement, les algo par d'autres mieux adaptés en fonction du contexte.
Dans son exemple, si on a besoin plusieurs fois de la surface d'un carré, on va la calculer a chaque fois. Bonjour la performance… alors qu'avec des classes, aussi pourries soient-elles dans l'exemple qu'il a effectivement piqué à un «défenseur de la POO» (la POO a des soucis a se faire avec ce type de défenseurs…) il serait possible à posteriori de modifier les classes pour ne faire le calcul que lors des modifications des côtés du carrés, rectangle, … et ce, sans devoir passer au peigne fin quelques dizaines de milliers de lignes de code. Pour les petits projets. On parle bien vite de centaines de milliers, après tout…
[^] # Re: l'héritage et les exemples pourris
Posté par barmic 🦦 . Évalué à 2.
Ça vaut le coup de vérifier avant de se lancer dans une memoïsation qui selon le contexte peut amener plus de bug que de gain :)
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: l'héritage et les exemples pourris
Posté par arnaudus . Évalué à 3.
C'était ma compréhension naïve de la POO, mais le contact avec la STL de C++ a mis mes certitudes à rude épreuve. Par exemple, conceptuellement, ça m'a beaucoup gêné de ne pas avoir accès à des fonctions inefficaces pour certains containers. Par exemple, vector::push_front(). C'est une opération inefficace sur un vecteur, certes, mais ça empêche l'abstraction (en particulier, ça rend le benchmarking très difficile, alors qu'il y a des algos pour lesquels les performances relatives de différents containers mériterait d'être testé empiriquement). En gros, avec les containers de la STL, tu es obligé de savoir comment ils sont implémentés, parce que tu n'as accès qu'à une liste restreinte de méthodes en fonction de l'efficacité algorithmique des opérations. Est-ce qu'il n'y a pas là des principes qui se contredisent, et que les concepteurs de la STL ont choisi de trancher dans un sens sans laisser l'utilisateur décider?
[^] # Re: l'héritage et les exemples pourris
Posté par freem . Évalué à 2.
En fait le cas de la STL est plutôt intéressant je trouve. A l'origine, c'était effectivement "tout dans les méthodes", "parce que les class c'est la classe". Bon, j'exagère peut-être un peu, mais probablement pas tant que ça. Voir par exemple
std::string
qui est selon moi une très mauvaise interface.Hors, de nos jours l'idée est plutôt d'utiliser au maximum
#include <algorithm>
qui par exemple fournitstd::back_inserter
, et qui devrais être ce que tu veux. Evidemment, ça n'a pas de sense avec un std::set ou std::map, puisque c'est trié… mais bon, ça permets effectivement de pouvoir remplacer aisément un conteneur par un autre, tant que l'opération est légitime pour le nouveau. Cela dis, je trouve la syntaxe "un peu" lourde, c'est surtout utile quand on écrit des template pour moi.[^] # Re: l'héritage et les exemples pourris
Posté par pulkomandy (site web personnel, Mastodon) . Évalué à 3.
On peut utiliser autre chose qu'UML, par exemple le langage SDL: https://en.wikipedia.org/wiki/Specification_and_Description_Language est plus adapté pour des processus qui s'échangent et réagissent à des messages.
[^] # Re: l'héritage et les exemples pourris
Posté par kantien . Évalué à 4.
Ça ne sert effectivement à rien. Un jour j'ai demandé à quelqu'un de m'expliquer l'UML, au bout de dix minutes je lui ai dit d'arrêter : je n'avais rien à apprendre d'une personne qui ne sait pas structurer logiquement sa pensée de manière adéquate. En revanche j'use abondamment de diagrammes : ceux de la théorie des catégories. Bon après, j'ai une gros rejet de la POO, non du concept d'objet de la POO, mais du paradigme dans lequel tout est objet (à la Java). C'est pour moi un paradigme d'asile d'aliéné, ou l'art de faire de l'algèbre en marchant sur la tête. Je m'explique en reprenant le cas d'étude de l'article.
Qu'un carré, un rectangle ou un cercle soient des formes, il n'y a là aucun doute. Mais cette propriété n'a rien à foutre dans leur définition ! Ce qui est inévitablement le cas si on les définit comme des objets. Personnellement je les définit ainsi :
Ce qui définit chacun des types, ce sont les enregistrements et la propriété qu'ils ont d'avoir une surface se traduit par l'existence d'une fonction
area
sur chacun de ces types. Après le concept d'un objet, au sens de la POO, c'est le type suprême de toutes les formes, celui dans lequel on peut injecter n'importe quel forme. Ce que ne semble pas avoir compris l'auteur de l'article du lien, puisqu'il ne considère que le type somme de quatre formes possibles, alors qu'il y a aussi n'importe quel polygone, des ellipses…Et voilà comment on définit, algébriquement, un tel type :
Je n'ai donné que l'interface (ou API) d'un module qui définit ce genre suprême, sans préciser les détails d'implémentation ("Code should not know about the internals of objects it’s working with" ;-). Pour l'implémentation du type
Shape.t
je peux prendre aux choix :et la liste n'est pas exhaustive.
Et c'est là qu'interviennent les diagrammes de la théorie des catégories. Une forme est caractérisée par l'existence d'une fonction
area
sur son type. Algébriquement cela se traduit par l'étude des structures algébriques ayant la signatureShape.S
:On peut faire un graphe orienté de toutes ces structures : chaque nœud du graphe est un module satisfaisant cette signature, et chaque arête est une fonction qui transforme un nœud en un autre (par exemple, la proposition tous les carrés sont des rectangles est représentée par une telle arrête). Mais dans ce graphe (qui, en soit, contient une infinité de nœuds), il y a un nœud particulier T (ou plutôt une famille de nœud). Ce dernier à la propriété que pour tout autre nœud N du graphe il existe une et une seule arrête qui va de N à T. Ce nœud T est le type
Top
de toutes ces structures algébriques, celui qui les contient toutes (les carrés, les rectangles, les cercles…). En réalité, il y a une infinité de tels nœuds Top, mais ils sont tous équivalents et ne diffèrent que par leurs détails d'implémentation (j'en ai donné 3 possibles au-dessus). La fonction centrale de cette structure c'est de faire du dynamic dispacth ou du polymorphisme ad-hoc : il applique la fonctionarea
adéquate à la forme qu'il contient. Mais la stratégie à base d'objets et de vtable n'est qu'une solution parmi d'autre pour faire cela. On trouve par exemple, dans des langages existant :Mais pour définir une fonction sur une liste de formes, ou sur une forme quelconque, c'est-à-dire pour faire du polymorphisme ad-hoc, il n'est absolument pas nécessaire de les injecter dans le type
Top
(qui est le type des objets de la POO), et c'est ce que je reproche à la POO : elle réduit la catégorie à son seul objet terminal (ce typeTop
), ce qui est une manière plus que bancal de faire de l'algèbre. D'autant que de nombreuses catégories de structures algébriques n'ont pas d'objet terminal, à commencer par toutes celles qui ont des opérateurs binaires (addition, multiplication), où il devient alors vraiment ridicule de suivre le paradigme du tout objet.Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
# Échange avec Uncle Bob
Posté par Julien Jorge (site web personnel) . Évalué à 7.
Ça vaut le coup de lire aussi la discussion qu'il a eu avec Robert C. Martin, l'auteur de Clean Code, le livre qui a posé les bases de ce qu'on appelle du code "clean" : https://github.com/unclebob/cmuratori-discussion/blob/main/cleancodeqa.md
[^] # Re: Échange avec Uncle Bob
Posté par arnaudus . Évalué à 10.
Lecture intéressante. Je trouve qu'il y a beaucoup de mauvaise foi chez ce Casey; en fait, il a une idée fixe, et il essaye de torturer les réponses pour faire rentrer son idée fixe. La discussion est pleine de sous-entendus fallacieux, ç'en est perturbant. Par exemple, Casey essaye de systématiquement donner des exemples de logiciels dont le manque de réactivité est problématique (Microsoft Word, des IDE, etc), comme si ce manque de réactivité était une conséquence directe du fait qu'ils étaient codés selon les principes du Clean Code.
Sur le fond, je trouve que Martin est beaucoup plus convainquant. Les bloatwares sont lents parce qu'ils sont mal conçus, et qu'ils seraient probablement mieux conçus s'ils suivaient le principe du Clean Code. Le coût des appels de fonctions et des tables virtuelles ne peut qu'être totalement négligeable pour de tels logiciels, leur lenteur ne peux être qu'un problème de conception et/ou de ressources externes (biliothèques graphiques?).
[^] # Re: Échange avec Uncle Bob
Posté par nico4nicolas . Évalué à 4.
Je partage cet intérêt pour la lecture et je partage également l'opinion sur les deux intervenants. Ce que je retiens surtout c'est que les besoins de très grandes performances sont limitées à certains logiciels et certaines parties de ces logiciels. Un autre argument qui est avancé est :
Cela pourrait se traduire par : "si tu souhaites améliorer les performances de ton code, regarde tes algorithmes avant de regarder ton architecture".
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 2. Dernière modification le 10 mars 2023 à 15:58.
Pourquoi? Qu'est-ce qui empêcherait ce coût d'être élevé en cas d'abus d'héritage (allez, disons qu'il y a 10 niveaux d'héritages, 5 fonctions virtuelles à chaque fois, et que chaque couche a plus de classes que sa couche parente…), de fonctions virtuelles, de
dynamic_cast
?Je suis d'accord que le polymorphisme n'est pas à jeter, loin de la, mais de la a dire que ça ne peut avoir qu'un coût négligeable en toute situation, il y a un pas que je ne franchirai pas.
Ca me rappelle le problème des libs qui exportent trop de symboles: ça rend l'OS lent à faire la résolution de nom, augmentant drastiquement le temps de démarrage des applications. Il y avait un article d'un type de RedHat sur ce sujet, je dois avoir ça chez moi dans mes favoris, je vais essayer de le retrouver ce week-end… enfin, si je n'oublie pas d'ici la :D
[^] # Re: Échange avec Uncle Bob
Posté par arnaudus . Évalué à 6.
Je suis d'accord que ça n'est pas totalement impossible. Mais on parle de nanosecondes. Donc oui, on va peut-être perdre des millions de nanosecondes. Il n'empêche que ça reste imperceptible, parce que le temps d'exécution, en réalité, c'est des cache miss, des accès disques, des appels système, etc. C'est assez perturbant de voir à quel point les processeurs sont rapides, en fait.
Ça me rappelle une discussion avec notre ingé système, qui n'était pas content quand on lançait 300 exécutables sur un serveur de 100 cœurs. Ça fait péter le load average (et ça fait clignoter ses alarmes), et en théorie les context switch sont coûteux. Et en effet, ça fait des dizaines de milliers de context switch… qui font perdre quelques dizièmes de microsecondes chacun. Donc au final, moins d'une seconde sur des jobs qui tournent sur 3 jours. C'est toujours une histoire d'ordre de grandeur, faire des millions de fois des choses inutiles qui durent 10 milliardièmes de secondes, c'est complètement instantané. Par contre, faire un milliard de fois quelque chose qui prend une microseconde, ça fait planter la machine.
[^] # Re: Échange avec Uncle Bob
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 4.
C'est très vrai, voici un article pour se rendre compte d'à quel point ça l'est : https://zestedesavoir.com/billets/2909/le-microprocesseur-ce-monstre-de-puissance-qui-passe-son-temps-a-attendre/
La connaissance libre : https://zestedesavoir.com
[^] # Re: Échange avec Uncle Bob
Posté par Julien Jorge (site web personnel) . Évalué à 4.
C'est intéressant que tu lui trouves de la mauvaise foi, je trouve au contraire qu'il est de bonne volonté :) Il me semble bien documenté, a clairement lu le livre, et il a regardé les présentations d'Uncle Bob. Il y a beaucoup d'aller-retours au début de la discussion avec des listes de logiciels mais non pas parce qu'ils sont lents, plutôt pour s'assurer que les deux parlent du même type de logiciels. La question étant : qu'est-ce qui a besoin d'un niveau de performance à la nanoseconde, a la milliseconde, ou plus. J'aime beaucoup l'approche d'Uncle Bob aussi, qui distingue le niveau de perf en fonction de la fonctionnalité (i.e. la fenêtre de préférences de Chrome n'a pas besoin d'une réactivité folle, mais le rendu d'une page web doit être le plus efficace possible).
Je ne sens pas de fort désaccord entre les deux. L'un comme l'autre considère que la perf est importante, tant d'un point de vue algorithmique que d'implémentation, et j'en ressors plutôt que l'intention d'Uncle Bob avec Clean Code était plutôt d'améliorer la clarté des parties non impactantes sur les perfs, pour les programmeurs de tous les jours. C'est un peu dommage que ça ait été interprété comme la méthode de développement à appliquer par défaut.
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 3.
C'est bien plus philosophique que ça. Y a t'il une qualité par défaut qu'un logiciel doit avoir ? Et si oui le quel est-ce ?
Certains diront qu'un logiciel doit être le plus rapide possible sauf cas particulier.
D'autres diront qu'il doit être le plus lisible possible sauf cas particulier.
Et on parle là que de 2 qualités, mais il y en a pleins d'autres. La subjectivité de chacun donnera des réponses différentes.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 3.
Oui.
Avoir le moins de bugs possible.
Le reste, la lisibilité du code, la vitesse, c'est accessoire. La priorité, c'est que le logiciel fasse sans erreurs ce qu'il a été conçu pour faire, et c'est un équilibre à trouver, toujours.
Les diverses optimisations de performance:
ainsi le coût de maintenance, ce sont des points importants, certes, mais si le programme maintenable; super rapide, mais qu'il ne fait pas ce qu'il devrais, je pense que l'utilisateur va juste le mettre très vite à la poubelle.
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 2. Dernière modification le 15 mars 2023 à 08:15.
Il fallait lire "et le reste, c'est un équilibre à trouver, toujours"
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 3.
Très bien mais du coup ça veut dire quoi ? Il faut passer par de la preuve logiciel ? Il faut s'assurer d'une couverture MC/DC ? Avec test par mutation ?
Tu as l'impression que c'est ce qu'il se passe ? Que les logiciels utilisés n'ont pas de bugs ?
Toutes les qualités peuvent avoir ce genre de phrases :
Toutes les qualités sont un équilibre entre ce qui est perçu du logiciel, son usage, la subjectivité/appétence/compétences des développeurs, l'investissement dans le logiciel, etc
Personnellement j'ai l'intuition qu'en l'absence de critères permettant de décider, il faut privilégier la lisibilité car elle permet le plus facilement d'améliorer les autres qualités quand on découvrira de quel côté on pèche. Mais c'est un avis. Tout a fait discutable.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 3.
Ca veut dire tendre vers 0. Je ne comprend pas pourquoi je dois expliquer ce que "essayer d'avoir moins de bugs" entends.
La méthode diffère selon la situation, les gens, et leurs compétences.
La preuve de coq est une possibilité (il paraît, je ne jamais vu ça mis en place), mais ce n'est pas la seule. On peut aussi utiliser des outils d'analyse statique, les tests unitaires, la programmation par contrat, … il y a plein de méthodes pour essayer de réduire le nombre de bugs. Aucune à ma connaissance n'est parfaite, mais la méthode joue clairement.
De toute façon, l'absence de bugs est impossible (oui, ça contredit les mathématiciens, qui sont pourtant plus intelligents que moi) avec juste du code: on est toujours exposé aux problèmes "environnementaux": avoir une machine qui plante parce qu'un appareil à proximité émets des ondes magnétiques trop fortes, ça arrive, et tu peux prendre l'algo que tu veux, l'archi que tu veux, la vérification formelle que tu veux, ça ne changera rien.
Bon, évidemment, ce n'est pas le type de bugs le plus fréquent, loin de la et heureusement (parce que c'est la merde a trouver, promis) et ça peut sembler extrême, mais j'ai déjà vu un appel téléphonique faire redémarrer un PC…
Tout à fait, et c'est pour ça que j'ai écrit:
que j'ai du corriger via un nouveau message en:
Que je rejoins globalement, mais je suis quand même intrigué: tu as déjà vu des logiciels dont les utilisateurs ne connaissent pas l'utilité? Après, il est aussi vrai que parfois certains bugs sont mués en fonctionnalités, et vice versa.
En tant qu'utilisateur, je me contrefout que le code de linux soit écrit en C, en Rust ou en brainfuck. Ce que je vois (ou pas), c'est quand ça bug. En tant qu'utilisateur, c'est bien la seule chose que je remarque la plupart du temps :D (et en tant que codeur, je sais que c'est difficile a digérer)
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 2.
Comme je l'ai dis tendre vers 0 ne veut rien dire en soit. Quel effort tu met derrière ce tendre vers 0 ? À moins de penser qu'il y a du sabotage, personne n'ajoute de bug volontairement.
L'effort c'est une autre façon de présenter la même question : sur quoi est-ce qu'il faut mettre de l'effort ? (performance/lisibilité/correction/sécurité/…)
Ce ne sont que des qualité personne ne te dira osef, dire que l'on tend vers le mieux possible c'est facile mais ça ne donne pas de réel indications. Je ne serais pas surpris que les logiciels les plus bugués soient ceux qui sortent avec le moins de bug connus par exemple.
Oui et la seconde partie où je voulais aller. C'est un équilibre qui est arbitraire, subjectif et contextuel. Pour reboucler avec le message au quel je répondais :
Favoriser la clarté quand on ne connaît pas de contraintes particulière, ça n'est pas quelque chose qui paraît déconnant. En particulier pour moi, ça me semble simplifier le refactoring vers autre chose en cas de besoin.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 2.
Ben du coup, on est d'accord j'ai l'impression?
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 2.
Dont les utilisateurs ne connaissent pas l'utilité ? Quel est le rapport avec des bugs ?
Je connais par contre un paquet de logiciels bugués et pourtant très utilisés, apprécié voir vénérés par leurs utilisateurs dans le jeux vidéo par exemple. Sans parler de cas particulier comme Star Citizen, beaucoup ont des bugs juste suffisamment peu pour que le gameplay ne soit pas trop embêté. Tu vend ça 45 à 80 € la licence d'utilisation et les clients sont globalement content.
Et c'est assez fréquent. Bon cette semaine j'ai eu un déréférencement de pointeur null au boot sur la dernière version stable, ce qui n'es pas fréquent par contre un driver du noyau qui marche pas si bien ça existe.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 2.
Il me paraît très difficile de déclarer qu'un comportement est un bug (sauf pour les crash, forcément… encore que?) si on ne connaît pas le rôle du programme.
Bien sûr. Les pilotes graphiques sont par exemple plutôt buggués. Je pense que les pilotes wifi sont aussi assez remarquables de ce point de vue. (être remarquable n'est pas forcément une bonne chose)
[^] # Re: Échange avec Uncle Bob
Posté par arnaudus . Évalué à 5. Dernière modification le 15 mars 2023 à 10:55.
Bah déja, des perfs problématiques peuvent être un bug. Si ton logiciel qui gère l'ESP d'une voiture réagit quand la bagnole est déja dans le fossé, c'est une sorte de bug.
Ensuite, je ne suis même pas d'accord. Il y a des logiciels pour lesquels une certaine catégorie de bugs est largement acceptable. Typiquement, c'est le cas des logiciels de rendu: quand Latex te sort des oversize box et te fait dépasser le texte dans la marge, c'est un bug; quand lilypond to sort un truc bof bof, c'est un bug (le rendu s'améliore d'ailleurs de version en version). En tout cas, ce genre de dysfonctionnement est inhérent à la complexité de certaines tâches, et c'est acceptable d'en avoir un certain nombre par rapport à d'autres critères à privilégier (par exemple, le temps d'exécution, ou la clarté des algorithmes). On pourrait aussi citer l'optimisation par les compilateurs par exemple, où il y a toujours un compromis entre l'efficacité et le temps de compilation. Des heuristiques de pathfinding, qui ne te donnent pas forcément le chemin optimal, mais qui font un compromis avec les perfs. Il semble y avoir une infinité d'exemples où une tâche peut ne pas être parfaitement exécutée et où un tel compromis est acceptable.
Du coup, "minimiser les bugs" me semble être un critère parmi d'autres à optimiser, et le poids respectif de ces critères dépend du contexte.
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 2.
Mais là tu ajoute une contrainte. Ma question initiale ce n'est pas :
mais
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par arnaudus . Évalué à 4.
Est-ce que ça existe seulement, un logiciel qui n'a pas de "contrainte particulière"? Tout logiciel a un contexte d'utilisation, qui pondère l'importance des différents critères: clarté du code, performance RAM, performance CPU, évolutivité, expérience utilisateur, fiabilité, etc. Je ne vois pas ce qui ferait qu'automatiquement, le nombre de bugs ouverts serait le critère à optimiser en priorité. Même, au contraire, quel type de logiciel devrait avoir ce critère prioritaire? Les systèmes embarqués ou les systèmes d'aide à la décision, quand des vies humaines, du matériel qui coûte cher, ou des gros capitaux sont en jeu? Mais sur les logiciels courants (traitement de texte, IDE, navigateur, jeux vidéo, compilateurs…), si les bugs ne nuisent pas à l'utilisation (par exemple s'ils sont facilement contournables ou qu'ils concernent des fonctionnalités avancées dont on peut se passer), il est difficile d'imaginer une raison pour laquelle les ressources devraient être consacrées exclusivement à la correction des bugs.
J'aurais donc tendance à considérer que la priorité aux bugs relève de "contraintes particulières". D'ailleurs, beaucoup des logiciels qu'on utilise sont buggés, sans que les équipes de développement s'en émeuvent particulièrement.
[^] # Re: Échange avec Uncle Bob
Posté par barmic 🦦 . Évalué à 2.
Un POC ? Un logiciel avec très peu ou pas d'utilisateurs (son résultat est utilisé mais de temps en temps) exécuté sur des machines sur dimensionnées (parce que c'est ce qu'on a) ? Ou simplement tu n'es pas en mesure au moment où tu commence à travailler à déterminer quelle contrainte prime.
Pour le reste c'est tout mon point qu'il est difficile de répondre à cette question et donc (pour reboucler avec le contexte) qu'appliquer clean code par défaut relève plus d'un choix subjectif de développeur que d'un manquement problématique car ça peut amener à des performances désastreuses.
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Échange avec Uncle Bob
Posté par freem . Évalué à 2.
Pour moi un logiciel qui ne fait pas son travail dans des temps acceptables, c'est effectivement un bug.
Et je suis d'accord que parfois, il faut choisir entre plusieurs problèmes le plus urgent à corriger, et il arrive que le plus urgent soit la rapidité d'exécution plutôt que la précision du travail.
[^] # Re: Échange avec Uncle Bob
Posté par totof2000 . Évalué à 2. Dernière modification le 24 mars 2023 à 10:19.
Le problème c'est que, mis à part dans les logiciels de type temps-réel, jamais on ne spécifie de durée d'exécution pour une tache à exécuter. Certes il existe des "conventions" qui indiquent par exemple qu'une interface de site web doit répondre en moins de x secondes si on ne veut pas perdre un client, mais mis à part les interfaces de logiciels "commerciaux - c'est à dire conçus pour générer de l'argent" (et encore … il y a pléthore d'apps sur android qui ne respectent pas ces règles), on s'asseoit vite sur ces règles lorsque par exemple il s'agit de développer un progiciel propre à une entreprise (on s'en fout que l'utilisateur doit attendre 10 à 15 secondes entre deux clics, ou que le parcours de l'interface soit contre-productif : la santé mentale des employés passe après les économies "de bout de chanfdelle"). Et la façon de faire de l'agilité en rance n'a pas arrangé ces problèmes.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.