EDIT 2021-02-26 : pour diverses raisons expliquées plus bas, mes notes seront dorénavant prise sur ce repo github, et la présente page ne sera plus alimentée.

Liste des liens

Pourquoi cette page

Je croise souvent des articles/vidéos/posts/etc. dont j’ai envie de garder quelque chose. Jusqu’ici, ma façon canonique de le faire était d’en "extraire" la connaissance et de trouver un endroit dans mes notes où l’ajouter.

Par exemple, après avoir regardé une vidéo sur les problèmes P et NP-difficiles, je vais créer un fichier notes/algorithmic_complexity.otl, et y mettre ce que j’en aurais compris, reformulé et réorganisé à ma sauce, éventuellement en ajoutant le lien vers la vidéo en référence. Si par la suite j’enrichis ma connaissance du sujet, alors je complèterai ou corrigerai ce fichier.

Avantage : j’ai des notes structurées sur les sujets que j’ai étudiés. L’outil que j’utilise, vim-outliner, produit un format texte facile à grepper, que je trouve particulièrement lisible. Le process de structuration et reformulation de l’information est très formateur puisqu’il me permet d’identifier les points mal compris.

Inconvénient : produires ces notes "raffinées" (dans le sens de "traitées pour en extraire l’essentiel") et structurées demande beaucoup de temps. Par ailleurs, je n’y mets que de l’info "académique", rarement partielle ou brute, et jamais ce qui est de l’ordre de l’opinion. De façon plus anecdotique, le format otl est parfois limitant, et elles sont compliquées à partager.

Je veux tester sur cette page une autre façon de prendre des notes, probablement complémentaire du process que je suivais jusqu’ici : j’empile en vrac les liens vers les références intéressantes, assorties de quelques notes assez brutes.

Ce que j’en attends

  • déjà, ça me permet de les partager plus simplement que l’arborescence de répertoires qu’est devenu mon projet notes.

  • mais surtout, le côté déstructuré, associé au fait que les notes peuvent être minimales (quelques point à retenir), voire inexistantes (la simple présence du lien étant le signe que j’ai eu envie de pérenniser l’article) rendront mon process certes moins poussé, mais plus simple : j’espère avoir moins d’inertie à jeter quelques notes sur des articles qui m’ont intéressés.

  • pour autant, il me reste possible de prendre des notes un peu plus touffues (e.g. ici), que je pourrais facilement convertir en notes structurées otl, ou en post.

  • même si la source reste textuelle (asciidoctor), le rendu HTML me permet des choses impossibles en textuel simple, à commencer par le formatage du code.

La règle d’or que j’essaye (avec succès jusqu’ici, yay) de suivre est de n’inclure que des références consultées et terminées : pas de "à lire" ou "à finir de visionner".

Pour le moment, ça m’arrange plutôt d’avoir toutes les notes sur la même page web. Lorsque/si la page devient inutilisable du fait de sa taille, j’envisagerai d’autres pistes (pagination ? shaarli ?).

EDIT 2021-02-26 : abandon de cette page au profit d’un repo github de notes

Après environ un an d’utilisation, mon besoin de notes déstructurées reste plus que jamais d’actualité, mais je change mon workflow, et je vais plutôt les écrire dans ce repo github.

Même si la présente page m’a permis d’affiner mon besoin, à l’usage, comme je prends mes notes d’abord au format de vim-outliner, je retranscris naturellement ces notes sous forme de listes à puces :

  • c’est beaucoup moins lisible et pratique que le format de vim-outliner

  • la liste à puce n’est pas forcément la meilleure façon de rendre mes notes (parfois, des titres et un sommaires sont plus intéressants)

  • je n’ai jamais pris le temps de la scripter, du coup la conversion du format vim-outliner en liste à puces asciidoctor est casse-bonbons

Par ailleurs, je suis obligé de publier le blog pour publier ces notes, et rassembler toutes les notes sur une seule page est certes greppable, mais ne scale pas très bien (c’était attendu).

A contrario, un repo github de notes fluidifiera la prise de notes — même si j’aime moins rédiger des notes en markdown qu’en vim-outliner :

  • le viewer github jouera le rôle d’interface de consultation

  • la publication sera (un peu) plus fluide puisque je n’aurai pas d’étape supplémenter pour publier : commiter/pusher sera suffisant

  • chaque note étant issue de la lecture d’un post (ou du visionnage d’une vidéo), il est plus logique de faire un fichier par prise de notes

De plus, il me permet via les permalinks de créer facilement un lien vers une ligne particulière de mes notes ; cette fonctionnalité a beaucoup de valeur pour moi, car j’essaye de croiser mes notes le plus possible.

Cerise sur le gâteau, il facilite le traitement automatique des notes, comme le mirroring des notes sur ce site, si je souhaite tout consulter à un seul endroit.

À court terme, je vais me contenter de prendre mes futures notes dans le repo github. À moyen terme, j’y copierai également les notes actuelles de la présente page, qui sera archivée ou supprimée. À long terme, j’y mettrai peut-être mes autres notes : notes structurées mentionnées plus haut (actuellement dans des fichiers privés au format vim-outliner), et notes sur des papiers scientifiques (actuellement dans leur propre repo ou dans des fichiers privés).

[ARTICLE] How to hire senior developers: Give them more autonomy

Lu le 2021-02-??, publié le ????-??-?? (pas clair, mais après 2018) sur un site destiné à promouvoir le travail autour du recrutement de Alexander von Franqué.

  • TL;DR = un intéressant point de vue sur la valeur de l’entreprise, qui est plutôt dans ses développeurs que dans son code-source.

  • Contrairement à ce que le titre laisse penser, l’article ne concerne que de loin le recrutement (juste la fin de l’article).

  • La valeur de l’entreprise n’est pas dans le code-source, qui est très vite dépassé, et dur à prendre en main.

  • La valeur de l’entreprise est plutôt dans la connaissance qu’ont les développeurs, et notamment ce qu’il appelle les intangible aspects of software :

    • how the code maps to the real world

    • why the code maps to the real world

    • the programmer can respond to a change request

  • Je reconnais plusieurs de mes points de vue dans l’article :

    • lien avec l’importance que j’accorde au fait de faciliter l’onboarding

    • lien avec mon goût pour tout ce qui facilite le fait de se construire simplement un modèle mental (accès simple aux données, outils ergonomiques, etc.).

  • Je ne suis pas d’accord avec le fait que ce qu’il appelle les intangible aspects of software ne peuvent pas être communiqués, ou le fait qu’il soit intuitif (plutôt qu’argumenté) : les architecture decision records peuvent aider dans ce domaine.

  • Il avance que des interactions entre devs (notamment nouveaux devs et devs expérimentés) sont indispensables pour transmettre ces intangibles aspects.

  • La thèse au coeur de l’article :

    The main value of a software company is the mapping of source code and problem space in the developer’s heads.

  • Je trouve la note de base de page suivante qui vient compléter cette thèse particulièrement pertinente :

    However, I believe this statement is only true for software companies where the core technology is the main asset.
    For many software companies, the main value might not lie in their technology, but in other things, such as the network effects, relationships to customers, etc.

  • Les 3 conseils à l’embauche sont intéressants :

    • Teaching is an undervalued skill, especially in senior developers

    • Retaining talent is even more important than you might think.

    • The Theory Building View also gives us insights on how to structure onboarding processes. It helps to explain why new developers often takes a very long time until they reach full productivity

[ARTICLE] Value semantics

Lu le 2021-01-??, publié le 2012-02-03, sur le blog d’Andrzej Krzemieński, un dev C++ polonais qui a l’air bien calé

  • TL;DR = une intéressante présentation de la value-semantics (un bon complément à la page de Patrice ROY sur la Sainte-Trinité)

  • value = "contenu" de l’objet

  • object = en value-semantics, simpel véhicule de la value

  • value semantics = on s’intéresse surtout à la value, plutôt qu’à son véhicule

    • par exemple : en value-semantics, si on ne travaille pas sur l’objet lui-même mais sur une copie qui a la même valeur, ça n’est pas grave

  • reference semantics = on s’intéresse à l’objet lui-même

    • par exemple : si on veut trier un tableau in-place, on veut trier le tableau lui-même (on veut le muter), et non pas une copie qui a le même contenu

  • En C++, la value-semantics est ce qu’on a par défaut : il faut faire du travail en plus (ajouter des caractères & ou *) pour avoir la reference semantics

  • Intérêts :

    • plus simple, car c’est ce qu’on a par défaut

    • plus proche du concept mathématique de fonction (functional programming FTW)

    • pas de memory-management à faire, car les objets ont un storage duration automatique

    • pas de problème de reference aliasing. Chaque thread a sa propre copie → pas de data-race.

    • referential transparency (TL;DR : on peut remplacer l’invocation d’une fonction par son résultat, ce qui n’est pas le cas si la fonction mute ses paramètres, car elle a alors des side-effects)

  • Pourquoi ne pas l’utiliser :

    • pour que plusieurs entités modifient le même objet, ils doivent manipuler une référence (e.g. std::cout, qui est global, et qui peut être utilisé à plusieurs endroits du code)

    • pour profiter de fonctionnalités nécessitant des pointeurs ou des références (e.g. polymorphisme)

    • pour gagner en perf en évitant de recopier de gros objets

  • Dans ce dernier cas, attention à bien mesurer, car le passage par valeur n’est pas nécessairement aussi coûteux qu’on pourrait le penser (le C++ a plusieurs mécanismes pour éviter les copies).

  • Note : les pointeurs ont beau être passés par valeur (le pointeur lui-même est copié), il est considéré comme reference semantic car il sert à référencer un objet.

[VIDEO] Back to Basics: The Abstract Machine - Bob Steagall - CppCon 2020

Vue le 2021-02-24, publiée le 2020-09-22 sur la chaîne de la CppCon, par Bob STEAGALL, membre du comité C++

  • TL;DR : talk sur les similitudes et différences entre la différence entre abstract machine (dans le modèle mental du développeur, c’est ce qui fait tourner le code qu’on écrit) et physical machine (qui exécute réellement le programme).

  • 03:00 définition d’une abstract machine : processor (instruction set, register set, memory model) conçu non pas pour être implémenté physiquement, mais pour supporter l’exécution d’un langage intermédiaire

  • 03:20 C++ abstract machine = portable abstraction of your os, kernel and hardware

  • 04:20 variety of computing Platforms :

    • général purpose cpu

    • GPU

    • embedded processors

  • 05:00 quand on écrit des programmes, on ne veut pas écrire pour un hardware particulier

  • 08:00 there is no room for another langage between C++ and the hardware : C++ se mappe bien sur le hardware (e.g. les types fondamentaux se mappent naturellement sur des entités mémoire hardware)

  • 08:40 : C++ defines how programs work in terms of an abstract machine. Cette abstract machine est délibérément proche du hardware

  • 09:00 our programs describe operations performed on the abstract machine

    • C’est le point principal du talk : When we write C++ code, we are writing to the C++ abstract machine

    • En résumé, le dev est en charge d’écrire du code pour l’abstract machine, le compilo est en charge d’écrire du code pour la physical machine (fort heureusement nommé code machine)

    • L’observable behaviour de l’abstract machine et de la physical machine sera identique.

  • 13:30 implementation-defined behaviour = marge laissée à l’implémentation (le compilo) sur son comportement. Elle doit être documentée. Certains aspects de l’abstract machine sont non specifies, et non déterministes

  • 16:00 définition de observable behaviour

  • ~23:00 implementation defined (= au choix de l’implémentation, mais déterministe et documenté, e.g. la taille d’un pointeur = sizeof(void*)) et unspecified (non-deterministe,p.ex. l’ordre d’évaluation des arguments d’une fonction)

  • 28:00 undefined behaviour. S’il y en a dans le programme, le programme ne sert plus à rien car il peut faire absolument n’importe quoi.

  • 29:00 ill-formed = l’implémentation doit avertir l’utilisateur. (il existe aussi une catégorie "no diagnostic required")

  • 31:30 structure de l’abstract machine :

    • Memory

    • Objects

    • Threads

  • 32:00 pour l’abstract machine, la mémoire est flat et homogène

  • 34:00 objects : size, alignement, storage duration (automatic/static/threadlocal), lifetime, type, value (possiblement indéterminé), name. At most ONE memory location.

  • ??:?? As-if rule : le compilo peut faire ce qu’il veut, si ça ne modifie pas le comportement observable du programme

  • 38:30 intéressant exemple concret d’arithmétique des pointeurs autorisée ou non par l’abstract machine : autorisé dans un tableau, mais pas en dehors.

  • 43:45 threads. Chaque thread a une toplevel fonction (celle du thread principal est main)

  • 52:40 le truc à retenir :

    When we write C++ code, we are writing to the C++ abstract machine

  • (Et c’est l’implémentation qui traduit les opérations effectuées par l’abstract machine en des opérations effectuables par la physical machine)

[VIDEO] CppCon 2017: Fedor Pikus “C++ atomics, from basic to advanced. What do they really do?”

Vue le 2021-02-23, publiée le 2017-10-10 sur la chaîne de la CppCon, par Fedor PIKUS, ancien dev Google, actuellement chez Mentor.

  • 04:30 définition d’un atomic = les autres threads ne voient pas d’état intermédiaire.

    • C’est un concept plus général que les atomic du C++ : c’est le A de Acid, par exemple (alors même qu’une transaction peut concerner des milliers d’istructions)

  • 10:45 tous les types trivialement copiables (i.e. copiables avec memcpy, continuons chunk of memory, no virtual fonctions, noexcept constructor) peuvent être atomiques.

  • 11:00 donne les opérations utilisables avec les atomic.

  • 13:00 certaines opérations compilent mais ne sont pas atomiques !

  • 15:40 opérations applicables aux atomic : load, store, exchange, compare_and_swap

  • 16:10 je commence à mieux comprendre compare exchange, qui semble être conçu pour être utilisé dans une boucle pour "setter" l’atomic de façon conditionnelle, s’il a bien la valeur attendue.

  • 17:40 confirme et détaille cette vision

  • 19:00 fetch_add (et ses cousins)

  • 21:30 attention quand on mesure les perfs des atomic : elles sont 1. hardware dependent, et 2. compiler-dependent

  • 27:00 atomic n’est pas nécessairement lockfree, et ça dépend de la plateforme, et n’est connu qu’au runtime (pour des contraintes d’alignement). On dispose tout de même de is_always_lock_free au compile-time.

  • 30:30 c’est toute la cacheline qui est synchronisée

  • 33:00 différence entre compare exchange weak et strong = le weak peut renvoyer false même si la condition est en fait remplie (spurious failure)

  • 39:00 l’archétype d’une atomic data structure : une variable atomic contient un index/pointeur sur une zone mémoire non-atomic

  • 42:00 par conséquent, les memory barriers vont avec les atomic, pour garantir que la zone mémoire à laquelle on accède via l’atomic est bien dans l’état souhaité.

    • Sans memory barriers, chaque cpu (donc chaque thread) voit un état indépendant de la mémoire. La MB permet de synchroniser les cachelines des cpus.

  • 43:30 memory barriers et memory order sont corrélés

    • Relaxed = pas de garantie sur l’ordre des instructions mémoire

    • Acquire = aucune opération ne peut être déplacée avant la barrière

    • Release = le contraire : aucune opération ne peut être déplacée APRÈS la barrière (valable même pour tout le code, y compris pour les variables non-atomiques !)

    • Utilisées conjointement sur la même atomic, release store dans un thread1 + acquire load dans un thread2 permet de synchroniser des threads, y compris sur des lectures / écritures de variables non atomic

    • Il y a deux autres memory order.

    • Si on ne précise pas l’ordre, le défaut est d’utiliser le plus fort : plus safe, mais moins efficace

  • 50:30 les perfs des différentes barriers dépendent de la plateforme

  • 57:30 : pour vraiment gagner des perfs avec les atomics, il faut utiliser les memory barriers

  • 58:00 quand faut il utiliser le lockfree ? Grosso modo quand on ne peut pas utiliser de locks, ou que c’est pas pratique de locker.

  • 01:03:00 IMPORTANT = Dans quels cas utiliser les memory order ? le cas où les memory order sont importantes sont les cas où l’atomic est utilisé pour synchroniser d’autres variables (e.g. pour synchroniser l’accès à un tableau). Si ça n’est pas le cas (e.g. car on ne s’intéresse qu’à la valeur en soi de l’atomic), alors on peut utiliser memory_order_relax.

[VIDEO] CppCon 2014: Herb Sutter "Lock-Free Programming (or, Juggling Razor Blades)" part 1 et part 2

Vue le 2021-02-??, publiée le 2014-10-16 sur la chaîne de la CppCon, par Herb SUTTER

  • TL;DR : un bon talk sur l’utilisation des atomic pour lock-free programming.

  • Notes vrac sur le premier talk :

    • 03:00 with locks, either simplicity or scalability

    • 03:30 mesurer avant (pour vérifier qu’on adresse bien le bon problème), et après (pour vérifier qu’on a bien avancé dans la bonne direction)

    • 05:00 useful to see locks as trafic lights

    • 06:00 analogie intéressante avec échangeur d’autoroute

    • 07:30 lockfree = transactional thinking + atomic

    • 17:00 wait-free vs lock-free vs obstruction-free

    • 18:30 double check locking (pour ne pas avoir à prendre le lock pour rien) utile pour initialiser un singleton, mais l’atomic reste indispensable

    • 22:00 les atomics ont des memory-barriers implicites (donc ne sont pas réordonnées).

    • 24:40 atomic store peut avoir un fort overhead, mais pas atomic load.

    • 27:00 call_once pour initialiser un truc une seule fois

    • 36:30 les invariants doivent être vrais aux frontières des sections critiques

    • 38:00 locking + lockfree

    • 45:00 et autour : ce qui est important pour le lockfree, c’est de comprendre comment les différents threads travaillent avec l’atomic

    • 55:00 (environ) : la question de savoir si travaille le plus vite (donc qui sera sous-utilisé) entre le producer et les consumers est importante pour choisir comment implémenter l’algo lockfree.

  • Notes vrac sur le deuxième talk :

    • ~13:00 ABA problème : on a un pointeur sur un T*, mais la même adresse pointe successivement vers deux objets différents (le premier a été détruit, et son adresse a été réutilisée pour créer le second)

    • ~26:00 utilisation de référence counting (shared ptr) pour être robuste à la concurrency.

    • 28:30 linearizability = même si deux opérations overlappent, tout se passe comme si elles avaient plutôt été successives car protégées par mutex.

    • Ndm : compare_exchange permet le pattern suivant, permettant de remplacer p par p→next, y compris en cas d’utilisation concurrente, tout en évitant les problèmes de TOCTOU :

      auto p = head.load();
      while(p && head.compare_exchange_weak(p, p->next)) {}
    • L’idée est que s’il n’y a pas d’accès concurrent, on fera l’échange directement. Mais si p a changé depuis qu’on l’a lu (ou plutôt load), alors on recommence avec la NOUVELLE valeur de p (car en cas d’échec, compare_exchange fait un load de la NOUVELLE valeur de p)

    • 51:00 :

      • Throughput = total work (ici, nombre d’objets) qui peuvent passer par la queue.

      • Scalability = la capacité à accomplir plus de travail si on a plus de cores disponibles

      • Contention = how much threads interfere with each other by fighting for resources.

      • Oversubscription = quand il y a plus de threads (cou bound) que de cores pour les faire progresser.

    • La façon de mesurer les perfs dans un contexte concurrent (sur les 20 dernières minutes) est très intéressante. (À 01:07:15, il y a un autre graphique intéressant)

    • Plus généralement, c’est un rex concret d’optimisation multithread super (et guidé par des mesures!)

    • 56:30 pour améliorer les perfs, un point intéressant : je peux dégrader les perfs d’un thread (e.g. en faisant plus de heap allocation) si ça permet plus de parallelisation (e.g. en ayant moins de code dans une section critique).

    • 01:06:00 alignas pour éviter le false sharing

    • 01:09:40 si on dépasse la "subscription boundary" (i.e. si on commence à avoir plus de threads que de cores), il y a une brutale discontinuité dans les perfs

    • 01:10:00 to improve scalability, minimize contention

