2 points par GN⁺ 2023-11-02 | 1 commentaires | Partager sur WhatsApp
  • Article sur les cinq règles de programmation de Rob Pike en 1989
  • Règle 1 : ne supposez pas où un programme passera l’essentiel de son temps ; les goulets d’étranglement peuvent apparaître de manière inattendue. Évitez les hacks de performance tant qu’un goulot d’étranglement n’a pas été démontré.
  • Règle 2 : mesurez toujours avant d’optimiser pour la vitesse. N’optimisez que si une partie du code a un impact significatif sur le reste.
  • Règle 3 : quand n est petit, les algorithmes complexes sont lents. C’est le cas la plupart du temps. N’utilisez des algorithmes complexes que si n est souvent grand, et même dans ce cas, appliquez d’abord la règle 2.
  • Règle 4 : les algorithmes simples et les structures de données simples sont préférables. Ils sont moins sujets aux bugs que les solutions complexes et plus faciles à implémenter.
  • Règle 5 : la bonne structure de données est décisive en programmation. Si les données sont bien organisées, l’algorithme deviendra évident.
  • Les règles 1 et 2 de Pike reflètent la maxime de Tony Hoare : « l’optimisation prématurée est la racine de tous les maux ».
  • Ken Thompson a reformulé les règles 3 et 4 de Pike ainsi : « dans le doute, utilisez la force brute ».
  • Les règles 3 et 4 mettent en pratique la philosophie de conception KISS (Keep It Simple, Stupid).
  • La règle 5 est cohérente avec une remarque de Fred Brooks dans The Mythical Man-Month, souvent résumée par : « écrivez du code stupide qui utilise des objets intelligents ».