[ARTICLE] Illusions de l’informatique distribuée

Lu (c’est un bien grand mot) le 2021-02-23, rédigée le ????-??-?? sur https://fr.wikipedia.org/

  • Les illusions en question :

    • Le réseau est fiable.

    • Le temps de latence est nul.

    • La bande passante est infinie.

    • Le réseau est sûr.

    • La topologie du réseau ne change pas.

    • Il y a un et un seul administrateur réseau.

    • Le coût de transport est nul.

    • Le réseau est homogène.

[ARTICLE] Comprendre la Sainte-Trinité

Lue le 2021-02-07, rédigée le ????-??-?? sur le site de Patrice ROY, professeur Montréalais, qui en plus d’être très pédagogique, de faire partie du Working Group normalisant le C++, est super gentil :-)

  • Synthèse :

    • "sainte-trinité" = règle de 3, de 5 ou de zéro selon les versions

    • TL;DR : soit tu ne définis aucune des 5 fonctions suivantes toi-même, soit tu les définis tous :

      • copy-constructor

      • copy-assignment operator

      • move-constructor

      • move-assignment operator

      • delete

    • attention : il y a des règles particulières indiquant si le compilo définira ou non lui-même ces fonctions

    • Plutôt qu’une règle absolue, c’est une bonne pratique, qui invite à double-checker lorsqu’on est amenés à ne pas la suivre.

    • le cas où il faut les définir soi-même est le cas où la classe est responsable d’une ressource (et dans ce cas, mieux vaut que ce soit sa seule responsabilité)

    • règle étendue = 3½ / 5½ : ajoute la fonction swap aux 5 fonctions ci-dessus

    • par ailleurs, l’article porte un regard intéressant sur les langages qui poussent la value-semantic, et ceux qui poussent la reference-semantic

  • NdM : l’état de mes connaissances avant de lire l’article :

    • soit tu implémentes les 3(5) opérateurs, soit tu n’en implémentes aucun

    • tu implémentes les 3(5) :

      • si tu as besoin d’implémenter l’un des trois, tu as sans doute besoin d’implémenter les deux autres

      • et c’est parce que la classe "gère une ressource externe"

      • exemple = dans son constructeur, une classe alloue un int (et le stocke dans un int*)

      • dans ce cas, on veut définir un destructeur particulier qui delete l’int*

      • la règle de 3 dit alors qu’il faut définir ce qu’on veut faire quand on recopie une autre instance (copy-constructor ou copy-assignment)

      • en effet, si on se contente de copier l’int*, la destruction des deux instances va delete deux fois le pointeur

      • plus probablement, on veut delete notre int actuel et en allouer un autre (ou encore garder notre int, et copier la valeur)

    • si tu n’en implémentes aucun :

      • notre classe a une plutôt un comportement "normal" vis-à-vis de ses membres

      • ils peuvent utiliser la copie-construction par défaut / destruction par défaut

      • du coup, MIEUX VAUT utiliser les default versions de ces opérateurs (et donc n’en définir aucun, cf. la règle de 3)

  • Définir ce qu’est un objet en POO :

    • accès à un objet

    • ça veut dire quoi construire l’objet

    • ça veut dire quoi finaliser l’objet (aka destruction)

    • ça veut dire quoi dupliquer l’objet

  • Plutôt qu’une règle absolue, c’est une bonne pratique, qui invite à double-checker lorsqu’on est amenés à ne pas la suivre.

  • Recommandation = expliciter son intention (au besoin, avec =default ou =delete)

  • Valeurs par défauts des 3 opérations :

    • construction par copie = appeler le constructeur de copie de chacun des attributs

    • affectation = appeler l’affectation de chacun des attributs

    • destruction = appeler le destructeur de chacun des attributs

  • Rôle des 3 opérations :

    • constructeur = mettre en place les invariants de l’objet construit

    • opérations de copie = maintenir ces invariants (parmi d’autres rôles)

    • destructeur = assurer la saine libération des ressources dont est responsable l’objet

  • Sainte trinité d’un objet correcte si tous ses membres ont une sainte-trinité correcte et une opération de copie correctement définie.

  • La sainte-trinité est à définir si un objet prend la responsabilité d’une ressource :

    • allocation dynamique de mémoire

    • ouverture d’un stream (NdM ou d’un fichier)

    • prise en charge d’un mutex ou autre mécanisme de synchronisation

    • (NdM : database connection, …​)

    • NdM : à mes yeux, cette guideline est la plus importante qui va avec la règle de 5 : si un objet prend la responsabilité d’une ressource, il faut définir les 5 opérations

  • Note importante : ce n’est pas parce qu’un objet contient des membres pointeurs (ou même un conteneur de pointeurs) qu’elle doit se préoccuper de la sainte-trinité !

    • en effet : si l’objet n’est pas RESPONSABLE des objets pointés (dans le sens où il doit penser à les détruire), il n’est pas oblité de définir les 3 opérations de la sainte-trinité

  • Au sujet du lien entre la sainte-trinité et l’opposition entre value/reference-semantic :

    • NdM : l’opposition reference-semantic vs. value-semantic semble être regroupée sous le terme "sémantique d’accès".

    • java/C# = reference-semantic :

      • objets créés dynamiquement (heap-allocated)

      • on n’y accède que par référence (on n’a jamais accès à l’objet lui-même)

      • garbage-collecté (donc la finalisation n’est pas déterministe)

    • C++ = value-semantic :

      • objets typiquement (mais pas exclusivement) stack-allocated

      • finalisation déterministe

    • L’article explique qu’avoir une sémantique de valeur implique qu’il est plus important de savoir ce que représente une copie d’objet :

      • possiblement, car il y a bien plus de copie que dans un langage ayant une sémantique de référence, notamment car il y en aura plein lors des appels de fonction ?

      • EDIT : exprimé dans l’autre sens, dans un langage avec une sémantique de référence comme python, les objets sont rarement copiés, et la sainte trinité moins importante

    • De même l’implication avec le fait qu’il est plus important (qu’avec une reference-semantic) de bien savoir ce qu’est un destructeur n’est pas claire…​

      • possiblement car on va vouloir faire plus de choses avec le destructeur, vu que celui-ci est un outil plus utile ?

      • possiblement aussi, car comme on copie souvent les objets, on les copie dans des contextes temporaires (comme un appel de fonction), et que du coup, les objets sont détruits souvents

    • EDIT : il clarifie ailleurs :

      Dans un langage où l’usage est de manipuler des objets directement, offrir un support automatique de la Sainte-Trinité va de pair avec des principes de base de saine programmation. Évidemment, dans un langage où l’accent est mis sur l’allocation dynamique de ressources, le partage d’objets et l’accès indirect, les pratiques sont différentes. En C# et en Java, mieux vaut penser immuabilité (pour réduire les conséquences néfastes du partage implicite des objets) que d’insister sur la copie des objets, puisque cette dernière opération y est moins naturelle, moins idiomatique.

    • EDIT 2 : et il détaille encore plus dans le chapitre "Risques du partage" :

      Puisque tous les objets en C# et en Java sont alloués dynamiquement, et puisque la copie dans ces langages est d’abord et avant tout une copie de référence, donc une sémantique de partage du référé il est très important de développer avec ces langages l’habitude de concevoir des objets immuables.
      Par immuable, on entend une classe dont les instances ne peuvent être modifiées une fois construite (pas de mutateurs ou de services semblables). Sans surprise, la plupart des classes importantes de langages comme C# et Java sont immuables (String en Java et string en C# en sont de bons exemples). Pour comprendre les enjeux, voir ce texte https://h-deb.clg.qc.ca/Sujets/Divers--cplusplus/Importance-constantes.html#immuabilite
      Partager un objet qui n’est pas immuable est dangereux en situation de multiprogrammation, et prête à risque même en situation de monoprogrammation, brisant le principe de moindre surprise. Il est difficile de savoir quand il est le plus opportun de dupliquer un objet mutable (par clonage ou par copie) pour éviter les bris d’encapsulation; les objets faisant partie d’une interface dans ces langages devraient conséquemment tous être immuables.

    • EDIT 3 : cette autre ressource listée en bas de page donne des billes :

      Because C++ copies and copy-assigns objects of user-defined types in various situations (passing/returning by value, manipulating a container, etc), these special member functions will be called, if accessible, and if they are not user-defined, they are implicitly-defined by the compiler.

      • Donc en gros, le lien avec la sémantique de référence, c’est qu’en C++, on a plus tendance à modifier les objets que par exemple en java, où les objets sont plus souvent immutables.

  • Le code d’exemple suivant est intéressant, notamment l’utilisation du copy-and-swap idiom pour implémenter l’opérateur d’affectation :

    class TiTableau {
    public:
       using size_type = std::size_t;
    private:
       size_type nelems{};
       int *elems;
    public:
       TiTableau(size_type n) : elems{ new int[n] } {
          fill(elems, elems+size(), 0);
       }
       TiTableau(const TiTableau &autre) : nelems{ autre.size() }, elems{ new int[autre.size()] } {
          copy(autre.elems, autre.elems+size(), elems);
       }
       void swap(TiTableau &autre) {
          using std::swap;
          swap(elems, autre.elems);
          swap(nelems, autre.nelems);
       }
       TiTableau& operator=(const TiTableau &autre) {
          TiTableau{ autre }.swap(*this);
          return *this;
       }
       ~TiTableau() {
          delete [] elems;
       }
    };
  • L’opérateur swap est si important qu’on appelle parfois plutôt la règle de 5½.

  • NdM : un autre exemple de cas où il a fallu se poser la question = unique_ptr.

    • comme la classe est responsable d’une ressource, il faut savoir quoi faire à la copy-construction, ou à la copy-affectation

    • en l’occurence, unique_ptr n’est pas copiable ou assignable par copie : deux instances de unique_ptr ne peuvent pas gérer le même objet.

    • (mais il peut transférer la responsabilité de l’objet qu’il gère via des move-operations)

  • Autre phrase intéressante :

    Là où shared_ptr définit une sémantique de partage (copier un shared_ptr signifie partager son pointé), unique_ptr définit une responsabilité exclusive sur le pointé. Le contenu pointé par un unique_ptr peut être transféré d’un unique_ptr à un autre, mais un unique_ptr ne se copie pas. Pour cette raison, unique_ptr est plus sécuritaire en situation de multiprogrammation, et est aussi plus léger en mémoire. En C++, donc, c’est unique_ptr si possible, et shared_ptr si nécessaire.

  • Il y a quelques lignes sur la comparaison à java et C#, l’intérêt des GC, et le fait que la structure C++ qui se rapproche le plus d’une création d’objet en java/C# est auto x = shared_ptr<X>(new X);

  • Génération automatiques par le compilo :

    • les 5 opérations de la règle des 5 sont générées automatiquement par le compilateur, sauf si l’utilisateur en a défini au moins une lui-même

    • si l’utilisateur empêche le compilo de définir les autres opérations parmi les 5, elles sont considérées comme =delete

    • règle de zéro : pour une classe qui n’est pas explicitement responsable de ressources, mieux vaut ne coder aucune des opérations de la règle de cinq

  • Les liens de la page sont une mine d’or :

    • https://en.cppreference.com/w/cpp/language/rule_of_three

      Rule of five : Because the presence of a user-defined destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions:
      Unlike Rule of Three, failing to provide move constructor and move assignment is usually not an error, but a missed optimization opportunity.
      Rule of zero : Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership (which follows from the Single Responsibility Principle).
      Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators.

    • https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rc-zero

      If you can avoid defining default operations, do
      If you define or =delete any copy, move, or destructor function, define or =delete them all

[VIDEO] What is an ABI, and Why is Breaking it Bad ? — Marshall Clow — CppCon 2020

Vue le 2021-02-05, publiée le 2021-02-03 sur la chaîne de la CppCon, par Marshall CLOW, notamment auteur de Boost.Algorithm, lead-dev d libc, et chairman of the Library working group of the C standard committee.

  • TL;DR = une vidéo intéressante sur les conséquences d’un changement d’ABI non-rétrocompatible.

  • 03:00 Apparemment, ABI est définie par platform, et non par c++ standard

  • 04:15 ABI break = specific break of ODR

  • If we had no shared compilation, there would be no ABI breaks.

  • 08:00 fragile base classe problème , si on modifie la classe de base, il faut recompiler les classes filles.

  • 09:00 c++ ne connait pas les noms des membres, juste leurs offsets

  • 11:00 vtable = array de pointeurs de fonctions. Taille de l’array est le nombre de fonctions virtuelles. L’ordre des fonctions dans l’array dépend du compilateur.

  • 14:00 vouloir ne pas casser l’ABI de iostream a empêché d’ajouter des short floats (floats de 16 bits) dans c++ 20. En effet, il aurait fallu ajouter de nouvelles fonctions virtuelles à iostream.

  • 15:40 quand clang a été fait, l’un des objectifs était de produire des object files compatibles avec GCC. (Du coup, ils étaient obligés d’utiliser le même ordre de fonctions virtuelles dans la vtable que GCC)

  • 18:00 wow, un obscur changement, (possiblement sans même modification du code assembleur généré) casse l’ABI ! En gros, remplacer un custom copy-constructor par =default rend possiblement les classes-filles copy-constructible, ce qui autorise certaines plate-formes à passer ses paramètres par registres plutôt que sur la stack, ce qui est un changement d’ABI.

  • 26:00 don’t have stale binaries = don’t have binaries older than things they use.

  • 28:30 quelques explications sur le changement d’ABI des strings en c++11. Dix ans après, ça pose encore problème.

  • 30:30 exemple de conséquence de ce changement d’ABI, si l’os a été compilé par GCC 4.3 ( donc avec l’ancienne ABI des strings, pre-c++11), et qu’on utilise dessus un binaire compilé avec gcc9 (éventuellement compilé soi-même, mais c’est pas obligatoire), donc utilisant la nouvelle ABI, ça va crasher, car le layout attendu par le code qu’il vient de compiler n’est pas celui de la shared lib de l’OS, qu’il utilise.

  • 34:30 les ABI breaks, c’est un problème des implémentations du langage c++ (plutôt que du langage lui-même)

  • 35:30 discussion par un working-group de "quand peut-on changer une ABI ?"

  • 38:15 pour s’autoriser un ABI break, tout en évitant à un dev d’utiliser une lib non compatible sans s’en rendre compte (donc être capable de détecter l’utilisation d’une ABI non-compatible), une proposition pour c++26 est de changer le mangling scheme. Ainsi, on ne pourra même plus linker son object file fraîchement compilé avec un object file plus vieux utilisant l’ancienne ABI (comme une lib système)

  • 40:00 autre possibilité pour s’autoriser des changements d’ABI : les binaires (ex, les libs systèmes) embarquent les DEUX versions du code assembleur, l’une utilisant l’ancienne ABI, l’autre utilisant la nouvelle.

  • 41:00 ça n’est que quand on exécute un programme que l’ensemble du système qui va tourner est complet (donc c’est seulement à ce moment tardif qu’on sait si les ABIS seront compatibles ou non). Du coup, les binaires ne sont pas vérifiables statiquement pour savoir si oui ou non ils utilisent des ABI compatibles.

  • 43:00 explication sur pourquoi même au linktime, on ne peut pas vérifier si les binaires sont compatibles. (TLDR : parce qu’au runtime, la dépendance à la lib sera peut-être résolue avec une AUTRE version de la lib que celle utilisée au linktime, du coup vérifier au linktime n’est pas suffisant).

  • 46:00 les conséquences d’un ABI break

  • 47:30 exemple concret avec Photoshop et ses plugins : ABI break d’une lib partagée suite à un upgrade d’os. Photoshop fait bien le boulot et propose une nouvelle version compatible avec cette nouvelle ABI. Mais pas nécessairement les 40 développeurs des 40 plugins que j’utilisais ! Du coup, pour retrouver un système stable, je rétropédale et downgrade mon os : je ne pourrai upgrader que quand tous les plugins proposeront une nouvelle version compatible du plugin, avec la nouvelle ABI.

  • 51:00 note que le problème de compatibilité d’ABI ne se pose QUE si on veut faire évoluer une lib (ou plus généralement, un binaire) existant. Notamment, si on se contente d’AJOUTER des nouvelles choses sans jamais modifier ce qui existe, on est tranquilles. Exemple concret = plutôt que de faire évoluer une structure Hashmap (en cassant son ABI au passage), si on se contente d’AJOUTER une nouvelle structure BetterHashmap, on n’aura pas de soucis d’ABI. Mais bon, c’est pas toujours faisable ou pratique. Apparemment, java fait beaucoup ça.

  • 52:30 pas directement lié à ABI, mais intéressant tout de même : on peut utiliser des options d’optimisation différentes pour différentes parties du code (i.e. différents fichiers objets) : certains fichiers objets sont compilés avec des options pour optimiser pour réduire la taille, et d’autres fichiers objets sont compilés avec des options pour optimiser la vitesse d’exécution.

  • 56:00 grosso modo, la version d’ABI de la lib standard de ton système définit l’ABI que tu vas utiliser avec ton système. (En gros, t’as pas trop le choix). C’est la raison pour laquelle j’ai dû préfixer par LD_LIBRARY_PATH pour utiliser un compilo plus récent que celui de mon système.

  • 58:00 symbol versioning semble être un moyen de contourner les problèmes de compatibilité. Ndm : car on retombe dans le "ajouter plutôt que faire évoluer". C’est pas hyper clair pour moi comment l’utilisateur de la lib qui a versionné ses symboles pourra "choisir" le symbole…​ Hum en fait si : il choisit soit via le header de la lib (si les deux fonctions ont des noms différents, mais bon, c’est plus vraiment du symbol-versioning), soit au linktime, selon la version de la lib avec laquelle il linke son binaire (si la fonction garde le même nom - l’aPi ne change pas - mais est manglée en un symbole différent, versionné)

  • Apparemment, c’est déjà le cas pour la stdlib de c++ : les noms manglés sont préfixés par _1, où le 1 correspond à du symbol versioning. EDIT : je vérifie qu’il y a bien des versions d’ABI dans les noms manglés (environ 25%, on dirait) :

    nm -D /usr/lib32/libstdc++.so.6.0.28|grep __cxx
    # [...]
    # 00121f10 W _ZTv0_n12_NSt7__cxx1118basic_stringstreamIwSt11char_traitsIwESaIwEED1Ev
    # 00121bf0 W _ZTv0_n12_NSt7__cxx1119basic_istringstreamIcSt11char_traitsIcESaIcEED0Ev
    # [...]
    
    nm -D /usr/lib32/libstdc++.so.6.0.28|wc -l
    # 5983
    
    nm -D /usr/lib32/libstdc++.so.6.0.28|grep __cxx|wc -l
    # 1488
  • 01:00:30 même si le comité c++ s’y intéresse, ces questions de compatibilité d’ABI, c’est plutôt le problème de ceux qui font les libs standard (donc les fabricants de compilateurs).

[POST] Type erasure — Part I

Lu le 2021-02-02, publié le 2013-11-18, sur le blog d’Andrzej Krzemieński, un dev C++ polonais qui a l’air bien calé

  • TL;DR = excellent article expliquant les différentes façons d’erase les types

    • type-erased = "interface" = on est générique vis-à-vis du type réel sur lequel on travaille

    • le fil rouge de l’article est la recherche dans un conteneur trié via un binary search

    • pas de type-erasure, templatization → le plus rapide, mais on a les inconénients des templates

    • void* type-erasure = pas terrible car pas type-safe + ne fonctionne qu’avec des arrays

    • OO-based type-erasure = pas terrible car nombreux problèmes (notamment, force d’hériter d’un type)

    • template-based type-erasure = pas parfait, mais mieux que le reste, notamment : type-safe + templatization uniquement sur le type de l’objet lui-même

    • l’article se poursuit avec deux autres articles : part II, part III

  • dispatch statique (pas de type-erasure ici) :

    • Le compilateur choisit statiquement la bonne spécialisation de cout en fonction du type de s ou i.

      void print (string s, int i)
      {
          cout << s;
          cout << i;
      }
    • Ici, le type (non-erased) contient donc des informations utiles au compilateur.

    • cout::operator<< est polymorphic, car il se comporte différemment lorsqu’appelé avec des variables différentes.

    • Point important : il n’y a PAS de runtime-dispatch : c’est bien au compile-time que le dispatch a lieu → pas d’overhead au runtime.

  • être générique avec les templates :exemple de l’article = algo de type binar-search en C

    • en C : bsearch, utilisant une fonction de comparaison, qui accepte des void*

    • en C++ : std::equal_range

      int c_bigger (const void * a, const void * b)
      {
          return *(int*)b - *(int*)a;
      }
    • l’équivalent en C++ utilise une lambda comme fonction, et est plus rapide qu’en C

    • en C, le compilo n’est pas aware du type de la fonction de comparaison, et du fait qu’elle travaille sur deux int → pas d’optimisation possible, on déréférencera au runtime le pointeur de fonction

    • du coup, ici, l’utilisation de template permet d’aller plus vite au runtime

    • TL;DR : on fait un trade-off : utiliser les templates (fast-runtime, slow build) ou ne pas les utiliser (le contraire)

  • inconvénient des templates :

    • binaire plus gros (vu qu’on a une instanciation par "spécialisation")

    • compile-time slow down

    • le body de la fonction doit être visible (dans le header), et on doit donc exposer toutes les dépendances (les headers dont notre template a besoin) → compile-time encore plus lent

    • messages d’erreur des template-instanciation souvent illisibles

  • avantage des templates :

    • programmes plus rapides que leurs équivalents non-templates

    • type-safety par rapport aux équivalents non-templates

  • void* type-erasure :

    • exemple = le bsearch de C : au runtime, on a une indirection pour utiliser un pointeur de fonction

    • note : cf. "man bsearch" la signature de bsearch attend un pointeur de fonction qui a cette signature :

      int (*compar)(const void *, const void *)
    • le code de la fonction de comparaison va caster les void* en int pour pouvoir les comparer (c_bigger plus haut)

    • citation : bsearch is in fact an example of basic type erasure. It is one function, with one interface that works for arrays of any element type.

    • TL;DR : en C, void* permet d’être générique sur le type des éléments qu’on passe à une fonction

    • utiliser void* permet d’overloader bsearch au runtime, qui marche dorénavant avec TOUT type de tableau

    • problèmes :

      • problème : on ne peut rien faire d’un void* :

        • soit on le passe à une autre fonction qui en fait qqch (exemple avec c_bigger, une autre fonction caste en int)

        • soit on passe d’autres arguments codant l’information qui aurait été apportée par le type (exemple avec bsearch : on passe un argument size de type size_t pour savoir comment incrémenter les itérateurs)

      • non-type safe : on peut tout à fait passer une fonction de comparaison qui compare des entiers à un bsearch appliqué à un tableau de string (et ça va sans doute UB)

      • le compilo ne détecte rien, car les deux fonctions de comparaison ont même signature (utilisant des void*)

      • autre limitation = bsearch ne fonctionne qu’avec des arrays (et ne saura pas par exemple travailler avec un std::set)

  • OO-based type-erasure :

    • tous les objets héritent d’un même classe, toutes les classes filles implémentent un opérateur de comparaison avec un autre Object* (qui throw lorsque l’Object auquel on compare n’est pas du bon type)

      int i_bigger (const Object* a, const Object* b)
      {
        auto ia = dynamic_cast<MyInteger const*>(a);
        auto ib = dynamic_cast<MyInteger const*>(b);
      
        if (ia == nullptr || ib == nullptr) {
          throw SomeException{};
        }
      
        return ib->getInt() - ia->getInt();
      }
      
      Object* OO_search (Object** base,
                         size_t size,
                         int (*comp)(const Object*, const Object*)
    • on a UN PEU amélioré la situation : au lieu d’un UB, on a une exception au runtime lorsqu’on compare des objets non-comparables

    • beaucoup de problèmes :

      • on est obligés d’hériter d’Object (ce qui n’est par exemple pas possible pour les int)

      • conséquence pas cool = c’est au CLIENT de notre code d’adapter ses types pour les faire hériter de Object

      • en plus d’être chiant, n’est pas toujours possible (notamment, si on veut pouvoir appliquer 3 algos différents, il faut hériter de ns1::Object, ns2::Object, et ns3::Object)

      • accessoirement prend plus de place au runtime (il faut de la place pour la vtable)

      • comme on ne connaît pas les types réels des objets, on ne peut plus stocker des objets hétérogènes dans un container

      • le fait d’avoir une exception au lieu d’un UB est pas fi-fou : on préfèrerait une erreur du compilateur

      • le fait d’hériter d’Object ne sert qu’à une pauvre chose : pour passer un Object* et tester le type réel avec dynamic_cast

      • tout comme void* / bsearch, notre fonction ne marche qu’avec des arrays

  • value-semantic type erasure :

    • pour être type-safe, on n’aura pas le choix, faut rester sur des templates…​

    • on utilise std::function pour abstraire le type de la fonction de comparaison (qui peut être n’importe quoi, tant qu’on peut lui appliquer f(int, int)) :

      std::function<bool(int,int)> predicate;
    • alias template :

      template <typename T>
      using AnyBinaryPredicate = std::function<bool(T const&, T const&)>;
    • citation :

      With std::function we :
      (1) erase the type of the underlying function/function-like object,
      (2) preserve the interface (operator()),
      (3) we are able to pass it by value,
      (4) we require of the erased types no declaration of conformance to an interface (no inheritance).

    • de la même façon qu’on a fait une "interface template" pour représenter des fonctions, on va faire une "interface template" pour représenter des itérateurs sur un type T.

    • On utilise une lib "any_iterator", le code est très illustratif :

      std::vector<int> vec {1, 2, 3};
      std::list<int> list {2, 4, 6};
      
      AnyForwardIter<int> it { vec.begin() }; // initialize
      it = list.begin();                      // rebind
    • on peut aussi utiliser un type-erased range :

    • boost::any_range, le code est très illustratif :

      std::vector<int> vec {9, 8, 5, 4, 2, 1, 1, 0};
      std::set<int> set {1, 2, 3, 5, 7, 9};
      
      AnyForwardRange<int> rng = vec; // initialize interface
      std::distance (boost::begin(rng), boost::end(rng));
      
      rng = set;                      // rebind interface
      std::distance (boost::begin(rng), boost::end(rng));
    • note : ça a l’air important : les interface typed-erased ci-dessus préservent la "value-semantic" = ce qui nous intéresse, c’est pas les objets eux-mêmes, c’est leur contenu

    • grâce à range et std::function, on a un algo qui n’est plus que templaté sur T (alors que pour une version template-pure, il était également templaté sur le type du conteneur (via l’iterator) et le type du prédicat) :

    • Thus, we have two value-semantic type-erased interfaces: AnyForwardRange<T> and AnyBinaryPredicate<T>. Using them we can define our (partially) type-erased searching function:

      template <typename T>
      AnyForwardRange<T> Search (AnyForwardRange<T> rng, T const& v,
                                 AnyBinaryPredicate<T> pred)
      {
        auto ans = std::equal_range (rng.begin(), rng.end(), v, pred);
        return {ans.first, ans.second};
      }
    • défauts :

      • on est obligé de préciser explicitement le paramètre template

      • moins performant au run-time qu’une version template pure

[PREZ] la notation hongroise

Présentation rapide par un collègue le 2021-01-21.

  • TL;DR = préfixer ses noms de variables pour ajouter du renseignement (à ne pas confondre avec polish notation, mathématique).

  • Apps Hungarian Notation = on ajoute de l’info sur ce que représente la variable = GOOD

    • rwPosition : variable represents a row ("rw");

    • usName : variable represents an unsafe string ("us"), which needs to be "sanitized" before it is used (e.g. see code injection and cross-site scripting for examples of attacks that can be caused by using raw user input)

  • Systems Hungarian Notation = on ajoute de l’info sur le type de la variable = BAD

    • lAccountNum : variable is a long integer ("l");

    • strName : Variable represents a string ("str") containing the name, but does not specify how that string is implemented.

    • bad car fait doublon avec le type-system (d’où p.ex. possible désynchro entre le type préfixé et le type réel). Tire son origine d’une incompréhension de la notation hongroise par l’équipe système de Microsoft.

  • intérêt mis en avant par Joël Spolsky (pour Apps uniquement) = le code qui est faux a l’air faux (e.g. rAngle += 360; )

[POST] The terminal, the console and the shell - what are they?

Lu le 2021-01-18, publié le 2021-01-13, sur unixsheikh, un dev freelance anonyme (son github).

  • TL;DR : l’article donne beaucoup d’infos intéressantes sur terminal/console/shell :

    • la console est "ce qui permet d’interagir avec l’ordinateur" (par analogie avec le meuble), c’est l’ensemble terminal+shell

    • le terminal reçoit le signal du clavier, et en déduit une séquence de caractères ASCII à envoyer au shell

    • le shell est un REPL autour du kernel, c’est un wrapper permettant d’interagir avec le kernel (y compris de lancer des programmes)

    • le shell reçoit et interprète la séquence de caractère, en fonction du terminal qui l’a envoyé

  • par exemple, si l’appui sur la touche "Fin" provoque un affichage bizarre au lieu d’amener le curseur en fin de ligne c’est peut-être parce que l’envvar TERM est mal configurée

    • p.ex. l’envvar du shell a p.ex. la valeur VT100 alors qu’on utilise un terminal VT220

    • et le terminal VT220 utilise TATA comme séquence de caractères identifiant "Fin", alors que VT100 utilise TOTO

    • du coup, à l’appui sur "Fin", notre terminal VT220 envoie "TATA"

    • le shell reçoit "TATA", et regarde si c’est une séquence spéciale pour le TERM qu’il utilise (VT100)

    • ce n’est pas le cas (en effet, VT100 utilise "TOTO" pour "Fin"), du coup il considère que la séquence n’est pas spéciale, et l’affiche…​

  • attention, quand on parle de terminal, à ne pas confondre :

    • terminal réel (sur les consoles physiques de l’époque, ou un teletype, ou encore un terminal hardware spécifique comme le VT100)

    • terminal virtuel (sur l’ordinateur, p.ex. Ctrl+Alt+F1)

    • terminal émulé (e.g. xterm, gnome-terminal, …​.)

    • terminal multiplexé (e.g. tmux, gnu screen)

  • terminal virtuel :

    • A virtual terminal or virtual console is a program that simulates a physical terminal.

    • For example, both the Linux kernel and BSD kernels support virtual terminals - terminals that are logically separate, but which access the same physical keyboard and monitor.

    • The virtual terminal gives the impression that several independent terminals are running concurrently.

    • Each virtual terminal can be logged in with a different user and it can run its own shell and have its own font settings.

    • Il faut comprendre ces phrases comme :

      • le clavier/écran qu’on utilise ne constituent pas un unique terminal permettant de se relier à l’ordinateur

      • en fait, tout se passe comme si le clavier/écran représentaient plusieurs terminaux indépendants (virtuels), permettant tous de se relier à un seul ordinateur

    • à chaque terminal virtuel est associé un /dev/ttyX

    • (attention à ne pas confondre un terminal virtuel avec un terminal émulé)

  • terminal émulé :

    • A terminal emulator is a computer program that emulates a physical terminal within some other display architecture, such as the X Window System.

    • Un terminal parent permettant de se connecter à l’ordinateur (ici, le serveur X, mais ça pourrait être un terminal virtuel) va lancer un programme qui émule un terminal

    • Ce programme va donc capturer les inputs clavier (et transmettre des caractères ASCII à un shell) et afficher les outputs du shell (ou de ses sous-programmes).

    • À la base, ces terminaux émulent des machines physiques (e.g. VT100)

      • notamment, chaque émulateur de terminal peut envoyer des séquences ASCII différentes en fonction des touches enfoncées

      • du coup, pour reprendre mon exemple de plus haut, quand on appuie sur "Fin", un émulateur qui émule VT100 enverra "TOTO", alors qu’un émulateur qui émule VT220 enverra "TATA"

    • The purpose of the terminal emulator is to allow access to the command line (NdM : to the shell, plutôt) while working in a graphical user interface, such as the X Window System.

    • Since the shell is "expecting" to interface with a human through a terminal, and we don’t use a physical terminal while in a graphical environment, we need the terminal emulator.

    • Ma compréhension est qu’un émulateur de terminal donné peut émuler plusieurs terminaux (dit autrement, il peut associer plusieurs séquences ASCII différentes à la touche "Fin")

    • Il est important de comprendre qu’un émulateur de terminal est un programme GRAPHIQUE (car il émule la sortie vidéo du terminal, et donne accès à une console)

  • l’envvar TERM :

    • The environment variable TERM tells applications the name of a terminal description to read from the terminfo database (see man terminfo).

    • Each description consists of a number of named capabilities which tell applications what to send to control the terminal.

    • For example, the cup capability contains the escape sequence used to move the cursor up.

    • Ma compréhension : grâce à TERM, une application sait que si elle reçoit "TOTO", il faut qu’elle bouge le curseur en fin de ligne.

  • ansi escape code :

    • The terminal interprets these ANSI sequences as commands, rather than text to display verbatim.

    • An escape sequence is a combination of characters that has a meaning other than the literal characters contained therein.

    • ANSI sequences were introduced in the 1970s to replace vendor-specific sequences

    • Although hardware text terminals have become increasingly rare in the 21st century, the relevance of the ANSI standard persists

    • because a great majority of terminal emulators and command consoles interpret at least a portion of the ANSI standard.

    • [The VT100] was one of the first terminals to support ANSI escape codes for cursor control and other tasks, and added a number of extended codes for special features like controlling the status lights on the keyboard.

    • This led to rapid uptake of the ANSI standard, becoming the de facto standard for terminal emulators.

    • sur la page wikipedia :

      • séquence ANSI = séquence d’octets particulière contrôlant le terminal (e.g. la couleur du curseur)

      • the relevance of the ANSI standard persists because a great majority of terminal emulators and command consoles interpret at least a portion of the ANSI standard

  • shell :

    • The operating system is the interface between the user and the hardware.

    • A shell process is the program that prompts you for input, takes your commands, and runs them for you.

    • It is a computer program that serves as a command-line interpreter. The shell implements a read-eval-print loop (REPL).

    • The most generic sense of the term "shell" means any program that users employ to type commands.

    • You enter commands at this input prompt and the shell acts as a "command interpreter".

    • The shell takes each command and passes it to the operating system kernel to handle.

    • The shell then parses the result of this action back to the terminal.

    • The shell is both an interactive command language and a scripting language, and is used by the operating system to control the execution of the system using shell scripts.

    • The shell exposes the operating system’s services to a human user or other programs.

    • The shell knows nothing about displaying characters on the monitor or about handling input keystroke codes from the keyboard - that is up to the hardware and software that is implementing the terminal.

      • C’est un keypoint important pour comprendre ce qu’est un terminal, et la différence entre terminal et shell.

      • Derrière, console = terminal + shell

[POST] How to do a code review

Lu le 2021-01-04, publié le 2020-??-??, sur Google’s Engineering Practices documentation

  • TL;DR :

    • ce groupe de 6 pages contient les bonnes pratiques à suivre lorsqu’on fait de la revue de code, en tant que reviewer

    • il existe également l’équivalent en tant que reviewee

    • il n’y a que 6 courtes pages, très faciles à lire, et contenant des conseils très concrets → ne pas hésiter à y revenir (d’ailleurs, je vais les mettre dans mes références)

  • The Standard of Code Review = la page la plus intéressante, remplie de conseils concrets :

    • The primary purpose of code review is to make sure that the overall code health of Google’s code base is improving over time. (NdM : j’ai un objectif supplémentaire = m’améliorer/aider le reviewee à s’améliorer ; cf. la section Mentoring).

    • trade-off entre être très picky, mais ne jamais faire avancer le schmilblick, ou être trop lâche, et dégrader la qualité

    • In general, reviewers should favor approving a CL once it is in a state where it definitely improves the overall code health of the system being worked on, even if the CL isn’t perfect.

    • A key point here is that there is no such thing as “perfect” code—there is only better code. Reviewers should not require the author to polish every tiny piece of a CL before granting approval.

    • A CL that, as a whole, improves the maintainability, readability, and understandability of the system shouldn’t be delayed for days or weeks because it isn’t “perfect.”

    • Reviewers should always feel free to leave comments expressing that something could be better, but if it’s not very important, prefix it with something like “Nit: “ to let the author know that it’s just a point of polish that they could choose to ignore.

    • Principles :

      • Technical facts and data overrule opinions and personal preferences.

      • On matters of style, the style guide is the absolute authority.

      • Aspects of software design are almost never a pure style issue or just a personal preference. They are based on underlying principles and should be weighed on those principles, not simply by personal opinion

    • Don’t let a CL [=ChangeList] sit around because the author and the reviewer can’t come to an agreement.

  • What to look for in a code review

    • Design

    • Functionality

    • Complexity

    • Tests

    • Naming

    • Comments

    • Style

    • Consistency

    • Every Line : Look at every line of code that you have been assigned to review. […​] If it’s too hard for you to read the code and this is slowing down the review, then you should let the developer know that and wait for them to clarify it before you try to review it.

    • Good Things : If you see something nice in the CL, tell the developer, especially when they addressed one of your comments in a great way.

    • Le summary de la page donne de nouveau des conseils très concrets et très intéresssants.

  • Navigating a CL in review

    • Does the change make sense? Does it have a good description?

    • Look at the most important part of the change first. Is it well-designed overall?

    • Look at the rest of the CL in an appropriate sequence.

    • If the CL is too large for you to figure out which parts are the major parts, ask the developer what you should look at first, or ask them to split up the CL into multiple CLs.

    • If you see some major design problems with this part of the CL, you should send those comments immediately, even if you don’t have time to review the rest of the CL right now.

  • Speed of Code Reviews

    • TL;DR : avoir un process de code-review trop lent est très impactant pour l’équipe.

    • One business day is the maximum time it should take to respond to a code review request (i.e. first thing the next morning).

    • On parle bien du temps entre le moment où un dev soumet une PR, et le moment où il obtient des retours (et non du temps entre le moment où la PR est soumise, et le moment où elle est mergée)

    • It is important that reviewers spend enough time on review that they are certain their “LGTM” means “this code meets our standards.” However, individual responses should still ideally be fast.

    • If somebody sends you a code review that is so large you’re not sure when you will be able to have time to review it, your typical response should be to ask the developer to split the CL into several smaller CLs that build on each other,

    • note : le cas des urgences est un cas particulier, et traité à part

  • How to write code review comments

    • Be kind.

    • Explain your reasoning.

    • Balance giving explicit directions with just pointing out problems and letting the developer decide.

    • Encourage developers to simplify code or add code comments instead of just explaining the complexity to you.

    • Explanations written only in the code review tool are not helpful to future code readers.

  • Handling pushback in code reviews

    • TL;DR : échanger en bonne intelligence, ne pas retarder le cleanup.

    • une façon de lutter contre les râleries "ta revue est trop stricte", c’est de les faire rapidement

[POST] Copy and Swap, 20 years later

Lu le 2020-12-23, publié le 2019-01-07, sur le blog de Mathieu ROPERT, dev C++, notamment contributeur de conan package manager

  • TL;DR : une intéressante présentation de l’idiome Copy-and-Swap, et du fait que c’est un trade-off où on gagne la robustesse (et la simplicité) en perdant (un peu) de la performance.

  • Contexte = la Rule of Three (et plus tard Rule of Five, pour tenir compte de la move-semantic) implique :

    • constructeur = laisse l’objet dans un état utilisable

    • on peut copier proprement un objet

    • on peut le détruire, et la destruction cleane proprement les états

      Copy/move constructors, copy/move assignment operators and destructors are the key part objects’ lifecycle.
      If one is wrong, users will get dangling references, leaks, double deletes and other unsavoury things.
      And of course they need to do that without leaking anything if an exception occurs.

    • (NdM : la quote s’applique aux objets qui gèrent des ressources)

    • En pratique, on ne peut pas toujours suivre la Rule of Zero, et il faut parfois gérer explicitement les membres (e.g. si on implémente une classe de type string, il faut gérer le buffer accueillant la string) ; dans ce cas, il faut respecter la Rule Of Five.

      Some operations like construction and assignment are quite similar so we would prefer to write one by calling the other (again reuse reduces the amount of code to review)

  • L’idiome Copy-and-Swap :

    • Write a destructor that deletes any owned resource.

    • Write a copy constructor that duplicates any owned resource and takes ownership of it.

    • Write a non-throwing swap() function that will exchange the contents of two containers by swapping the internal bits.

    • Write the copy-assignment operator by making a temporary copy of the source object, then swap the copy with this.

      T& operator=(const T& rhs)
      {
        T tmp(rhs);  // pas d'impact si exception ici
        swap(tmp);  // non-throwing
        return *this;
      }
  • À quoi sert cet idiome ?

    • à s’assurer d’être robuste aux exceptions dans l’implémentation du copy-assignment operator :

      Copy-assignment is usually the trickiest one to write since it must delete existing content, insert a copy of the source objects and survive if an exception is thrown somewhere in the process.

    • L’intérêt : le swap ne peut pas throw + la construction a lieu dans un objet temporaire, détruit en fin de scope → ce code, pourtant simple, est robuste aux exceptions.

    • (et l’idiome a un frère-jumeau pour le move-assignment operator)

  • keypoint de l’article : l’idiome Copy-and-Swap est un TRADE-OFF dans lequel on échange la robustesse+simplicité contre la performance !

  • Problème n°1 = la copy-construction va systématiquement faire une heap-allocation, alors qu’on n’en a pas forcément eu besoin :

    • heap-allocation systématique, car la classe gère des ressources (sans quoi on n’aurait pas besoin de suivre la rule of five)

    • exemple : si la classe T est une classe gérant un array de int (un genre de vector<`int`>)

    • si elle contient actuellement 5 ints

    • si on essaye de lui copy-assigner un T contenant 5 autres int

    • alors en pratique, on n’a PAS besoin d’une allocation dynamique, vu qu’il suffit de remplacer les 5 ints précédents par les 5 nouveaux ints

    • or, avec le Copy-And-Swap, on va créer un objet temporaire (avec la heap-allocation qui va avec) quoi qu’il arrive.

  • Problème n°2 = on sur-utilise les ressources, puisqu’il faut stocker 3 fois les états de l’objet. Pour reprendre mon exemple avec le tableau de 5 ints, à un moment donné, on stocke en RAM :

    • les 5 ints du T rhs depuis lequel on copy-assign

    • les 5 ints que le T contient AVANT la copy-assignation

    • les 5 ints du T temporaire que l’on vient de construire

    • Alors qu’en pratique, seuls les deux premiers sont indispensables.

  • si on veut conserver la strong exception guarantee, ce trade-off est inévitable :

    The reason is that to offer strong exception guarantee, there is no way around it.
    There must be a temporary copy done first that we can simply delete if something goes wrong without touching the existing collection.

  • L’article propose une implémentation alternative pour remédier à ces problèmes, mais d’une part elle est bien bien plus complexe, et d’autre part elle n’est pas générique (elle dépend du conteneur sous-jacent).

[POST] No Kings: How Do You Make Good Decisions Efficiently in a Flat Organization?

Lu le 2020-12-22, pas de date de publication dans l’article, mais probablement publié le 2019-05-?? d’après le code-source de la page, sur le blog de doist.com, une boîte vendant des apps orientées productivité.

  • l’article est une discussion autour d’une RFC de l’IETF très intéressante exposant leur process de prise de décision. Les présentes notes annotent les deux ressources.

  • TL;DR :

    • les compromis sont pas toujours bons (tractations, capitulation, …​)

    • il y a deux types de désaccord : bloquant ou "je peux vivre avec", à traiter différemment (ça fait

    • c’est ok d’avancer avec une solution qui ne satisfait pas tout le monde, tant que l’insatisfaction est "j’aurais pas fait ça mais je peux vivre avec"

    • Coming to consensus by looking for objections, (plutôt que de voter, p.ex.)

    • tracking open issues, (pour savoir si oui ou non il y a consensus)

    • using hums as the start of discussions (technique rigolote, mais n’a pas une valeur mirobolante à mes yeux)

  • quelques citations de l’article :

    • “Not the best choice” versus fundamental flaws feedback

    • Once everyone can live with a given solution, you’ve reached rough consensus, even if there are outstanding objections.

  • Face à une proposition, il y a deux types de désaccods :

    • bloquant (e.g. il y a un défaut fatal dans le design)

    • "j’aurais pas fait comme ça mais je peux vivre avec"

  • c’est ok d’avancer avec une solution qui ne satisfait pas tout le monde, tant que l’insatisfaction est "j’aurais pas fait ça mais je peux vivre avec"

  • Les compromis ne sont pas toujours une bonne chose :

    • tractations = j’accepte tes remarques si tu acceptes les miennes

    • capitulation = j’abandonne la défense de mes idées par flemme ou fatigue

  • NdM : mon interprétation = deux types de "granularité", quand on recherche le consensus :

    • il faut ABSOLUMENT qu’on atteigne le degré maximal de qualité

    • il faut avancer, et le fait que la solution retenue n’est pas optimale n’est pas critique

  • quelques citations de la RFC :

    • Any finding of rough consensus needs, at some level, to provide a reasoned explanation to the person(s) raising the issue of why their concern is not going to be accommodated.

    • A good outcome is for the objector to understand the decision taken and accept the outcome, even though their particular issue is not being accommodated in the final product.

    • we come to consensus by looking at the open issues and not counting heads (aka pas de vote)

    • One hundred people for and five people against might not be rough consensus […​] If there is a minority of folks who have a valid technical objection, that objection must be dealt with before consensus can be declared. It’s the existence of the unaddressed open issue, not the number of people, which is determinative in judging consensus.

    • Coming to consensus is not the goal in itself. Coming to consensus is what we do during our processes to arrive at the best solution. In particular, "declaring" consensus is not an end goal. Attempts to declare consensus at the end of a discussion just for the sake of being able to say that there is consensus often get us back into the voting mentality that we’re trying to avoid.

    • Five people for and one hundred people against might still be rough consensus.

  • technique du humming = prendre la température de la pièce (par opposition au vote) ; sert surtout à choisir comment commencer la discussion :

    • Sometimes, the hum will make it clear that choice "foo" has a significant amount more support than choice "bar", and it is therefore likely easier to start the discussion by saying, "OK, 'foo' seems to have quite a bit of support. Let’s have the people that think 'foo' is a bad idea come up and tell us why it is problematic." […​] All that the hum does is give the chair a starting point

    • The advantage of the hum (par rapport au vote) is that it makes it perfectly clear that the chair is simply figuring out the direction of the conversation.

  • Ce process n’est pas sans inconvénient : When we decide that a discussion is too factious and opt to simply go with a majority, it creates more polarized arguments in the future

[VIDEO] CppCon 2017: Carl Cook “When a Microsecond Is an Eternity: High Performance Trading Systems in C++”

Vue le 2020-12-??, publiée le 2017-10-08 sur Cppcon = The C+++ Conference

  • TL;DR = une revue du mindset à avoir + quelques techniques pour le high-frequency trading

  • ISO SG14 = the GameDev & low latency ISO C++ working group

  • Electronic market making :

    A market maker (MM) is a firm or individual who actively quotes two-sided markets in a security, providing bids and offers (known as asks) along with the market size of each.

    + For instance, a market maker in XYZ stock may provide a quote of $10.00-$10.05, 100x500. This means that they bid (they will buy) 100 shares for $10.00 and also offer (they will sell) 500 shares at $10.05. Other market participants may then buy (lift the offer) from the MM at $10.05 or sell to them (hit the bid) at $10.00. Market makers provide liquidity and depth to markets and profit from the difference in the bid-ask spread.

  • 07:00 avoir une petite stdev est plus important qu’améliorer la médiane

  • 10:00 hyperthreading = plutôt négatif car moins de cache dispo, donc plus de latence

  • 12:00 exemples de modifs de code qui améliorent la latence

  • 20:00 supprimer une branche (branchless)

  • 32:00 inline = ne sert qu’à dire "ne râle pas s’il y a plusieurs définitions de cette fonction". Pour réellement inliner, il faut plutôt utiliser les attributs non-standards de gcc/clang

  • 48:00 profiling (=regarder ce que fait le code) est différent de benchmarking (=regarder combien de temps met le code pour s‹exécuter). Une fois qu’on a amélioré le profiling, il faut toujours remesurer le benchmarking pour vérifier qu’on a bien amélioré le temps d’exécution.

  • 50:00 comment benchmarker des systèmes aussi précis qui s’exécutent sur qqs centaines de nanosecondes ? Avec un Switch externe.

[VIDEO] KEYNOTE: What Everyone Should Know About How Amazing Compilers Are - Matt Godbolt C++ on Sea 2019

Vue le 2020-09-01, publié le 2019-02-15 sur C++ on sea = conférence C++

  • TL;DR = une revue d’optimisations chouettes des compilos, et quelques guidelines pour tirer parti au mieux de leurs optimisations

  • Compiler awesome at math :

    • 20:05 si j’essaye d’être futé À TORT, le compilo est capable de s’en rendre compte et de me corriger

    • 21:05 il vaut mieux essayer d’être explicite sur l’intention que j’ai, pour que le compilo puisse trouver la meilleure façon de le faire

    • 21:20 trust the compiler to do the right thing, don’t try to be clever

  • Compiler awesome at vectorization :

    • 30:20 version lisible de la vectorization effectuée par le compilateur

    • 31:05 le même algo implémenté "correctement" (i.e. de façon idiomatique)

    • 33:10 le compilo N’EST PAS CAPABLE (sur x86-64, en tout cas) de vectoriser la somme des carrés d’un vector de char → il vaut mieux rester à des ints !

    • 34:25 vu par un processeur, l’addition de flottants n’est pas commutative ou associative ! (alors que l’addition d’entiers l’est). C’est à cause de la précision relative des flottants : la précision d’un flottant dépend de sa valeur (cf. https://fabiensanglard.net/floating_point_visually_explained/)

  • Compiler awesome at control-flow

  • Compiler awesome at architectural tricks :

    • 41:30 clang reconnaît qu’on essaye de compter les bits, et utilise l’instruction dédiée

    • 44:30 idem pour le fait de changer d’endianness

    • 45:00 comparaison futée pour savoir si un caractère appartient à un jeu donné

  • Compilers slightly less awesome at reading minds :

    • 49:20 si on utilise une fonction qui n’est pas visible par le compilo (e.g. pas dans l’unite de compilation), il ne pourra pas optimiser

    • 50:30 on peut tout de même donner de l’info au compilo via pour dire que la fonction est pure → il retrouve la possibilité d’opitmiser

    • 51:40 en plus de la vtable, les fonctions virtuelles EMPÊCHENT les compilos de savoir quels sont les effets des fonctions, et donc de les optimiser (car une fonction virtuelle peut…​ faire n’importe quoi)

    • 53:20 wow, inline virtual function ! "au cas où" la fonction appelée est bien la fonction qui m’intéresse, on l’optimise !!

    • 55:00 le compilo peut pas optimiser, car il peut pas vérifier que mTotal n’overlappe pas avec le vector lu. Juste changer le type suffit à aider.

  • 58:00 conclusion :

    • compilers are cleverer than we are + assembly isn’t THAT scary

    • trust your compiler

    • don’t compromise readability

    • attention à l’aliasing (si le compilo n’est pas capable de prouver l’absence d’aliasing, il ne pourra pas optimiser)

    • attention à la visibilité des fonctions (il faut que le compilo puisse inspecter pour optimiser)

[POST] But I was helping the compiler!

Lu le 2020-08-28, publié le 2020-08-16 sur le blog de Pankaj SARATHY, un dev C++ / python / embarqué (an electrical power engineer turned software developer)

  • TL;DR : ne pas faire de move explicite quand la NRVO se débrouille très bien toute seule

  • Je note deux analogies que j’aime bien car très "visuelles" :

    • un document papier que détient un collègue, sur lequel je dois travailler :

      • passage par copie = j’en fais une photocopie, et il garde l’original

      • (NdM) passage par référence = je le lui emprunte pour travailler, et le lui rend quand j’ai fini

      • move = quand il a définitivement fini de travailler avec, il me le donne

    • j’ai une bouteille que je veux remplir, et c’est quelqu’un d’autre qui a le robinet :

      • pas de (N)RVO : avec son robinet, il remplit une bouteille "temporaire", que je transvase plus tard dans ma bouteille

      • avec (N)RVO : je lui donne ma bouteille, qu’il peut remplir avec son robinet

      • la bouteille est la zone mémoire destinée à accueillir l’objet

[POST] The Urban Legend of the 10X Developer

Lu le 2020-08-??, sur http://codefol.io/ , blog d’un dev anonyme (surtout ruby)

  • l’article a un point de vue intéressant sur le mythe du dev 10x

  • pas de recherche et de donnée formelle sur le sujet

  • sujet difficile à quantifier de toutes façons

  • lien avec la façon dont l’organisation soutient le dev : A lot of stories of 10X developers have their roots in “well supported by the company” situations.

  • point de vue pragmatique (que j’incline à partager) sur la rareté des dev 10x :

    That’s not to say that “anybody could be one.” I think actual “solid, ordinary” developers who can do good work on many different types of projects are rare and underrated. But they’re not magic unicorns. They’re about as rare as good plumbers, good mechanics or good doctors. You wouldn’t expect to find one every time you hire a professional. But you’d also expect to be able to find one with some time, work and patience. They may already be booked solid, of course.

[COURS] Programmation dynamique

Lu le 2020-07-28, c’est pas très clair quand le cours a été publié. Fait partie d’un cours d’algorithmique à Supinfo, présenté par Laurent GODEFROY, enseignant là-bas.

  • présentation propre de la programmation dynamique, avec notamment deux très bons exemples (rendu de monnaie et sac-à-dos)

  • fait écho au cours d’Erik DEMAINE annoté plus bas

  • conditions d’application de la programmation dynamique :

    • problème découpable en sous-problèmes discrets

    • le problème a une optimal substructure : la combinaison de solutions optimales à des sous-problèmes doit donner naissance à une solution optimale au problème global

    • NdM : j’ajoute "les sous-problèmes se recouvrent" (sans quoi inutile de faire de la prog dynamique, on peut faire un classique divide-and-conquer)

  • programmation dynamique =

    • expression du problème sous forme d’une relation de récurrence ← c’est la partie difficile

    • condition d’arrêt

    • memoization

  • inconvénients de l’approche bottom-up = on peut se retrouver à calculer des valeurs intermédiaires inutiles (elles ne nous servent pas pour la solution)

  • inconvénients de l’approche top-down = on peut se retrouver à faire une trop grosse récursion, et à exploser la callstack (en revanche, on ne calcule que ce qui sert réelement)

  • la partie difficile est d’exprimer le problème sous forme d’une relation de récurrence. Par exemple celle pour le sac-à-dos est issue de ces considérations :

    • Les objets ont un poids wi et une valeur vi.

    • on récurse sur l’indice i de l’objet parmi les N objets (en partant de la fin du tableau des objets).

    • la donnée pertinente est V[i][w] = le valeur maximale qu’on peut transporter dans un sac de capacité w, en ne considérant que les i premiers objets. Elle est issue de la combinaison optimale des i premiers objets dans le tableau (ce sont les objets "restants", vu qu’on a commencé à la fin du tableau)

    • notamment, la relation de récurrence indique que lorsqu’on traite l’objet i, on retient le MAX entre :

      • vi + V[i-1][w-wi] = la valeur optimale si ON METS l’objet i dans le sac

      • V[i-1][w] = la valeur optimale si ON NE METS PAS l’objet i dans le sac

    • en quelque sorte, ce max "choisit" si on mets ou non l’objet i dans le sac, en supposant connue la façon optimale d’agencer les i-1 objets précédents dans un sac (de poids w ou w-wi).

    • et c’est ce qu’on veut au plus haut niveau : V[N][W] choisit si on mets le dernier objet (d’indice N) dans le sac de poids W, en supposant connue la meillere façon de mettre les N-1 objets dans un sac de capacité W (si on ne retient pas l’objet N) ou de capacité W-wn (si on retient l’objet N)

  • à noter qu’il est plus simple de commencer par exprimer la relation de récurrence et l’algo en supposant que ce qui nous intéressent c’est la VALEUR recherchée, et pas la façon dont elle est construite :

    • dans le cadre du rendu de monnaie, commencer par se limiter à rechercher le nombre de pièces minimal

    • dans le cadre du sac à dos, commencer par se limiter à rechercher la valeur maximale

    • dans le cadre de Bellman-Ford, commencer par rechercher le poids du plus court chemin

  • complexité pour le problème du sac-à-dos :

    • à noter que lorsque la complexité algorithmique dépend d’une VALEUR plutôt que d’une TAILLE, on l’exprime sous forme du nombre de bits de sa représentation, i.e. complexité_VALEUR = log2(VALEUR)

    • ici, l’approche bottom-up avec deux boucles imbriquées montre que la complexité est en N.WN est le nombre d’objets, et W la capacité du sac-à-dos

    • MAIS comme la capacité est une valeur, on utilise son nombre de bits : W = 2 ^ log2(W) = 2 ^ complexité_W, et la complexité de l’algo est en fait exponentielle en la taille de W

[VIDEO] Lecture 1: Algorithmic Thinking, Peak Finding

Visionnée le 2020-07-08, cours publiée le 2013-01-13 mais semble mur la chaîne MIT OpenCourseWare (mais semble plutôt correspondre à un cours présenté en 2011) , présenté par Srini DEVADAS, professeur au MIT. La vidéo fait partie de la série de cours Introduction to Algorithms.

oveview

  • 16:15 définition du problème 1D

  • 18:43 algo naïf en O(n) = parcours linéaire du tableau

  • 24:40 algo efficace en O(logn), détaillé ci-dessous

  • 33:35 étude de la complexité 1D

  • 36:15 définition du problème 2D

  • 37:20 algo naïf en O(n²) = greedy ascent

  • 45:00 algo efficace…​ mais incorrect !

  • 47:00 algo efficace et correct divide-and-conquer en O(m x logn), détaillé ci-dessous

  • 51:20 étude de la complexité 2D

objectif = trouver un peak

  • définition d’un peak ⛰ = une cellule supérieure ou égale à ses voisines

  • la définition reste vraie sur un bord, une cellule peut être un peak même si elle a moins de voisines que les autres cellules

  • en 2D, on parle d’une 4-connexité : les voisines sont les 4 cellules au nord, sud, est et ouest

algo proposé en 1D

  • 1. on prend la cellule au milieu du tableau, cellule pivot P, on regarde son voisin de gauche et son voisin de droite :

              ? P ?          
    • si les deux voisins sont inférieurs, on a trouvé notre peak \o/

    • si les deux voisins sont supérieurs, on jette une moitié au hasard (y compris la cellule pivot), et on garde l’autre moitié

    • si seul l’un des voisins est supérieur, on jette toutes les cellules de la moitié DU CÔTÉ INFÉRIEUR (y compris la cellule pivot), et on garde l’autre moitié

  • 2. on recommence à l’étape 1 avec ce nouveau sous-tableau :

      ? P ?    
  • 3. si on n’a pas arrêté avant, quand il ne reste plus qu’une cellule dans le sous-tableau, c’est forcément un peak

Pourquoi l’algo 1D fonctionne

Ça repose sur la relation entre le MAX local à un sous-tableau, et le peak ⛰.

  • constat n°1 = tout sous-tableau du tableau 1D donné en entrée contient une cellule MAX sur le sous-tableau (il peut y en avoir plusieurs en cas d’égalité, ça ne change rien)

  • constat n°2 = quel que soit le sous-tableau extrait du tableau donné en entrée, tout MAX du sous-tableau est forcément un peak recherché, À CONDITION qu’il ne soit pas sur un bord du sous-tableau

    • considérons le sous-tableau suivant :

                               
    • toute cellule MAX du sous-tableau est (par définition) supérieure ou égale à ses deux voisines, à condition que celles-ci soient aussi dans le sous-tableau. Dans ce cas, le MAX est un peak.

    • et cette condition est vérifiée si la cellule MAX n’est pas au bord du sous-tableau. Ci-dessous, si le MAX est l’une des cellules vertes, c’est un peak :

          ? ?          
    • si le sous-tableau est collé au bord de son tableau parent, vue la définition du peak sur le bord, la cellule de bord du tableau sera également un peak si c’est un MAX : la seule cellule litigieuse qui reste est celle sur le bord du sous-tableau, et au MILIEU du tableau parent :

      ?              
  • si le MAX du sous-tableau est sur la cellule orange ci-dessus, on ne peut rien dire en l’état :

    • il se peut que ce ne soit pas un peak, si sa voisine de droite lui est supérieure :

      3 8            
    • mais il se peut que ce soit un peak, si sa voisine de droite lui est inférieure :

      8 3            
    • constat n°3 = dit autrement, tout MAX d’un sous-tableau quelconque est forcément un peak recherché si et seulement si la dernière cellule du sous-tableau est supérieure à sa première voisine en dehors du sous-tableau :

      GROS petit            
  • ainsi, en choisissant le sous-tableau de sorte que sa dernière cellule soit supérieure à sa voisine hors du sous-tableau, trouver le max global d’un sous-tableau quelconque permet de trouver un peak du tableau complet donné en entrée

  • à partir de ces constats, l’idée de l’algo va être de choisir des sous-tableaux de plus en plus petits, par rapport à une cellule pivot :

    • lorsqu’on évalue la cellule pivot, pour garantir la propriété nécessaire, on choisit de conserver le sous-tableau (gauche ou droite) de sorte que la cellule pivot (qui sera donc la voisine de la cellule extrême du sous-tableau) soit INFÉRIEURE à sa voisine dans le sous-tableau

    • ainsi, à chaque étape, on garantit que le MAX du sous-tableau retenu sera bien un PEAK du tableau 1D donné en entrée

    • si on ne s’est pas arrêté avant, lorsque notre sous-tableau n’a plus qu’une seule cellule, c’est forcément son maximum global, et donc le peak recherché

    • CQFD :-)

algo proposé en 2D

  • 1. on prend la colonne au milieu du tableau, colonne pivot P :

                P            
                P            
                P            
                P            
  • 2. on la parcourt entièrement pour trouver sa cellule maximale ↑

                             
                           
                             
                             
  • 3. on regarde les voisins de gauche et de droite de la cellule maximale ↑ :

                             
                           
                             
                             
    • si les deux voisins sont inférieurs, on a trouvé notre peak \o/

    • si les deux voisins sont supérieurs, on jette une moitié des colonnes au hasard (y compris la colonne pivot), et on garde l’autre moitié des colonnes

    • si seul l’un des voisins est supérieur, on jette toutes les colonnes de la moitié DU CÔTÉ INFÉRIEUR (y compris la colonne pivot), et on garde l’autre moitié des colonnes

  • 4. on recommence à l’étape 1 avec ce nouveau sous-tableau :

          P    
          P    
          P    
          P    
  • 5. si on n’a pas arrêté avant, quand il ne reste plus qu’une colonne, son max est forcément un peak

Pourquoi l’algo 2D fonctionne

  • pour les mêmes raisons qu’en 1D : on construit à chaque étape un sous-ensemble (un subset de colonnes) tel que tout MAX sur ce sous-ensemble est aussi un peak de la matrice 2D complète

  • comme précédemment, presque tout MAX sur le sous-ensemble est en fait DÉJÀ un peak de la matrice 2D complète :

    • c’est le cas À COUP SÛR si le MAX n’est pas sur la colonne adjacente à la colonne pivot

    • c’est PEUT-ÊTRE le cas si le MAX est sur la colonne A, adjacente à la colonne pivot

    • pour que ce soit le cas dans cette dernière situation, il faut que toute cellule MAX sur la colonne A soit supérieure à sa voisine sur la colonne pivot

  • rechercher la plus grande cellule de la colonne pivot, et choisir de garder les colonnes du côté supérieur à celle-ci garantit que cette propriété est vraie :

    • en effet, par définition, la plus grande cellule de la colonne pivot est supériere à toutes les autres cellules de la colonne pivot :

                  <            
                             
                  <            
                  <            
    • et comme on ne garde que les colonnes du côté où la voisine (marquée > ci-dessous) est plus grande que la plus grande cellule de la colonne pivot, toutes les cellules de la colonne pivot lui sont inférieures :

                  <            
                > <            
                  <            
                  <            
    • …​ et cette voisine sera elle-même inférieure ou égale au MAX (noté M ci-dessous) du subset de colonnes (rappel : on s’intéresse au cas où ce MAX est située sur la colonne adjacente à la colonne pivot). Donc par transitivité, en construisant le subset de colonnes tel que décrit, même s’il est situé sur la "mauvaise" colonne, le MAX M sera forcément supérieur a sa voisine sur la colonne pivot :

                <              
                <              
                <              
                M <            
    • donc le MAX M de cette colonne adjacente est également un peak ⛰ de la matrice 2D complète, CQFD

  • dans ce qui précède, attention à ne pas confondre :

    • le peak ⛰ (qui porte sur toute la matrice 2D initiale) = une cellule supérieure à ses 4 voisines, ce qu’on recherche

    • la plus grande cellule de la colonne pivot (qui porte juste sur les cellules de la colonne pivot)

    • le MAX du subset des colonnes (qui porte juste sur une partie des colonnes de la matrice 2D initiale)

greedy algo en 2D

  • on trouvera forcément un peak local…​

  • …​si on n’a pas de pot, on parcourera tout le tableau ou presque avant de le trouver → O(N*M)

[ARTICLE] I’m not feeling the async pressure

Lu le 2020-07-07, publié le 2020-01-01 par Armin RONACHER, co-leader de pocoo, un groupe de dev open-source bossant sur des projets comme sphinx, flask, werkzeug, ou encore pygments.

  • point de vocabulaire = confusion (qui semble assumée) entre back pressure et back pressure management :

    • back pressure = resistance that opposes the flow of data through a system

    • back pressure management = moyen de faire en sorte que la back pressure ne pose pas problème

    • dans l’article (et ailleurs), on peut lire des choses comme this library doesn’t have back pressure, mais il faut lire this library doesn’t have back pressure MANAGEMENT

  • exemple pris = la gestion des bagages dans un aéroport :

    • quand on veut faire voyager des bagages, on les mets (= produits) dans un container

    • lorsqu’un container est plein, il est alors chargé (= consommé) dans un avion

    • backpressure = quid si de nouveaux bagages arrivent alors qu’on n’a plus de containers de disponibles à charger ?

  • les 3 stratégies possibles (cf. les notes précédentes ci-dessous) :

    • buffering = on garde le bagage de côté, et on attend qu’un nouveau container vide arrive

    • dropping = on brûle discrètement le bagage en trop sur le côté de l’aéroport

    • control the producer = on avertit l’aéroport de ne plus accepter de nouveau bagage

  • pourquoi l’async a changé les choses ? quelle différence avec le code synchrone (multi-threadé) qu’on utilisait avant pour faire de l’IO bloquant ?

    • exemple donné avec un echo server

    • en asyncio : le serveur accepte toutes les connexions, y compris quand il ne pourra pas les traiter : mais si le write buffer est plein, la lib va bufferiser indéfiniment

    • en synchrone : lorsque le pool de threads capable d’accepter une connexion est vide (tous les threads sont occupés), la connexion va être mise en attente / refusée

  • à noter qu’on peut très bien accepter plus que ce qu’on peut traiter, pour être sûr d’avoir toujours de quoi traiter : si on n’a que 50 connexions BDD possibles (ou 50 threads dans le pool), on peut accpeter 200 requêtes (4 x plus), une partie va attendre un peu, mais les threads/connexions seront exploitées à fond

  • la "bonne" façon de faire selon l’auteur :

    • le service doit être capable de connaître son état : "prêt à traiter" ou "surchargé, je ne traiterai pas une prochaine requête"

    • si une requête arrive alors que le service est surchargé, on retourne 503 (éventuellement, en indiquant dans combien de temps réessayer avec le header retry-after)

    • en gros : plutôt que d’essayer de répondre à toute requête qu’on nous passe (et c’est niveau OS que ça va bloquer), on faile early si on voit qu’on est surchargé

  • cas du streaming :

    • ce qui est exposé ci-dessus marche bien pour des patterns de type request→response, mais pour des patterns de type stream c’est plus compliqué

    • normalement, il y a du control-flow intégré dans TCP, mais en pratique, des mécanismes de flow-control custom sont souvent implémentés par dessus. Par exemple, en HTTP2, plusieurs streams peuvent être multiplexés sur une seule connexion TCP, d’où le besoin d’un mécanisme custom de flow-control.

    • MAIS le fait que le mécanisme de flow-control de TCP soit plutôt invisible (en effet, il n’est pas accessible via l’API socket) est DANGEREUX : le dev PEUT faire comme si c’était transparent pour lui, alor qu’il FAUT qu’il prenne en compte le cas où il y a de la backpressure : lorsqu’on implémente un protocole de streaming, il FAUT qu’il soit bidirectionnel : du client vers le serveur pour envoyer les données ET du serveur vers le client pour réguler la vitesse

    • et ça c’est pas trivial du tout !

  • le problème (ne pas gérer la backpressure) est commun à plein de monde : go, rust, aiohttp, etc.

[ARTICLE] Backpressure explained — the resisted flow of data through software

Lu le 2020-07-07, publié le 2019-02-01 sur la page medium de Jay PHELPS, dev google, ancien dev Netflix

  • backpressure = résistance au flow

  • cas typique = un producteur de message, et un consommateur de message, la backpressure apparaît lorsque le producteur produit plus vite que le consommateur ne consomme

  • 3 stratégies pour y faire face :

    • buffering = on accumule les messages en trop dans un buffer, en espérant pouvoir les dépiler lorsque le pic de charge sera passé. inconvénient = attention à ce que le buffer ne grossisse pas indéfiniment + quid si le buffer est plein ?

    • dropping = on droppe les messages en trop. inconvénient = on perd des messages.

    • control the producer (flow control) = on avertit le producteur qu’il va trop vite, et qu’il doit ralentir. La meilleure solution si elle est disponible. inconvénient = pas toujours réalisable + peut-être compliquée à implémenter.

  • exemple (tiré de cet autre excellent article) : la gestion des bagages dans un aéroport : quand on veut faire voyager des bagages, on les mets (= produits) dans un container. Lorsqu’ils sont pleins, chaque container est alors chargé (= consommé) dans un avion. Quid si de nouveaux bagages arrivent alors qu’on n’a plus de containers de disponibles à charger ?

    • buffering = on garde le bagage de côté, et on attend qu’un nouveau container vide arrive

    • dropping = on brûle discrètement le bagage en trop sur le côté de l’aéroport

    • control the producer = on avertit l’aéroport de ne plus accepter de nouveau bagage

[ARTICLE] CRC vs hash functions

Lu le 2020-06-26, publié le 2016-06-12 sur le blog d’Evan KLITZKE ex-dev über + dev bitcoin core

  • CRC et hash functions semblent similaires : à partir d’une entrée quelconque, ils produisent une sortie "réduite" (checksum pour CRC, digest pour hash function), typiquement de 32 à 512 bits

  • objectif de CRC = détecter les erreurs de transmission :

    • mathématiquement, les 32bits-CRC de deux messages différents seront obligatoirement inégaux si la différence de message est < 32 bits, quel que soit le message.

    • (ils seront sans doute inégaux même pour des différences plus importantes)

    • Mais même si les CRC sont inégaux, ils peuvent être très similaires, et on s’en fiche : l’important c’est qu’on puisse dire "si les CRC sont différents, le message a été altéré"

  • objectif de hash = ne pas être biaisé en fonction de l’entrée :

    • deux messages différents mais très proches doivent produire des digest aussi dissemblables que deux messages différents et très éloignés

    • dit autrement : étant donné deux digests différents, on ne doit pas être capables de dire si les messages initiaux étaient proches ou non (à la différence des CRC)

    • une autre façon de voir ça : si on change un seul bit sur un message d’entrée, chaque bit de son digest doit avoir une chance sur deux d’être modifié

[ARTICLE] How TCP Sockets Work

Lu le 2020-06-25, publié le 2017-01-27 sur le blog d’Evan KLITZKE ex-dev über + dev bitcoin core

  • TL;DR : explications de haut-niveau sur la stack TCP/IP Linux

  • quand un paquet arrive, le kernel est soit notifié (interrupt), soit polle le NIC (= network interface) pour savoir qu’il y a un nouveau paquet

  • le paquet est alors décodé, et attribué à une connexion TCP à partir de ip+port de source/destination

  • son payload est copié dans le receive buffer de la socket, puis "réveille" un éventuel read/select qui bloquait jusqu’ici

  • en userland, le process peut alors copier le contenu du receive buffer dans le buffer en userland (c’est ce que fait read, cf. man 2 read) → le receive buffer en kernelspace est vidé par cette opération

  • conséquence = si on appelle read trop rarement, le receive buffer peut grossir démesurément. Pour éviter ça, le kernel limite la taille du receive buffer…​ qui peut donc finir par être plein si on read trop rarement !

  • en résumé, quand on appelle read :

    • si le receive buffer est vide, read bloque jusqu’à ce qu’on ait des données

    • si le receive buffer n’est pas vide, read retourne en copiant les données du receive buffer dans le userland buffer (éventuellement, partiellement si on n’en avait pas assez reçu)

    • si le receive buffer est plein, tout envoi de paquet sur la socket sera refusé par la pile TCP/IP (ACK ne sera pas envoyé). C’est ue partie de la TCP congestion control , déjà évoqué dans l’article What every developer should know about TCP

  • (l’article détaille également le fonctionnement de write, je ne le reproduis pas ici)

  • c’est le même principe pour write (je ne détaille pas ici), ainsi que pour une listen-socket (chargée de spawner d’autres sockets en réponse aux tentatives de connexion par des clients) : si elle n'`accept` pas assez vite, le kernel va refuser les nouvelles connexions.

  • le mécanisme est donc similaire dans les 3 cas : read / write / accept, je l’illustre avec read :

    • on a une queue = le receive buffer

    • on a un producteur = les paquets reçus par la stack TCP/IP (resp. envoyés, ou les demandes de connexions)

    • on a un consommateur = les appels à read (resp. write / accept) pour vider la queue

  • si le consommateur ne consomme pas assez vite, le kernel bloque (refuse de recevoir/envoyer de nouveaux paquets, ou bien refuse les nouvelles connexions)

[ARTICLE] The Tail at Scale

Lu le 2020-05-21, publié le 2013-20-?? sur le site présentant de google dédié à la recherche

  • TL;DR : article assez varié présentant les causes de latences dans le traitement des requêtes, et tout un tas de pistes pour y être robuste. Un point important : inutile de chercher à être fault-free : mieux vaut être fault-tolerant.

  • objectif = répondre en moins de 100 ms (quelques dizaines de ms pour le service de suggest du moteur de recherche de google)

  • même de rares augmentations de la latence dégradent l’ensemble des requêtes : plutôt que de viser à un système sans latence, il faut concevoir un système pour répondre rapidement même en présence de latence occasionnelle : latency tail-tolerant

  • causes de latence "individuelle" (i.e. sans prendre en compte le fait qu’une requête est un agrégat complexe d’agents et de sous-requêtes) :

    • compétition pour des ressources partagées localement : temps CPU, cache, memory bandwidth, network bandwidth, …​

    • daemons : peu consommateur en moyenne, mais lorsqu’ils se déclenchent, peuvent consommer des ressources en burst

    • compétition pour des ressources partagées globalement : network switches, shared filesystems

    • maintenance automatiques : e.g. passage du garbage collector d’un runtime (e.g. java)

    • queuing : passage obligé dans une queue potentiellement déjà chargée

    • hardware power limit : throttling automatique si le CPU chauffe trop

    • hardware garbage collection : pour les SSD, il y a un GC hardware qui multiplie la latence par 100

    • hardware energy management : latence nécessaire pour sortir d’un mode "économie d’énergie"

  • même si on répartit les sous-requêtes sur différents sous-systèmes, la queue de la distribution va être limitante :

    • leur approche est de regarder le 99ième percentile de temps de réponse (d’où le "tail")

    • si les services répondent en 10 ms mais que le 99ième percentile répond en une seconde, une requête sur cent sera longue

    • sur un service qui requête 100 sous-serveurs en parallèle, 63% des requêtes prendra plus d’une seconde (1 - 0.99^100)

    • même si seule 1/10000 requête est lente, si on a besoin de 2000 sous-requêtes, alors 1 requêtes sur 5 (0.18 = 1 - 0.9999^2000) prendra plus d’une seconde

  • comment diminuer cette latency-tail pour un composant donné ?

    • prioriser les éléments d’une queue qui sont destinés à servir une requête qu’un utilisateur final attend (par opposition aux requêtes où c’est pas très grave si ça prend ponctuellement du temps, par exemple pour des tâches automatiques)

    • autoriser la préemption des requêtes, pour éviter qu’une seule requête très lente bloque toutes celles derrière elle (en effet, celles-ci pourront préempter la requête lente au bout d’un moment)

    • limiter l’impact des activités en tâche de fond (e.g. en ne les lançant que lorsque l’activité est faible)

    • note : le caching est hors de propos ici, puisqu’il n’adresse pas le problème de la queue de la distribution (car les requêtes responsables de la queue de la latency-distribution ne sont pas cachées)

  • étant donné qu’on ne pourra de toutes façons pas supprimer la latency-tail, comment réduire la sensibilité à celle-ci ?

    • hedged requests :

      • profiter du fait que les serveurs soient répliqués en envoyant N fois la même requête en parallèle à différent serveur, en gardant la première réponse (et en discardant les suivantes)

      • pour ne pas surcharger le système inutilement, plutôt que de faire ça systématiquement, on ne le fait que lorsque la première requête met un peu de temps à répondre

      • en n’augmentant le volume des requêtes que de 2%, ils arrivent à réduire la latence du 99.9 percentile de 1800 ms à 74 ms !

    • tied requests :

      • proglème des hedged requêtes = on est coincés entre Charybde (sursolliciter les serveurs de façon inutile) et Scylla (devoir attendre avant de déclencher les requêtes supplémentaires).

      • l’une des causes principales des variabilités de latences est le temps de queuing des serveurs : une fois la requête en cours en cours de traitement par le serveur, la variabilité n’est pas énorme.

      • du coup solution simple = le load balancer tient compte de l’encombrement des queues pour choisir le serveur

      • solution alternative = enqueuer plusieurs requêtes en parallèle dans plusieurs serveurs, et leur permettre de communiquer : quand un serveur commence à traiter une requête, il transmet aux autres serveurs un message d’annulation de leur requête équivalente.

      • encore une autre alternative = avant de faire une requête à un serveur, on le probe pour savoir s’il est occupé. Cette solution créée d’autres problèmes : l’occupation du serveur peut augmenter entre la probe et la requête, il peut-être difficile à un serveur de savoir s’il est occupé, et ça peut occasionner un pic de charge sur un serveur considéré comme le moins occupé.

  • en temps normal, on essaye de partitionner le problème uniformément entre les ressources permettant de le résoudre. En pratique, d’une part les ressources ne répondent pas toutes de façon uniforme, et d’autre part une portion du problème peut prendre de l’importance après le partitionnement (e.g. si une recherche google se met à être à la mode). Pistes :

    • micro-partition : si on a 10 serveurs, au lieu de partionner le problème en 10 morceaux, on le partitionne en 100, et chaque serveur en traite 10. Si l’une des micro-partitions (on peut plus facilement redispatcher les micros-partitions si nécessaires)

    • selective replication : répliquer dynamiquement les morceaux qui sont cause de surcharge, pour les faire traiter par plus de serveurs. Deux exemples :

      • sur 24h, en fonction des fuseaux horaires, la répartition des langues des requêtes change avec l’avancée des heures → on adapte les documents servis en répliquant les langues les plus populaires à une heure dite

      • si un data-center en Asie est down, on réplique dynamiquement les documents de langues asiatiques sur un serveur nord-américain pour répondre aux requêtes

    • latency-induced probation : on sort temporairement du flux un serveur qui semble occupé, par exemple par un autre job sur le serveur (paradoxalement, c’est donc en réduisant les ressources qu’on améliore la latence moyenne)

  • dans les information retrieval systems , c’est plus important de renvoyer un bon résultat rapidement que de renvoyer le meilleur résultat lentement :

    • good enough : de temps en temps, on n’attend pas que 100% des leaf-servers aient répondu, on se permet de répondre si une fraction suffisamment grande a déjà répondu, en supposant qu’il y a peu de chances que les réponses manquantes améliorent la réponse globale

    • canary requests : un risque est qu’une requête particulière fasse emprunter un chemin de code buggé, qui fait planter TOUS les leaf servers d’un coup. Pour éviter ça, on envoie d’abord la requête à 1 ou 2 leaf-servers, et seulement s’ils répondent correctement, on envoie la requête à tout le monde.

  • mutations : la latence sur les requêtes de mutation est plus simple à gérer :

    • souvent les attentes sont moindres

    • les mutations peuvent être effectuées après avoir répondu à l’utilisateur, donc sans se presser

    • les services nécessitant des mutations peuvent être structurés pour être plus latency-tolerant

    • lorsqu’on cherche à muter, souvent on utilise un algo (genre Lamport-Paxos) pour recueillir un consensus, et on n’a pas besoin de la queue de la distribution

[ARTICLE] Response Times: The 3 Important Limits

Lu le 2020-05-20, publié le 1993-01-01 (mis à jour en 2014 : l’article reste d’actualité) par Jakob NIELSEN, un spécialiste de l’UX sur le site du Nielsen Norman Group, supposément "World Leaders in Research-Based User Experience".

  • 3 temps de réponses pertinents :

    • < 100 ms = le système semble répondre instantanément, l’utilisateur a l’impression d’agir directement sur les données

    • < 1 seconde = l’utilisateur perd l’impression d’agir directement sur les données, mais le système n’interrompt pas le "flow of thoughts" de l’utilisateur

    • < 10 secondes = le système interrompt le "flow of thoughts", mais est suffisamment réactif pour qu’on n’ait pas envie d’aller faire autre chose pendant qu’il mouline

    • > 10 secondes = l’utilisateur va aller faire autre chose pendant que le système mouline → il faut lui donner un indicateur de "quand la tâche sera finie" (e.g. un indcateur de pourcentage restant, ou spinner)

  • un peu plus de temps : https://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/

[POST] Dismissing Python Garbage Collection at Instagram

Lu le 2020-05-20, publié le 2017-01-17 sur le blog tech d’instagram

  • sur un serveur instagram = django avec un process master qui forke pour spawner des douzaines de sous-process

  • lorsqu’un sous-process démarre, le RSS (resident set size) monte vite à 250 Mio, mais la fraction de la mémoire "partagée par les autres process" redescend vite à 140 Mio (ce qui montre que ~90 Mio sont devenus "propres au process forké" plutôt que "partagé avec le parent")

  • COW = copy-on-write = les sous-process partagent leurs memory-frames avec leur process parent, jusqu’à ce que celle-ci soit modifié par l’un ou l’autre

  • mais en python : même une lecture de variable modifie la memory frame (pour incrémenter le refcount) du coup, à la moindre lecture, le COW se déclenche (c’est en fait un …​ COR = copy-on-read)

  • ils essayent de profiler d’abord, en monitorant les page-fault (vu que le mécanisme de COW fait un page-fault pour copier la memory frame) → surprise, c’est en fait le garbage collector qui génère le plus de page fault

  • gc.disable() ne marche pas car une lib externe appelle gc.enable(), du coup ils ont utilisé gc.set_threshold(0)

  • la désactivation du GC évite de trigger les COW, du coup la part de mémoire partagée entre le process master et ses fork remonte de 140 Mio à 225 Mio \o/

  • MAIS désactiver le GC présente un effet de bord : redémarrer leurs process sur le serveur devient d’un seul coup très lent (merci au continuous deployement pour l’avoir détecté) :

    • avant de s’arrêter, l’interpréteur python fait un dernier gc.collect (qui n’est pas bypassé par gc.set_threshold(0))

    • du coup TOUTES les COW des processus fils se déclenchent en même temps, augmentant fortement la consommation de RAM d’un seul coup → il n’y a plus de RAM libre, et le page-cache se vide

    • du coup quand le process redémarre, au moment de recharger en RAM toutes les pages disques du processus, elles NE SONT PLUS dans le page cache, il faut les relire depuis le disque dur, ce qui est très lent

  • pour éviter ça, ils bypassent le process de finalization de python (l’idée est : de toute façons, le process s’arrête → inutile de cleanup ou d’appeler gc)

  • question : disabler le GC n’est-il pas problématique ? Réponse : non, car le GC n’est là que pour briser les références cycliques, le mécanisme principal de désallocation est lorsque le refcount tombe à zéro.

  • bilan = 8Gio de RAM en moins consommée, mais surtout : amélioration de la vitesse d’exécution (mesurée en IPC = instruction CPU per cycle) :

    • en effet, à nombre de process identique, il y a moins de pages mémoire différentes existantes (vu qu’on a augmenté le partage des pages mémoires entre les process, en déclenchant moins souvent le COW)

    • et comme on a moins de pages mémoires différentes à code identique, on aura moins de cache-miss

    • or chaque cache-miss force le CPU à attendre, du coup diminuer les cache-miss implique qu’on augmente l’IPC \o/

[GIST] Latency numbers every programmer should know

  • résumé des ordres de grandeur des différentes latences

  • notamment :

    • L2 cache ~ 10x plus lent que L1 cache

    • main memory ~ 100x plus lent que L1 cache

    • disk seek+read ~ 10.000.000x plus lent que L1 cache

  • les représentations visuelles et "humaines" sont top

[POST] What every developer should know about TCP

Lu le 2020-05-15, publié le 2020-05-10 par Roberto Vitillo, dev Microsoft, ancien dev Mozilla

  • RTT = round-trip time, qui dépend de la latency

  • TL;DR : latency et bandwidth ne sont pas indépendants. Plusieurs causes :

    • les handshakes TCP et TLS nécessitent plusieur RT → le moment où on pourra envoyer le premier paquet dépend de la latency

    • cold start = le sender maintient une congestion window , le temps qu’elle prend pour augmenter (et donc pour que la bandwidth augmente) dépend du RTT, donc de la latency

    • congestion control = le sender adapte ses envois de paquets en fonction du receive buffer du receiver → le temps pris pour revenir à la normale après un timeout dépend du RTT, donc de la latency

  • réutiliser les connexions déjà ouvertes est une façon de mitiger les deux premiers points

[POST] Invariants and Preconditions

Lu le 2020-05-07, publié le 2020-03-05 par Anthony WILLIAMS sur https://www.justsoftwaresolutions.co.uk/ qui semble être le site vitrine de consltants.

  • invariant = doit rester valable pour TOUTES les instances de l’objet.

    • y compris après un move, qui laisse l’objet dans un état "emptier than empty"

    • y compris avant un init, si des constructeurs défèrent la construction finale avec un init

  • si les invariants sont vrais tout le temps, sauf dans ces cas…​ c’est que ce ne sont pas des invariants !

  • dans le cas d’un init, plutôt que d’appeler ces "faux-invariants" des invariants, il est plus juste de considérer que TOUTES les méthodes de la classe SAUF init ont une précondition (qui est qu'`init` ait été appelé)

  • équivalent dans le cas du move : toutes les méthodes de la classe ont comme précondition que l’instance n’ait pas été move-ée.

  • de base, c’est ok que les méthodes de la classe brisent les invariants temporairement (par exemple, au cours d’un appel de méthode), tant que ceux-ci restent vrais avant et après l’appel de méthode.

  • mais dans ce cas attention au multithreading : si l’état de l’instance est visible par un thread B pendant qu’un thread A est dans une méthode qui brise "temporairement" l’invariant → le thread B a accès à une instance pour laquelle les invariants sont faux !

  • et ça peut arriver même si chaque ligne respecte les invariants : la thread-safety n’est pas composable

[VIDEO] Systematic error handling in C++, aussi sur youtube

Vue le 2020-04-27, publiée par Andrei ALEXANDRESCU, C legend, à l'occasion de https://cppandbeyond.com/[C and beyond 2012], une conf organisée par Scott MEYERS, Herbe SUTTER et Andrei ALEXANDRESCU.

  • contexte = error handling :

    • error handling is about bad DATA (e.g. bad inputs), not bad STATE → it’s not about bugs

    • exemple de situation qui n’est PAS de l’error handling = ram défecteuse, programme incorrect, …​

    • exemple de situation qui est de l’error handling = plus d’espace disque, on a demandé à l’utilisateur un entier, et il a entré toto

  • présentation de Expected (malheureusement toujours pas standard à l’heure où j’écris ces lignes), un peu l’équivalent des Maybe d’Haskell

  • Expected<T> = contient soit T, soit l’exception qui a empêché d’avoir T

  • l’essentiel du talk présente l’implémentation de Expected comme union de T et std::exception_ptr

  • le reste du talk concerne ScopedGuard11, une intéressante forme de RAII (simplifiant la composabilité) : le principe reste du RAII : exécuter du code arbitraire (lambda) à la destruction, MAIS ça permet également d’annuler le code avec sg.dismiss()

  • pour voir l’intérêt dans le cadre de la gestion d’erreur, cf. l’exemple de la vidéo. On cherche à composer deux tâches action et next (qui peuvent échouer et raise une exception), en sachant d’une part que si action réussit, elle va nécessiter du cleanup, et d’autre part que action et next doivent réussir toutes les deux ou échouer toutes les deux (transaction) : si next échoue, il faut donc rollback ce qu’a fait action

  • façon "classique" avec RAII :

    class RAII {
    RAII() { action(); }
    ~RAII() { cleanup(); }
    }
    
    RAII raii;
    try {
        next();
    } catch (...) {
        rollback();
        throw;
    }
  • le problème de ce qui précède, c’est la composabilité : si next est à son tour une transaction de second_action et second_next, le code devient horrible à cause des nested try-catch.

  • les ScopedGuard simplifient le problème :

    action();
    auto sg1 = ScopeGuard([](){ cleanup() });  // en fin de scope, on cleanup
    auto sg2 = ScopeGuard([](){ rollback() });  // en fin de scope, on rollback
    next();
    sg2.dismiss();  // si on arrive ici, next a réussi -> on annule le rollback
    // fin du scope -> on va cleanup
  • et on peut vérifier que même si on next est une transaction de second_action et second_next, le code reste simple

[VIDEO] Understanding the Python GIL, voir aussi le post qui va avec

Vue le 2020-04-24, publiée à l’occasion de la PyCON 2010 le 2010-02-20 par David BEAZLEY, speaker et dev python très influent.

  • attention, talk de 2010, deprecated (mais intéressant tout de même), il parle de python < 3.2

  • présentation d’un comportement curieux, avec un calcul CPU-bound :

    • monothread : 5s

    • 2 threads sur deux cores : 10s

    • 2 threads sur un seul core : 8s

    • 2 threads sur deux cores avec un process fils qui mouline en plus : 7s

  • GIL = un seul thread avance a chaque instant.

  • ancien modèle du GIL :

    • GIL relâché lors des io AINSI QUE lors du "check" (si un compteur de 100 ticks=instruction de la VM arrive a zéro), pour eviter qu’un thread cpubound ne monopolise le GIL

    • Lors du check, c’est l’os qui choisit quel thread va tourner : ça peut très bien rester celui qui tournait juste avant le check

    • Ce qu’on veut éviter c’est que l’os réveille un thread à tort : le thread essaye d’acquerir le GIL sans succès puis se rendort.

    • Quand on a autant de cores que de thread, c’est EXACTEMENT ce qui se passe, du coup, BEAUCOUP de travail supplémentaire de l’os pour rien, qui empêche le thread "en cours" d’avancer, d’où les mauvaises perfs du cas 2 ci-dessus.

  • Nouveau GIL en python 3.2 développé par Antoine PITROU

    • on n’a plus de ticks pour empêcher les threads CPU-bounds de monopoliser le GIL

    • à la place, on a une variable globale, un thread CPU-bound tourne jusque a ce que cette variable soit mise à 1

    • pas hyper clair, mais il semblerait qu’à chaque instruction, le thread checke si la var est à 1, et si oui, relâche le GIL ?!

    • un thread tourne donc indéfiniment, tant que le GIL ne lui est pas réclamé (ou, bien sûr, tant qu’il ne fait pas d’io)

    • si un deuxième thread arrive, il commence par attendre un peu (par défaut 5 ms) voir si le premier thread relâche le GIL de lui même, puis met la variable globale à 1, ce qui force le premier thread à relâcher le GIL.

    • et pour éviter que l’os ne le refasse tourner immédiatement, le thread qui vient de relâcher le GIL sleep un peu.

  • défauts de ce modèle :

    • tous les threads (notamment les threads importants ou qui doivent faire de l’io) doivent purger les 5 ms avant d’agir…​ manque de responsiveness

    • si beaucoup de threads, rien ne dit que c’est le thread qui a réveillé le GIL qui va être exécuté par l’os, il peut starve

  • À noter que les io ne bloquent pas nécessairement : write bufferisé donc IO retardée, ou bien lecture depuis le page cache

  • Du coup, un thread qui fait beaucoup d’io va être TRÈS concurrencé par un autre thread cpu-bound, qui va lui piquer le GIL (et le garder! au moins le temps du timeout) à chaque io, même si cette io n’aurait pas bloqué

  • ce qui manque au nouveau GIL :

    • pouvoir prioriser les threads (e.g. certains threads vont rendre le GIL très vite)

    • possibilité de preempter : les threads importants (e.g. qui répondent à une requête réseau) devraient pouvoir préempter

  • certains OS ont un mécanisme de priorisation pas mal :

    • si un thread a rendu la main sans être préempté, il gagne en priorité

    • à l’inverse, si un thread a dû être préempté, il perd en priorité

[POST] A successful deployment model

Lu le 2020-04-13, publié le 2019-08-02 par Thomas VILHENA dev web.

  • Selon lui, les règles pour limiter les risques liés au déploiement :

    • Use the same deployable image for test, staging and production environments

    • Update systems without downtime

    • Fully automate the deployment process

    • Set up and rely on automatic monitoring for early problem detection (splitté en health monitoring et error monitoring)

    • Support rollback to earlier application versions

[POST] Systems design for Advanced Beginners

Lu le 2020-04-06, publié le 2020-04-06 par Robert HEATON, dev sécurité à Stripe, société de paiement en ligne

  • une revue d’assez haut de system design pour une application web. Quelques points intéressants en vrac :

    • webhooks = endpoints chez les clients qu’on appelle quand on veut les avertir de quelque chose (e.g. gitlab peut appeler un webhook lorsqu’il se passe un évènement intéressant, comme un push)

    • database sharding + comment migrer

    • database replication (asynchrone vs. synchrone)

    • elasticssearch pour le full text search

    • pubsub

[POST] Our journey to type checking 4 million lines of Python

Lu le 2020-04-01, publié le 2019-09-05 sur le blog tech de Dropbox, utilisateur massif de pythonn par Jukka LEHTOSALO, auteur initial et maintenant lead dev de mypy.

  • L’intéressante histoire de mypy racontée par son créateur.

  • On suit l’outil depuis ses débuts sur un langage de recherche (Alore) jusqu’à python, en passant par la rencontre avec Guido VAN ROSSUM, la standardisation du type-hinting l’adoption massive au sein de Dropbox, et les résolutions des problèmes liées aux performances.

  • Au final, au sein de Dropbox, 4 millions de LOC sont type-checkées.

  • Un REX intéressant est la façon dont ils ont atteint ce chiffre, en cumulant plusieurs stratégies :

    • forcer les type-annotations pour les nouveaux fichiers de code

    • produire toutes les semaines un rapport sur la couverture de code

    • sensibilisation des équipes

    • prendre le retour des utilisateurs

    • améliorer les perfs pour faciliter l’adoption

    • ajouter des outils pour les IDE populaires

    • outils d’analyse statique

    • stub-files pour des librairies tierces

  • L’une des difficultés a été la gestion des imports cycliques.

[VIDEO] 19. Dynamic Programming I: Fibonacci, Shortest Paths

Visionnée le 2020-04-01, publiée le 2013-01-14 sur la chaîne MIT OpenCourseWare, présenté par Erik DEMAINE, qui a l’air d’être une star (entre autre : licence à 14 ans, professeur au MIT à 20 ans). La vidéo fait partie de la série de cours Introduction to Algorithms.

Principe

  • programmation dynamique (= dynamic programming = DP) = explorer exhaustivement et récursivement toutes les solutions + memoization

  • exemple très didactique qui sert de fil-rouge : calcul du n-ième terme de la suite de Fibonacci

  • la DP est utile lorsqu’on cherche à résoudre un problème d’optimisation : trouver le min, le max, le "plus court", etc.

  • principe = découper le problème en sous-problèmes qui aident à résoudre le problème principal → l’un des challenges de la DP c’est d’identifier les sous-problèmes

  • les sous-problèmes peuvent être d’une nature DIFFÉRENTE du problème initial (même si ce n’est pas le cas pour le fil rouge, où les sous-problèmes sont identiques au problème principal)

  • memoization = lorsqu’on a déjà résolu l’un des sous-problèmes, on n’a plus besoin de le refaire (tiens, j’apprends l’origine du terme : "memoize something" c’est "le transformer en memo")

  • terminologie : à l’époque, le terme "programmation" signifie "ordonnancement" → DP = ordonnancement dynamique

Approche top-down vs. bottom-up

Deux façons d’approcher un problème en programmation dynamique :

  • TOP-DOWN : on part du problème final, et on le décompose récursivement en les sous-problèmes. Cette approche correspond au problème, mais il faut réfléchir un peu pour savoir ce qui est memoizé.

    • Exemple du fil rouge : quand on visualise l’arbre binaire des Fn, on part du top (le calcul de F(n)) et on descend, en calculant les termes suivants (F(n-1), F(n-2)) pour finir par les racines (F0, F1) :

      fibonacci binary tree topdown
      Figure 1. Approche top-down
      def fib(n: int) -> int:
          if n == 0:
              return 0
          if n == 1:
              return 1
          if n not in memo:
              # on part de fib(n) et on "descend" l'arbre vers fib(n-1) et fib(n-2) :
              memo[n] = fib(n-2) + fib(n-1)
          return memo[n]
  • BOTTOM-UP : en partant de zéro, on construit ce dont on aura besoin, en terminant par le problème final. Exemple du fil rouge : quand on visualise l’arbre binaire des Fn, on calcule successivemnt tous les termes en partant du bas de l’arbre (F(0), F(1), …​) pour finir par exprimer la solution au problème final en utilisant les éléments calculés jusque-là.

    • Dans le diagramme suivant, seuls les noeuds coloriés en rose sont effectivement calculés et mémoizés : les autres noeuds ont déjà été calclés, et sont donc simplement récupérés dans le mémo.

      fibonacci binary tree bottomup
      Figure 2. Approche bottom-up
      def fib(n: int) -> int:
          memo = dict()
          # on itère sur tous les sous-problèmes en commençant par le "bottom" de l'arbre
          for i in range(n):
              if i == 0:
                  memo[i] = 0
              elif i == 1:
                  memo[i] = 1
              else:
                  memo[i] = memo[i-2] + memo[i-1]
          # le problème final s'exprime naturellement en fonction des sous-problèmes résolus jusqu'ici :
          return memo[n-2] + memo[n-1]
    • à noter que l’approche bottom-up est un tri topologique du DAG des sous-problèmes. Pour le fil rouge de Fibonacci, le DAG est simplement chaque Fn qui dépend de Fn-1 et Fn-2 :

      dependencies dag
      Figure 3. DAG des dépendances pour Fibonacci
    • par ailleurs, l’approche bottom-up peut parfois permettre d’être plus efficace en espace (e.g. avec le fil rouge fib, dans l’approche bottom-up, on pourrait se contenter de garder les deux dernières valeurs de fib, et jeter les autres)

Reste des notes

  • autre exemple donné avec le calcul d’un plus court chemin dans un graphe : l’approche par programmation dynamique aboutit à l’algorithme de Bellman-Ford

  • page wikipedia sur la programmation dynamique = trois catégorisation d’un problème en fonction des sous-problèmes :

    • doesn’t have optimal substructure : on ne peut pas résoudre un problème en résolvant ses sous-problèmes. Exemple = le prix d’un billet d’avion Paris→Heathrow→New-York N’EST PAS la somme du prix de Paris→Heathrow et de Heathrow→New-York.

    • has optimal substructure, et les sous-problèmes sont indépendants : on peut résoudre ces problèmes par une approche divide and conquer. Exemple = merge sort.

    • has optimal substructure, et les sous-problèmes se recouvrent : on peut résoudre ces problèmes par une approche de programmation dynamique. Exemple = calcul du n-ième terme de la suite de Fibonacci, descente d’une pyramide de nombre maximisant la somme.

  • complexité algorithmique en DP = nombre de sous-problèmes * complexité de chaque sous-problème

    • exemple pour fib : chaque sous-problème a un temps constant (vu que c’est la somme de deux entiers déjà calculés)

    • il y en a N (pour calculer fib(n), il vaut avoir calculé les N-1 fib)

    • -→ complexité de l’algo DP pour calculer fib = linéaire

  • pour que la DP soit possible : les dépendances des sous-problèmes doivent être un DAG : s’il y a un cycle, il n’y aura pas d’ordre (tri topologique) selon lequel résoudre les sous-problèmes.

    • une astuce futée pour les calculs dans les graphes (alors même que le graphe lui-même est cyclique !) c’est de les représenter comme évoluant avec le temps. Ainsi, le graphe cyclique suivant :

      from cyclic graph
      Figure 4. Graphe cyclique
    • Pourra être représenté par une série de graphes successifs évoluant avec le temps, ce qui brise les cycles :

      to acyclic graph
      Figure 5. Le même graphe rendu acyclique

[ARTICLE] Safe Systems Programming in Rust:The Promise and the Challenge

Lu le 2020-03-??, publié le 2020-??-?? (article en cours de soumission) par Robbert KREBBERS assistant professor in the programming languages group at the department of software technology at Delft University of Technology, ainsi que Ralf JUNG, Jacques-Henri JOURDAN, et Derek DREYER.

  • très bon article (très détaillé) sur rust et son borrow checker

  • quelques mots qui ne lui rendent pas justice : pourquoi rust est safe ?

    • Interdit d’avoir de l’aliasing (i.e. deux pointeurs différents qui pointent vers la même zone mémoire) à moins qu’un seul des pointeurs aie les droits d’écriture

    • Borrow checker = seule une référence à la fois a le droit de muter (donc éventuellement détruire ou invalider) un objet

    • Dit autrement, une référence peut autoriser l’aliasing ou la mutabilité mais pas les deux en même temps

[POST] My Coding Interview Style

Lu le 2020-03-11, publié le 2017-12-04 par Amy NGUYEN, dev d’API de paiement à Stripe, société de paiement en ligne

  • Une revue du sprocess qu’elle suit à chaque fois qu’elle passe un coding interview.

  • L’article est court mais concret, ne pas hésiter à le relire.

[VIDEO] P vs NP et le zoo de complexité informatique

Visionnée le 2020-03-10, publié le 2014-08-26 par hackerdashery, un blog tech ?

  • Différentes classes de problèmes :

    • problèmes de classe P = étant donné un problème, on dispose d’un algo pour le résoudre "facilement", i.e. en trouver la solution.

      • Exemple concret = trouver le plus court chemin dans un graphe

    • problèmes de classe NP = étant donnée une solution supposée, on sait dire "facilement" si c’est bien une solution ou pas.

      • Exemple concret = si tu me donnes comme problème une grille de départ (incomplète) de Sudoku, et comme solution supposée la même grille remplie, je sais dire facilement si la grille remplie est bien une solution valide de la grille de départ. Pour autant, je n’ai pas d’algo efficace pour trouver une solution à la grille de départ.

    • problème non-NP = étant donnée une solution supposée, on ne sait même pas dire "facilement" si c’est bien une solution ou pas.

      • Exemple concret = si tu me donnes comme problème une situation de jeu d’échecs donnée où il faut que je trouve le meilleur prochain coup, et comme solution supposée un coup X, je ne peux même dire facilement si X est bien le meilleur prochain coup ou non.

    • on sait résoudre "facilement" signifie on peut trouver une solution en un nombre de steps polynomial par rapport à la "taille" du problème

  • question : est-ce que P == NP ? C’est l’un des 7 problèmes du prix du millénaire, on conjecture sans pouvoir le prouver que P != NP

  • à noter que NP contient P : en effet, si on sait déjà trouver la solution à un problème facilement, on saura aussi évaluer si une proposition donnée en est une solution (il suffit de trouver la solution, et de la comparer à la proposition)

  • ce qui nous intéresse, c’est le pire cas, lorsqu’on fait grossir la "taille" du problème :

    • (NP) résoudre un petit sudoku est facile vs. (P) multiplier deux petits nombres est facile

    • (NP) résoudre un très grand sudoku est impossible vs. (P) multiplier deux très grands nombre est certes moins trivial, mais reste facile

    • dit autrement : comment la difficulté du problème évolue lorsque la "taille" du problème augmente ?

      • "taille" pour la multiplication = p.ex. nombre de digits dans les nombres

      • "taille" pour le sudoku = p.ex. largeur de la grille

  • il y a BEAUCOUP de classes de complexité :

    • lorsqu’on nous donne une proposition de solution, on ne sait même pas dire si elle est bonne (non-NP, du coup)

    • peut être facile en temps mais pas en espace, et vice versa

    • peut être exponentiel, probabiliste, dépendre d’un ordinateur quantique, etc.

  • un point rigolo : la crypto repose sur le fait que P != NP (en effet, étant donné une clé, on sait dire si c’est la clé qui a servi à chiffrer le message ou pas → NP, mais on ne sait pas trouver facilement la clé → pas P)

  • Si P == NP, ça veut dire que "le fait d’être capable de RECONNAÎTRE une solution à un problème signifie qu’on est aussi capable de la TROUVER à partir de rien)

  • Exemples de problèmes NP-difficiles = voyageur de commerce, problème du sac-à-dos, etc.

En pratique, les informaticiens et les développeurs sont souvent confrontés à des problèmes NP-complets.

Dans ce cas, savoir que le problème sur lequel on travaille est NP-complet est une indication du fait que le problème est difficile à résoudre, donc qu’il vaut mieux chercher des solutions approchées en utilisant des algorithmes d’approximation ou utiliser des heuristiques pour trouver des solutions exactes.

[ARTICLE] A brief introduction to C++’s model for type- and resource-safety

Lu le 2020-03-08, publié le 2015-12-?? par Bjarne STROUSTRUP (Morgan Stanley), Herb SUTTER (Microsoft), Gabriel DOS REIS (Microsoft aussi, a participé au dévelopemment des modules)

  • propositions pour plus de type-safety et resource-safety (= non-leaking resource management), contraintes = zero-overhead principle + rétrocompatible

    We say that a program is memory safe if every allocated object is deallocated (once only) and no access is done through a pointer (or reference, iterator, or other non-owning indirection) to an object that has been deleted or gone out of scope (and thus technically isn’t an object any more – just a bag of bits).

    To be type safe, we need memory safety so that an object cannot be accessed through a dangling pointer

    [...]

    Furthermore, to be perfectly type safe, a program must be free of range errors (access beyond the end of an array), free of access through the null pointer, etc.

  • TL;DR : suggestions =

    • type system avec une abstraction pour l’ownership

    • lib de support (GSL)

    • analyse statique pour enforce les rules

  • revue rapide des erreurs liées à la mémoire :

    • resource leak (= si un objet n’est pas détruit)

    • accesss through an invalid pointer

    • memory corruption (= on peut écrire des données d’un type T1 sur une zone mémoire qui est d’un type T2 → on corrompt T2)

    • confusion statique (pas besoin de delete) / dynamique (besoin de delete)

    • use after free / out of range access / null pointer

  • Non-retenu = modèle dynamique :

    • what = bit encodant l’ownership dans les LSB de l’adresse pointée par le pointeur

    • deux pointeurs "identiques" peuvent être owner ou non-owner :

      • si on a obtenu le pointeur par new, le pointeur est owner, sinon, le pointeur est non-owner

      • si un owner pointeur goes out of scope (ou est overwritten), on delete la zone mémoire

      • on peut se transmettre l’ownership

    • (du peu que j’en connais, ça ressemble au borrowing de rust ?)

    • non-retenu car :

      • augmente la taille mémoire du pointeur (ou bien utilise des bits "cachés" qui dépendent de l’alignement)

      • augmente la complexité de manipulation des adresses mémoires (e.g. arithmétique des pointeurs)

      • pas rétro-compatible

  • Retenu = modèle statique :

    • what = au lieu d’utiliser T*, on utiliser owner<T*> pour marquer l’ownership

    • pour rester ABI-compatible, owner<T*> est un alias vers T* (c’est ça qui est fourni par GSL)

    • ce marquage par owner NE FAIT RIEN, il permet surtout l’analyse statique

    • recommandation = quand c’est possible, utiliser plutôt les classes d’ownership (i.e. les resource-handlers) faîtes pour ça (e.g. vector, unique_ptr)

    • c’est pas rose non plus, il y a des limitations

[POST] A modern ‘Hello, World’ program needs more than just code

Lu le 2020-03-06, publié le 2020-03-05 par Charles R. MARTIN, sur le blog de StackOverflow

  • le point principal de l’article, c’est que Hello world ne sert pas à réussir à afficher une chaîne à l’écran, mais à bootstrapper un projet :

    • créer le code source dans un fichier quelque part

    • le compiler/linker

    • l’exécuter

    • trouver où il a produit sa sortie

  • de nos jours, un Hello world adapté est donc plutôt :

    • disposer du repo et savoir commiter/pusher

    • avoir choisi son IDE/ses outils

    • savoir builder le process

[ARTICLE] Low-Cost Deterministic C++ Exceptions for Embedded Systems

Lu le 2020-03-04, publié le 2019-??-?? par James RENWICK, Tom SPINK et Björn FRANKE, chercheurs de l’université d’Edinburgh.

  • implémentation actuelle des exceptions = gratuit si pas de throw, mais coûteux si throw

  • mais surtout : gros volumes de binaires + imprédictibilité de l’utilisation des ressources

  • en embarqué :

    for use in embedded systems, where binary size and determinism are often as, if not more, important than overall execution time

  • suggestion = status (throw ou pas) stocké sur la stack, et le mécanisme d’exception maintient le statut

  • en assembleur, les fonctions retournent classiquement, puis on vérifie si le status est exceptionnel (et si oui, goto le catch handler)

  • le throw est équivalent à un set du status + return

  • à la différence de l’implémentation standard des exceptions, la proposition a un petit coût au runtime (même en l’absence de throw) à cause du check du status systématique après un call

[SITE] The Computer Language Benchmarks Game

Lu le 2020-02-26, publié le ????-??-?? par Debian

  • des résultats de benchmarks sur divers programmes (mandelbrot, binary-trees, digits de pi, etc.), systématiquement sourcés, pour les langages principaux

  • pour chaque langage, il y a des comparaisons avec d’autres langages, e.g. go vs C++

[VIDEO] L’API Management : au-delà des promesses

Vidéo vue le 2020-02-26, publié le 2020-02-03 par Adrien GRAUX & Daniel SABIN dans le cadre de la DuckConf, conférence tech d’OCTO

  • TL;DR : attention, tout n’est pas rose avec les API managers, surtout si on sort des cas bateaux

  • notamment pour la sécurité, on se retrouve à coder des choses soi-même

  • mais également pour le monitoring (ils se retrouve à brancher du ElasticSearch + kibana sur les logs de la gateway)

  • ou le portail développeur (ils se retrouvent à le recoder pour avoir qqch de différenciant)

  • point de vigilance = l’organisation des équipes et des modèles pour scaler et industrialiser la consommation d’API

  • organisation suggérée = squad API : une équipe transverse maintient le tool, et chaque équipe est autonome dans sa publication d’API

[POST] The Day The Standard Library Died

Lu le 2020-02-25, publié le 2020-02-24 sur https://cor3ntin.github.io/, le blog de Corentin JABOT, dev C++ bordelais.

  • TL;DR : un point de vue intéressant mais pessimiste sur la décision du comité C++ de ne pas casser l’ABI-compatibility dans un futur proche.

  • le comité à choisi de ne pas casser l’ABI du C dans C23, mais dans un futur non déterminé

  • pourtant, casser l’ABI a des avantages, parmi lesquels rendre les conteneurs associatifs plus efficaces.

  • mais surtout : le fait de NE PAS casser l’ABI a des inconvénients : lourd en terme de design, rend les futurs modules moins intéressants, empêche de meilleurs implémentations des exceptions, etc.

  • problème : si on refuse de le faire maintenant, rien ne dit que ce sera plus facile plus tard !

  • pose une question importante : What is C++ and what is the standard library?. Si on répond performance, zero-cost abstractions ou don’t pay for what you don’t use, on ne PEUT PAS répondre en même temps "ABI stability".

  • extrait : No you shouldn’t link against apt-installed c++ system libraries (which are intended for the system)

  • extrait : The estimated performance loss due to our unwillingness to break ABI is estimated to be 5-10% → du coup, pas mal d’initiatives pour shunter la lib standard : EASTL, folly, abseil, …​

  • parmi d’autres non annotées ici, une proposition intéressante (mais pas possible en pratique car ajoute une indirection + oblige la heap-allocation) est : One solution to some ABI issues could be to access the data of a type trough a pointer such that the layout of a type would only be that pointer. This corresponds roughly to the PIMPL idiom which is used extensively in Qt for ABI reasons.

  • extrait : Many believe that the committee could simply not make that decision because implementers would simply ignore the committee.

[POST] Advantages of monorepos

Lu le 2020-02-26, publié le 2009-07-19 par Dan LUU, qui fait de la vulgarisation informatique sur des sujets assez bas-niveaux

  • l’article liste les intérêts du monorepo (sans revenir particulièrement sur les inconvénients)

  • le plus gros avantage (qui revient quasiment pour tous les points, même s’ils sont censés adresser des questions différentes) : ça simplifie la gestion des dépendances :

    • With multiple repos, you need to have some way of specifying and versioning dependencies between them.

    • With a monorepo, it’s easy to have one universal version number for all projects.

    • Using a monorepo where HEAD always points to a consistent and valid version removes the problem of tracking multiple repo versions entirely.

  • l’organisation des fichiers / répertoires n’est plus dictée par les contraintes liées au fait d’avoir plusieurs repos : on organise les choses comme on veut.

  • tooling plus simple : analyse statique, tests d’intégration, grep du code, etc : tout ça est plus facile si tout est dans un seul repo.

  • les modifs qui auraient impacté plusieurs repos sont plus facile : with a monorepo, you just refactor the API and all of its callers in one commit.

  • analogie avec la transition [svn→git] :

    • svn (=un commit modifie un fichier) → git (=un commit modifie plusieurs fichiers)

    • monorepo (= un commit modifie un repo) → multirepo (= un commit modifie plusieurs repos)

  • modèle utilisé par des grands donc solide : Google, Facebook, Twitter, Digital Ocean, and Etsy

[SITE] C++ Frequently Questioned Answers

Lu le 2020-02-24, publié le 20??-??-?? par Yossi KREININ, dev plutôt bas-niveau (hardware / compilers) dans le domaine de la sécurité, et des voitures autonomes.

[POST] Fastest Way to Load Data Into PostgreSQL Using Python

Lu le 2020-02-24, publié le 2009-07-19 par Haki BENITA, pythonista intéressé par webdev, databases et perfs, auteur de quelques articles sur realpython

  • à retenir = pour peupler une DB postgres avec beaucoup de données, utiliser COPY FROM sur un fichier CSV (éventuellement, en RAM avec StringIO)

    If you are loading a freshly created table, the fastest method is to create the table, bulk load the table’s data using COPY, then create any indexes needed for the table.
  • tooling sympa (indépendant de la problématique de l’article) :

  • problématique = méthode la plus rapide + la moins consommatrice de RAM pour peupler une DB postgres avec beaucoup de données ?

  • très lent (~ 2 minutes) = insérer les données ligne par ligne est très lent

  • rapide (~ 2 à 4 secondes) = insérer en batch, cf. psycopg2 execute_batch / execute_values

  • très rapide (~ 0.5 secondes) = remplir un fichier CSV (en RAM avec StringIO), et utiliser un copy-from à partir de ça

  • et pour ne pas avoir à charger toutes les données en RAM, il créée un iterator custom sur ses données, qui présente l’interface d’un StringIO

[POST] Exceptions

Lu le 2020-02-22, publié le 2003-10-13 par Joël SPOLSKY, dev Microsoft sur Excel, co-créateur de stackoverflow avec Jeff ATWOOD, créateur de Trello, …​

  • Son avis sur les exceptions :

    • en pratique, ce sont des goto (i.e. jump vers un endroit arbitraire du code)

    • et même encore pire que goto : pas immédiatement visible dans le code-source + il y en a beaucoup au sein d’une même fonction

  • Sa politique :

    • ne jamais lancer d’exceptions

    • si on doit utiliser du code qui peut throw, catcher dès la ligne d’appel même si c’est verbeux

  • Le problème auquel répondent les exceptions = retourner DEUX return-values (la "vraie" return-value, et l’error-status) là où le langage n’en permet qu’un.

  • Il préfère retourner explicitement l’error-status (et donc passer un paramètre T& out en argument pour stocker la vraie return-value) même si c’est BEAUCOUP plus verbeux

[POST] Réussir la Developer eXperience de son API web

Lu le 2020-02-18, publié le 2020-02-18 par Octo

  • TL;DR : bonnes pratiques à suivre lorsqu’on ouvre ses APIs aux développeurs extérieurs

  • conception : faire rapidement des tests avec de vrais clients (éventuellement, POC-és)

  • TTFAC = time to first API call = est-ce compliqué de bootstraper ce qui faut pour appeler l’API ? (s’il faut se farcier une doc de 30 pages : oui !)

  • DX = Developer eXperience (à corréler à UX = User eXperience)

  • génération automatique de la doc : alternatives au très populaire swagger = API Blueprint et RAML.

  • points bonus : portail dev / sandbox / illustration (= exemples concrets) / SDK / assistance / communication

[POST] Designer une API REST

Lu le 2020-02-18, publié le 2014-12-01 par Octo

  • affordance = capacité d’une API à suggérer son utilisation, pour limiter le besoin de recourir à la doc

  • il ne doit y avoir qu’une seule façon de faire les choses

  • suggestion = limiter les domaines à 3 :

    • api.fakecompany.com = les appels à l’API

    • oauth2.fakecompany.com = récupération d’un token pour utiliser l’API

    • dev.fakecompany.com = portail develop de l’API

  • distinguer case de l’URL et case du contenu (et au passage, je connaissais pas le nom de spinal-case=lisp-case)

  • versioning = dans l’URL, assez tôt, et doit être explicitement passé par les clients (pas de default-version)

  • réponse partielle = précisesr dans l’URL les champs qui nous intéressent (NdM : et GraphQL alors ?!)

  • pagination = à prévoir dès le début : query params + headers Content-Range et Accept-Ranges

  • lien vers "le reste" = RFC5988 (NdM : HATEOAS) + exemple de comment github fait

  • combinaison de pagination, filtre, tri

  • recherche = ressource à part entière

  • exception (qui doit rester exceptionnelle !) à la règle ressource=nom plutôt que verbe → non-ressource API (= service) → verbe. (e.g. un service "convert")

  • erreur : renvoyer 1. short description 2. long description 3. URI vers la doc de l’erreur

[POST] How to Make Other Developers Hate to Work with You

Lu le 2020-02-18, publié le 2019-02-20 par Anaxi, tool de gestion de projet SAAS ?

  • focus sur les défauts des développeurs, classés du plus impactant au moins impactant.

  • arrogance : "as long as you take responsibility for and learn from your mistakes, you’re not a bad developer"

  • sloppiness in the work delivered : beaucoup de choses ici, mais en gros : ne pas prendre le temps de faire les choses bien

  • non-respect du temps des autres personnes : arriver en retard aux réunions, interrompre ses collègues, etc.

  • négativité : toujours râler et critiquer, de façon non-constructive

  • avarice : tirer la couverture à soi sur le travail réalisé

  • disregard for the team : ignorer la big picture et les responsabilités des autres membres de l’équipe

  • lack of focus : ignorer la big picture et se disperser

  • lack of accountability : chercher des excuses au lieu de chercher des solutions

[POST] Demystifying C++ lambdas

Lu le 2020-01-??, publié le 2014-03-07 par Glennan CARNIE, dev embarqué expérimenté

  • Quel est l’intérêt de std::function ?

  • Il existe plusieurs types de callables :

    1. pointeur de fonction

    2. foncteur (= classe implémentant operator() )

    3. pointeur de fonction membre

    4. lambda

    5. bind-expression

  • Comme ces objets sont différents, ils ont un type différent, et ça m’embête si je veux par exemple coder l’application d’un processor à tous les éléments d’un container de callables :

    void apply(Container& container, WhichTypeShouldIUse& processor) { ... }
  • Quel type utiliser à la place de WhichTypeShouldIUse ci-dessus ? std::function est conçu pour ça, et peut représenter tout type de callable :

    void apply(Container& container, std::function<void(int)>& processor) { ... }

[STACKOVERFLOW] Is int safe to read from multiple threads?

Lu le 2020-01-??, publié le 2011-09-28 par Adam ROSENFIELD, contributeur hyperactif de stackoverflow, dev amazon.

This makes volatile objects suitable for communication with a signal handler, but not with another thread of execution, see std::memory_order).

[POST] Motherboard Chipsets and the Memory Map

Lu le 2020-01-??, publié le 2008-06-04 par manybutfinite, blog tech

  • CPU communique avec le monde extérieur via ses pins

    In a motherboard the CPU’s gateway to the world is the front-side bus connecting it to the northbridge. Whenever the CPU needs to read or write memory it does so via this bus. It uses some pins to transmit the physical memory address it wants to write or read, while other pins send the value to be written or receive the value being read.

  • les adresses vues par le CPU sont divisées en portions, dont certaines ne mappent même pas vers la RAM, mais plutôt vers des memory-mapped IO devices

  • le CPU n’a pas connaissance des devices à l’autre bout des adresses : pour lui, ce ne sont que des adresses

  • c’est le rôle du Northbridge de mapper les requêtes (en lecture ou écriture) sur une adresse vers d’autres devices que la RAM

  • (les adresses qui mappent sur la RAM sont les adresses physiques (nous, on n’a accès qu’aux adresses logiques, c’est le TLB qui mappe une adresse logique à une adresse phyisque)

  • memory address map

    • associe une plage d’adresses physiques à sa destination : RAM / video card / autre memory-mapped IO device

    • pour la consulter : sudo cat /proc/iomem

    • il y a des "trous" dans les plages attribuées à la RAM, pour autre chose : BIOS / video card / carte de périphériques / carte PCI

[POST] Parse, don’t validate

Lu le 2019-11-??, publié le 2019-11-05 par Alexis KING, webdev spécialiste d’haskell

  • quel est le type de retour d’une fonction qui renvoie le premier élément d’une liste de T ?

    1. T : non, car si la liste est vide, on ne renvoie pas T

    2. Maybe T ? on renvoie Just x, ou Nothing si la liste est vide. Inconvénient = le client doit traiter le cas Nothing, même quand on est sûr que ça ne peut pas arriver.

    3. on modifie le type d’entrée de la fonction pour n’accepter que des listes NonEmpty

  • le truc cool : l’info la liste n’est pas vide est définie DANS LE TYPE : on a défini une précondition à la fonction, mais qui est vérifiable statiquement au compile time

  • différence parse vs. validate :

    • validate = on vérifie la condition à un moment donné, mais on n’en fait rien (plus loin dans le code, elle pourrait redevenir fausse)

    • parse = on vérifie la condition, et on stocke l’info dans un type contraint (le compilo s’assure donc qu’elle ne pourra jamais redevenir fausse)

  • mon exemple concret (pas dans l’article) :

    • situation n°1 = on représente une couleur avec un int :

      int parse(const InputFile& f)
      {
          int value = f.get_value();
          if (value != 0 || value != 1) { throw std::runtime_error("boum"); }
          return value;
      }
      int color = parse(input_file);
      // ... some stuff, maybe very long ...
      
      // should I check again that color is in [0,1] ?
      // if no, what happens if color is not in [0,1] anymore ?
      void do_something(Color) { /* something that relies on color being 0 or 1 */ }
      do_something(color);
    • situation n°2 = on représente une couleur avec un enum class Color :

      Color parse(const InputFile& f)
      {
          int value = f.get_value();
          if (value == 0) { return Color::RED; }
          else if (value == 1) { return Color::BLACK; }
          else { throw std::runtime_error("boum"); }
      }
      Color color = parse(input_file);
      // ... some stuff, maybe very long ...
      
      // no need to check again that color is in [0,1] : it's in the type !
      void do_something(Color) { /* something that relies on color being 0 or 1 */ }
      do_something(color);
  • dans la situation n°1, il faut re-valider quand on utilise la couleur (danger si on oublie, ou si le code a évolué dans parse et qu’on a oublié de mettre à jour do_something, etc.). En bref, le compilo NE RALERA PAS si on passe la valeur 42 à do_something.

  • dans la situation n°2, la validation a été faite une fois pour toute, et le type system s’assure que do_something n’utilisera jamais de valeur invalide

  • parser en amont et utiliser un type contraint (plutôt que valider plus tard) est intéressant, car une fois le parsing fait, on ne manipule plus que des types toujours corrects

  • intérêt du type statique contraint = comme c’est le type qui véhicule l’info, il n’est même pas POSSIBLE d’avoir des valeurs incorrectes

    The problem is that validation-based approaches make it extremely difficult or impossible to determine if everything was actually validated up front or if some of those so-called “impossible” cases might actually happen. Parsing avoids this problem by stratifying the program into two phases—parsing and execution—where failure due to invalid input can only happen in the first phase.

  • shotgun parsing = anti-pattern : le parsing/vérification de validité, est fait "tardivement" (voire au moment du processing), au lieu d’être faite une fois pour toute en amont

  • à retenir :

    • My advice: focus on the datatypes.

    • Use a data structure that makes illegal states unrepresentable

    • Push the burden of proof upward as far as possible (= parser au plus tôt les inputs en des types qui n’ont pas la possibilité de représenter des valeurs illégales)

[SITE] The Definitive Guide to API Management

Lu le 2018-07-??, c’est un ebook pour avoir un overview de ce que propose apigee.

  • fichier = apigee-ebook-api-mgmt-2015-07.pdf

  • L’outil d’Apigee est :

    • Apigee EDGE API management product

  • API management tool = une solution qui permet :

    • un portail pour développeurs : découvrir, explorer, acheter, tester, s’enregistrer pour utiliser des API

    • une passerelle d’API : sécuriser et gérer le traffic entre les clients et les backends, et plus généralement entre une API et ses utilisateurs

    • un gestionnaire de cycle de vie : gérer la conception, le développement, la publication, le déploiement, et le versioning des API

    • éventuellement, un outil d’analyse d’utilisation des API, orienté business

    • éventuellement, un outil de monetization pour packager, pricer et publier les APIs, et pour faire payer les clients

[POST] Présentation de Protocol Buffers

Lu le 2018-06-??, publié le 2017-09-20 sur le blog d’Eleven Labs, SSII.

  • Protobuf est un système de sérialisation de données (comme json ou XML) binaire.

    • +++ : language-agnostic : on décrit les données dans un fichier .proto, puis un outil (protoc) génère le code de (dé)sérialization pour le langage voulu.

    • +++ : très performant (aussi bien sur la taille de la donnée encodée, que sur la vitesse de (dé)sérialization)

    • --- : message en binaire plus dur à débugger que du json

    • --- : on a une couche de complexité (le fichier proto) en plus

  • (langage-agnostic utile dans une architecture micro-services où chaque service doit communiquer avec d’autres quel que soit le langage)

[POST] Dropbox starts using POST, and why this is poor API design

Lu le 2018-05-??, publié le 2015-03-02 par Evert Pot, un dev web avec un focus sur les APIs et HTTP.

  • Utiliser des requêtes GET pour développer des APIs peut-être compliqué :

    • limitation du volume de données qu’on peut transmettre dans une URL

    • mettre des données dans l’URL est moins flexible que dans le body (notamment : json ?)

  • Du coup, dropbox permet le POST là où avant on ne pouvait que le GET.

  • Problème avec POST = non-safe / non-idempotent → non-cachable (notamment par les proxies).

  • Solutions possibles :

    • Utiliser REPORT (safe + idempotent + body autorisé), verbe défini dans une extension WEBDAV à HTTP.

    • Utiliser GET avec un body : BAD car l’intérêt du GET (caching) est perdu + HTTP dit explicitement que le body n’a pas de sens.

    • (side-note : le gros intérêt de GET, c’est l’adressabilité → permettre de faire un simple lien vers une ressource est le top !)

    • Décorréler la requête (faite avec POST, donc avec body) et la récupération de la réponse (faite sur une autre URL, récupérée avec GET)

  • Plus de détail sur cette dernière solution :

    1. le client fait un POST sur "/queries" (en passant ce qu’il souhaite dans le body)

    2. le serveur répond à cette requête POST en indiquant dans le header "Content-Location" une URL gettable : p.ex. "/queries/42"

    3. le client fait un GET sur "/queries/42" pour récupérer sa réponse

[POST] Don’t Share Libraries among Microservices

Lu le 2018-05-??, publié le 2016-04-17 sur le blog de Philipp HAUER, dev java/kotlin.

  • Si des microservices utilisent la même librairie, ils sont couplés.

    • On va les livrer plus souvent, on va avoir plus de bugs.

    • De plus, on va naturellement mettre la librairie à jour moins souvent.

    • Et ça induit des problèmes de dépendances.

  • “Duplication is better than the wrong abstraction”

  • Pistes de solutions :

    • accepter d’avoir de la redondance pour rester indépendant

    • sortir la librairie dans un SERVICE partagé (plutôt qu’une lib partagée)

    • refactorer les microservices (ou leur architecture) pour ne plus avoir besoin de partager la librairie

  • contexte au travail : je fais le lien avec lbsserver/lbsdevtool, utilisées par routemm, et qu’on ne maintient jamais…​

[BBL] No estimates

Présentation le 2018-12-05, par Julien TOPÇU de la Société Générale

Tout un tas de notions vrac pour la culture générale

  • Tel ticket = notre référence-unité, on chiffre tous les autres par rapport à ça.

  • Le titre "No Estimates" n’est pas forcément pertinent, c’est plutôt une provocation : en effet, l’idée n’est pas de ne plus estimer les tâches, l’idée est plutôt de lutter contre la tendance qu’on a à tout driver par le chiffrage.

  • Vasco DUARTE = chantre du NoEstimates (https://twitter.com/duarte_vasco)

  • Kent BECK = fondateur de l’extreme programming)

  • Loi de Conway = le design reflète l’organisation de la structure.

  • Loi de Hofstadter = on utilise toujours tout le temps alloué, et même plus (https://fr.wikipedia.org/wiki/Loi_de_Hofstadter)

  • Distinguer deux types de complexité :

    • essentielle = dûe au métier, qui est complexe (impossible de la réduire sans modifier le métier)

    • accidentelle = dûe à d’autres choses (e.g. dette technique), qu’on peut réduire en faisant autrement

Le NoEstimates

  • Même en NoEstimates, le besoin reste le même = visibilité + aide à la décision

  • L’idée principale, c’est de calculer des métriques (cycle time, vélocité) en s’aidant du passé, puis de faire de l’analyse statistique dessus pour en déduire une probabilité raisonnable sur la réalisation d’un périmètre fonctionnel

  • Un point important (qui disqualifie probablement la méthode pour notre équipe), c’est la STABILITÉ de notre cycle-time et de nos métriques.

  • Cependant, je retiens un conseil réalisable en pratique :

    • on se fixe une taille de référence en pratique pour une tâche (e.g. 4 jours, dont 2 de dev, et le reste en review/release)

    • en sprint planning, l’objectif est de n’avoir des story QUE de cette taille de référence

    • les stories plus petites sont mergées pour atteindre 4

    • les stories plus grandes sont splittées pour atteindre 4

    • avantage = plus de visibilité sur les stories

    • inconvénient = pas forcément facile (et parfois long et coûteux) de découper pour atteindre la taille souhaitée