1 commentaires

 
GN⁺ 2023-11-02
Avis de Hacker News
  • Je suis entièrement d’accord avec l’idée que « les données dominent ».
    C’est pourquoi les entretiens LeetCode m’ont toujours semblé étranges. Ils se concentrent généralement sur les algorithmes, alors qu’en réalité, il ne faut souvent pas aborder les choses ainsi dès le départ : les structures de données devraient être davantage au centre.
    Bien sûr, si l’on ne connaît aucun algorithme, on peut ne pas repérer les cas particuliers ou les moments où il faut s’appuyer sur un algorithme précis pour une raison donnée. Malgré tout, les algorithmes peuvent s’enseigner assez rapidement, tandis que sentir quelle structure de données utiliser semble être plus difficile pour beaucoup de gens.

    • D’après mon expérience, je suis d’accord. En entretien, quand on dépasse une simple vérification d’algorithme à la FizzBuzz et qu’on commence directement à parler structures de données, architecture et correspondance avec le domaine, j’ai vu les intervieweurs devenir beaucoup plus respectueux.
      À ce moment-là, l’ambiance change : « ah, on a vraiment un ingénieur senior en face ». Les discussions techniques s’ouvrent davantage, et l’attitude consistant à prouver « qu’on sait coder » diminue.
      À l’inverse, les équipes avec lesquelles il a été le plus difficile de créer de bons changements, d’atteindre des jalons et de collaborer étaient celles où personne ne posait correctement les structures de données et l’architecture du code. On dirait que beaucoup se sont habitués à l’idée que le framework fait tout, et que si ça ne marche pas, un plugin ou un middleware conçu par quelqu’un de plus intelligent réglera le problème.
      Un ingénieur qui évite les structures de données se tire une balle dans le pied : il renonce à l’un des outils les plus utiles, et ses limites finissent par apparaître au quotidien.
    • En aidant mon neveu à préparer des concours de programmation compétitive, j’ai constaté que, dans la plupart des problèmes, une grande partie de la solution consistait à transformer les données en une structure de données appropriée.
      Par exemple, le code peut largement servir à trouver le plus long chemin dans un graphe orienté acyclique (DAG) pondéré, mais l’essentiel était de comprendre que le problème pouvait être représenté comme un DAG pondéré. Si on ne le voit pas, on peut encore résoudre le problème, mais avec une solution beaucoup plus lente et complexe.
    • Les problèmes LeetCode classiques se concentrent en fait eux aussi sur les structures de données. Le candidat doit avoir en tête une liste de structures de données à sortir lorsqu’il fait correspondre le problème et la solution à un motif connu.
      L’intervieweur ne vous dira pas d’emblée qu’il faut utiliser une file de priorité, une matrice d’adjacence ou un trie. Il peut donner un indice si vous bloquez, mais un guidage trop appuyé passe difficilement pour un signal fort à l’embauche.
    • « Montrez-moi le schéma de flux et cachez-moi les tables, et je resterai perdu. Montrez-moi les tables, et je n’aurai pas besoin du schéma de flux : tout deviendra évident de soi-même. »
    • Si l’on doit choisir l’un des deux, il me semble qu’il faut quand même avoir une idée approximative de l’autre. Si l’on ne sait absolument pas comment accéder aux données, il est difficile de savoir quelle structure de données utiliser.
  • À propos de l’affirmation selon laquelle « les algorithmes sophistiqués sont lents quand n est petit, et n est généralement petit », ce que j’ai ressenti dans un projet récent, c’est que les grands n peuvent être bien plus grands qu’on ne l’imagine.
    On a vite fait de se dire « il faut absolument optimiser, il y a 100 000 opérations à faire », mais les ordinateurs sont rapides, et 100 000 multiplications sont généralement si rapides qu’il n’est pas forcément nécessaire d’y réfléchir en profondeur.
    Je ne dis pas qu’il ne faut jamais réfléchir, mais la vitesse délirante du matériel moderne surprend souvent.

    • J’ai du mal à être vraiment d’accord avec ça. Les algorithmes en temps quadratique sont du genre à mordre au moment où on ne s’y attend pas.
      J’ai vu des incidents de production causés par du code devenu quadratique par erreur, et même si 99 % des utilisateurs ont toujours un petit n, certains rencontrent souvent de grands n et se retrouvent avec une application très lente.
      Dans la plupart des cas, je préfère choisir un algorithme meilleur que le temps quadratique, même s’il est un peu plus lent dans le cas courant et un peu plus complexe à implémenter. Les chemins lents fréquents finissent par être optimisés, mais les chemins lents rares passent sous le radar des développeurs ou explosent en production.
      Bien sûr, si l’algorithme est trop complexe, je peux choisir une implémentation quadratique simple, mais par défaut, j’essaie de viser moins que quadratique quand c’est possible. J’ai aussi écrit un article à ce sujet : https://kevincox.ca/2023/05/09/less-than-quadratic/
    • La hiérarchie mémoire joue aussi là-dessus. Beaucoup d’algorithmes sophistiqués ont une mauvaise localité des références et ajoutent des branchements.
      Cela pouvait donc être plus vrai il y a 40 ans, quand les CPU n’étaient pas aussi rapides par rapport à la mémoire et qu’on se souciait moins des échecs de prédiction de branchement sur le matériel grand public.
    • Dans les questions d’entretien LeetCode, je vois sans cesse des parcours répétés de listes de 100 000 éléments. Ce n’est peut-être pas optimal, mais en temps d’exécution réel, parcourir 100 000 éléments n’est rien comparé à l’appel réseau qui vient juste après.
      À chaque entretien, il arrive que le responsable du recrutement le veuille, mais qu’un débutant LeetCode qui n’a pas encore connu les blessures de la production refuse cette décision.
    • La référence emblématique sur ce sujet est Scalability! But at what COST?
      https://www.frankmcsherry.org/assets/COST.pdf
    • Quand j’ai commencé mon premier vrai travail de programmation dans une société de jeux au début des années 2000, le directeur technique m’a conseillé : « si le nombre d’éléments traités est de l’ordre de 10 000, n’optimise pas ».
      Vu les progrès des performances des ordinateurs ces 20 dernières années, relever ce seuil à 100 000 semble assez raisonnable.
  • Le célèbre adage « l’optimisation prématurée est la racine de tous les maux » ne vient pas de Tony Hoare mais en réalité de Donald Knuth, et il est souvent utilisé hors contexte comme un argument contre toute optimisation.
    La phrase complète est : « Nous devrions oublier les petites efficacités, disons dans 97 % des cas. L’optimisation prématurée est la racine de tous les maux. Mais nous ne devons pas laisser passer les occasions dans les 3 % cruciaux. »
    L’idée est d’optimiser en consacrant du temps là où cela a un impact.

    • Knuth dit que c’est de Hoare, et Hoare dit que c’est de Knuth ; c’est donc une question de savoir qui croire. Le mieux est probablement de l’attribuer aux deux.
      Il est possible que Tony l’ait dit en premier et que Knuth l’ait peaufiné puis publié. Il est toujours bon d’inclure la citation longue qui donne le contexte nécessaire.
    • On oublie aussi souvent que cette citation date de la fin des années 1970. Cela fait presque 50 ans.
      À l’époque, la programmation était très différente d’aujourd’hui. L’« optimisation prématurée » d’alors ne signifiait pas « utilisons simplement une bibliothèque populaire et scalable », mais plutôt « utilisons un algorithme de manipulation de bits incompréhensible qui ne fonctionne que sur ce matériel ».
    • Je ne pense pas que la citation longue ajoute un contexte vraiment significatif. Si l’on a mesuré et identifié les 3 % importants, alors ce n’est plus prématuré.
      C’est déjà inclus dans « l’optimisation prématurée est la racine de tous les maux » ; l’adage n’est pas « l’optimisation est la racine de tous les maux ».
    • Trop de gens prennent cette phrase comme un dogme et n’apprennent même pas les méthodes efficaces.
      Lors d’entretiens sur les structures de données et les algorithmes en entreprise, j’ai vu d’innombrables développeurs front-end dire que le tri à bulles était la meilleure option. Il n’est pas nécessaire de tout redériver sur le moment ; connaître quelques options et savoir proposer un bon choix adapté au problème suffit.
      Si l’on pousse « ne pas optimiser prématurément » à un tel extrême qu’on ne connaît même pas les méthodes efficaces, comment savoir où se trouvent les points importants ?
    • Dans ce contexte, cela ne semble pas être un argument contre l’optimisation en général.
  • L’idée que « les structures de données sont essentielles » est doublement importante dans les bases de données.
    Les gens qui utilisent une base de données comme un simple réservoir de bits idiot, ou comme le reflet 1:1 de définitions d’objets, sont souvent surpris lorsque la base le prend personnellement et détruit les performances.
    Si je dois revoir un schéma de base de données généré par un ORM, ce sera probablement une rencontre trop précoce.

    • Je considère que la plupart des ORM génèrent le schéma qu’on leur demande. Utiliser un ORM ne produit pas automatiquement une conception de base de données pire que si elle était faite à la main.
      Le problème est que certains développeurs, voire beaucoup, ne connaissent pas SQL et n’ont pas non plus les connaissances en bases de données nécessaires pour utiliser un ORM.
      Un ORM est une abstraction qui fuit pas mal : il faut savoir ce qu’il y a en dessous. Si on le comprend, on peut produire un schéma correct avec la plupart des ORM.
    • On peut aussi ajouter ici la loi de Conway : « les organisations qui conçoivent des systèmes produisent des conceptions qui reproduisent les structures de communication de ces organisations ».
      Pour bien organiser les structures de données et les maintenir ainsi malgré l’évolution de la conception, il faut séparer les données et le code au niveau organisationnel.
      La conception du schéma de base de données, les cas d’usage et le mapping entre les deux devraient être séparés du reste de l’implémentation, et ce groupe devrait aussi écrire les contrôles d’intégrité, entre autres. Si la structure de l’organisation ne sépare pas les données et le code, il est difficile de séparer le code et les données.
    • Les procédures stockées gagnent.
  • Ma règle supplémentaire est que les petits gaspillages de performance, accumulés, finissent par ralentir un programme même si chacun paraît insignifiant.
    Si cela n’a pas d’impact sur la complexité, la lisibilité, la maintenabilité ou le coût d’implémentation, il ne faut pas simplement jeter la performance. Toutes choses presque égales par ailleurs, choisir l’option la plus lente entre deux solutions n’est pas acceptable.
    Par ailleurs, si l’on suppose que n est petit, presque tout fonctionne. Mais si l’on écrit du code qui marche bien jusqu’à 100 et s’effondre au-delà de 10000, par exemple en O(n²), il faut tout simplement imposer une limite. Si l’hypothèse d’un petit n est violée, il vaut mieux échouer bruyamment qu’avoir une facture AWS explosive ou un programme bloqué.

    • Les règles 1 et 2 s’appliquent ici.
  • Une bonne partie de ces directives revient finalement à des stratégies pour éviter la surconception.
    D’après mon expérience, l’optimisation prématurée est l’un des pièges les plus coûteux. Contourner trop tôt des problèmes potentiels laisse ces hypothèses non vérifiées, et l’équipe suivante doit élaborer des solutions coûteuses pour gérer une complexité inutile.
    L’approche que j’ai apprise est la suivante : l’optimisation repose sur des estimations, et les estimations du début sont souvent fausses.
    J’ai aussi constaté que, pour empêcher les gens de produire du code excessivement complexe, la gestion de l’ego et la compréhension psychologique comptent beaucoup.

    • Je le formule souvent ainsi : « Résolvez le problème que vous avez, pas celui que vous pensez avoir. »
    • Ce concept rejoint aussi l’identification des gaspillages dans le lean et Six Sigma.
      La surproduction est généralement considérée comme le pire des gaspillages, car non seulement elle produit quelque chose d’inutile, mais elle consomme aussi des efforts qui auraient pu être consacrés à ce qui était réellement nécessaire. La surconception est similaire.
    • En allant un niveau plus loin, la surconception vient de l’idée qu’une complexité pourrait être nécessaire plus tard, et qu’étendre le système à ce moment-là serait plus difficile ou plus risqué.
      Par exemple, même avec seulement 100 utilisateurs, on commence avec une architecture microservices parce qu’un jour, s’il y en a un million, il sera difficile de reconcevoir le monolithe.
      Il faut donc d’abord s’attaquer à la question de savoir pourquoi le code devient moins malléable avec le temps.
    • Pour la gestion des erreurs, mieux vaut ne pas faire le malin et échouer tôt et simplement.
  • Ce sont globalement de bonnes règles, mais dans la pratique, la règle 1 ne tient pas telle quelle
    Au départ, il faut une hypothèse sur ce qui deviendra le goulot d’étranglement. Il n’est pas toujours possible de se dire qu’on implémentera XYZ, puis qu’on mesurera ce qui est lent et qu’on le corrigera. X, Y et Z sont parfois liés, au point qu’il faut concevoir X et Z d’une certaine manière pour rendre Y rapide, et il arrive qu’on sache déjà que Y sera le goulot d’étranglement
    Même si, plus tard, les mesures permettent de savoir ce qui est lent, il faut parier sur une approche pour le rendre plus rapide. Plus ce pari est éclairé, mieux c’est
    Les bons programmeurs mesurent, mais ils savent aussi prédire ce qui sera lent, bogué et gourmand en mémoire, ce qui leur évite des itérations. Présenter comme une règle l’idée qu’on ne peut pas prédire le comportement en matière de performances revient à ignorer l’expérience et le savoir-faire accumulés par les bons programmeurs

    • La règle 1 est une règle absolue pour ceux qui n’y croient pas, et une ligne directrice souple pour ceux qui y croient
      Car c’est précisément en appliquant la règle 1 qu’on acquiert l’expérience et le bagage empirique nécessaires à une bonne intuition pour anticiper les goulots d’étranglement
    • Un algorithme qui semble lent peut être vérifié avec une implémentation spike. En général, les algorithmes lents sont simples à implémenter et à tester
      Si la prédiction de performance est fausse, on se retrouve à traîner du code inutilement complexe pendant toute la durée de vie du projet
      Les gens se trompent souvent sur la vitesse des algorithmes. Si l’ordinateur passe 99 % de son temps à récupérer n depuis le serveur de base de données, O(n) et O(n²) peuvent souvent sembler équivalents en temps réel
      Un algorithme écrit en C peut aussi être plus lent que le code Python équivalent, parce que le compilateur de bytecode a pu faire quelque chose d’intelligent
      J’ai beaucoup travaillé à rendre du code legacy plus rapide, et c’est généralement bien plus facile qu’on ne l’imagine, avec des lenteurs dues à des raisons qui n’étaient pas évidentes pour l’auteur initial. En pratique, le code est souvent lent parce que la base de code est devenue trop complexe pour que son auteur d’origine puisse encore raisonner dessus. De mon côté, j’ai un exemple concret de quelque chose de « trop lent », donc il est facile de l’exécuter, d’observer où ça ralentit et de le déboguer
    • Cela ne semble pas vraiment répondre à l’ensemble du texte original. Celui-ci dit de ne pas introduire de hacks de performance tant qu’on ne connaît pas le goulot d’étranglement. Ce n’est pas la même situation que celle décrite
      Si vous développez un jeu vidéo avec beaucoup d’objets physiques et que, par expérience, vous savez avec certitude que la détection des collisions sera un gros problème, concevoir le jeu et le système autour de cela n’est pas un hack de performance
      Si vous savez que la performance sera une préoccupation majeure pour une tâche, il faut évidemment mesurer. Il ne s’agit pas de vérifier si c’est une préoccupation, mais de vérifier à quel point vous la traitez bien
    • J’aimerais voir un exemple concret. Dans la plupart des cas, je doute que cela fasse réellement une différence
      Si l’on crée un nouveau système pour de nouvelles exigences, il me semble que, souvent, on peut simplement commencer. Construire, tester et mesurer, jeter ou refactorer, puis recommencer
      Prenons Rust comme exemple : il a commencé avec un langage de brouillon et un compilateur écrit en OCaml, puis a itéré. Même si l’on savait qu’un jour il serait possible de passer d’OCaml à l’auto-hébergement, je ne suis pas sûr que cela aurait changé grand-chose
    • Si les développeurs pouvaient aussi bien prédire ce qui sera lent, le taux de réussite des startups pilotées par des développeurs ne devrait-il pas être de 100 % ?
      S’il n’y a pas d’utilisateurs, même une fonction qui prend plusieurs heures reste suffisamment rapide par rapport à une fonction qui passerait à quelques millisecondes une fois optimisée. Je ne sais pas vraiment si quelqu’un a prouvé qu’il pouvait faire ce genre de prédiction avec précision
  • En objection à la règle 5, des algorithmes complexes appliqués à des données simples peuvent apporter de gros gains de performance, supprimer des obstacles et même simplifier les choses
    Par exemple, si l’on utilise une recherche binaire dans un tableau trié plutôt qu’un objet BinaryTree, la fusion devient plus simple — concaténation puis tri —, l’absence de pointeurs facilite la sérialisation et, dans certains cas, rend la sérialisation elle-même inutile. Le tableau peut résider sur disque, en mémoire, ou dans les deux via mmap ; il peut gérer des données plus grandes que la RAM ; il permet aussi un cold start où l’on pointe simplement vers un fichier ou un mapping et l’on exécute directement. Il a aussi des propriétés cache-oblivious
    Le codage de Huffman en est un autre exemple. À l’université, on l’a généralement appris comme un algorithme à base d’arbre en complexité O(n log n), mais j’ignorais qu’il existait une méthode pour construire un arbre de Huffman en temps linéaire, en place, à partir d’un tableau
    Bien sûr, 99 % du temps, on construit des microservices backend avec les structures de données des collections standard. Mais si je dois traiter du big data au travail, je préfère de très loin le faire sur une seule machine locale équipée de gros disques plutôt que d’adopter la famille MapReduce à la mode du moment

    • Je ne vois pas la recherche binaire comme un algorithme élégant. Ce sont les fonctions de tri modernes qui le sont, et elles peuvent contenir des bugs subtils, donc un développeur lambda ne devrait pas les réécrire lui-même. Même le quicksort a ses pièges
      Rob Pike aurait sans doute commencé par profiler le code, puis regardé si le code élégant ou la structure de données alternative était effectivement plus rapide
    • Je ne vois pas cela comme une objection. Du point de vue du conseil de Pike, « recherche binaire dans un tableau trié » et « objet BinaryTree » ne sont que deux implémentations différentes de la même structure de données
    • Il ne faut pas oublier que, 99 % du temps, la ressource la plus chère, ce sont les développeurs. La maintenabilité et la vitesse de mise sur le marché sont généralement bien plus importantes
  • J’ai lu ce texte pour la première fois sur cat-v il y a plus de dix ans, et il a eu une influence indélébile sur ma façon d’aborder et de penser la conception et la complexité
    http://doc.cat-v.org/bell_labs/pikestyle

  • Je ne vois pas comment on passe de la règle originale « les structures de données sont au cœur du sujet » à « écrivez du code stupide qui utilise des objets intelligents »
    L’expression « smart objects » était vraiment mauvaise, et la règle originale est bien meilleure, même si elle est plus longue

    • Rob Pike serait probablement d’accord pour dire que les « smart objects » sont une mauvaise façon de penser : https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
    • Il est plus facile de comprendre, tester et modifier la logique « intelligente » quand on la remonte à un niveau supérieur. À mon avis, il est beaucoup plus difficile de rendre des smart objects cohérents entre eux
    • On peut comprendre cela comme : écrivez du code qui découle naturellement d’objets bien structurés