Tel que je comprends la doc, il n'y a que les handlers de requête qui sont parallélisés. Du coup à partir du moment où tout ce qui n'est pas handler utilise 100% de CPU, ça ne sert à rien d'ajouter davantage de parallélisme.
If you run across tuning advice that suggests running one thread pool for each CPU core, rest assured that this is old advice. Experiments and data from production environments have revealed that as long as you have two thread pools (which is the default), there is nothing to gain by increasing the number of thread pools.
Cela dit c'est complètement possible que j'aie raté des choses.
Vu que dans les programmes déjà existants Java était single-threadé et Rust multi-threadé la question se posait, et j'ai préféré prendre ce qui était le plus performant dans une configuration relativement standard.
Avec node.js en mode cluster on arrive à :
Requests/sec: 140514.83
Transfer/sec: 26.00MB
soit 4.5x la perf. monothreadée, donc un scaling quasi-linéaire (sur les 6 CPUs, 1 CPU était pris par wrk, et 0.7 par d'autres process).
Donc pour l'instant, AMHA, les generics ne servent pas à grand chose si ce n'est composer des interfaces.
Je dirais qu'ils servent surtout à gérer les conteneurs et les fonctions qui opèrent dessus en ignorant le contenu de façon simple, type-safe et sans codegen.
Par exemple, le package sort permet de trier un slice en utilisant la fonction https://pkg.go.dev/sort#Slice qui n'est pas vraiment ergonomique (je dirais même pas sûre).
Avec les génériques on pourrait avoir func Slice[T any](x []T, less func(xi, xj *T) bool) à la place.
Pour prendre un exemple plus compliqué, l'outil dataloaden permet de générer du code pour un pattern "dataloader", ie. permettre d'avoir une fonction fn(T): U dans l'interface publique, et d'avoir le traitement implémenté dans une fonction fn2(T[]): U[], qui est appelée beaucoup moins de fois que fn (et donc optimiser plus facilement le traitement). Aujourd'hui c'est fait avec de la codegen, et ce serait beaucoup plus ergonomique avec des génériques (compilation plus rapide que la codegen, pas besoin de déclarer la liste des variantes à générer, facile d'adapter le code, etc.)
De plus, ça force à écrire les fameuses callback dont parle l'auteur, mais avec une variante en utilisant la "arrow syntax" (qui ne propage pas le this dans les callback), qui est courte et élégante.
En quoi utiliser des fonctions avec l'arrow syntax est plus court et élégant que ne pas en utiliser du tout ?
Avec cette syntaxe, une autre chose que j'aime bien: on a pas besoin de déclarer ses variables (const toto) vu qu'on les récupère directement en paramètre des closures et on peut arriver à un stade où on peut considérer que déclarer des variables est une erreur de programmation
Absolument pas. Non seulement c'est possible de ne pas déclarer de variables temporaires avec async/await, mais surtout en déclarer n'est pas une erreur de programmation et rend le code plus clair.
Quand on écrit const xml = await res.text() ou const totozList = obj.totozes.totoz.length ? obj.totozes.totoz : [obj.totozes.totoz], il suffit de lire le nom de la variable pour savoir ce qui est retourné.
on rajoute des embranchements et la complexité cyclomatique et donc on augmente la probabilité d'avoir des bugs (certains analyseurs statiques d'ailleurs mettent ça en valeur).
Je ne vois absolument pas en quoi une structure de type
r1 = f1()
r2 = f2(r1)
r3 = f3(r2)
ajoute quoi que ce soit en complexité par rapport à f3(f2(f1())).
Par ailleurs, avec les promesses, on ne mélange pas les scopes, et donc on ne partage pas les variables: on réduit par la même occasion les chances d'écrasement ou d'utilisation de variable accidentels de par cette isolation
Dans l'exemple, tout est déclaré avec le mot-clé const, rien ne peut être écrasé. Par ailleurs rien n'empêche d'appliquer les bonnes pratiques comme donner des noms clairs aux variables et écrire des fonctions courtes en utilisant async/await.
Si vos données sont sur un SSD au format M.2, cas le plus favorable, vous en avez pour une petite journée (au moins 5h30) de recherche à la bibliothèque (plus de 0.02 ms, soit plus de 20 000 ns… et donc 80 000 instructions2).
Le cas le plus favorable, c'est un SSD NVMe, qui communique directement sur le bus PCI Express. Le format M.2 permet d'utiliser des SSD NVMe mais aussi SATA.
C'est aussi possible d'utiliser des SSD NVMe sans port M.2, qui se branchent sur un connecteur PCI Express classique.
le message d'erreur se trouvera a l'appel de la fonction et non a la définition/implémentation de celle-ci
Effectivement, comme tu ne tapes pas le type de retour, tu n'auras pas de message d'erreur si le type de retour n'est pas celui que tu t'imaginais dans ta tête (tant que tu n'auras pas écrit une autre fonction qui appelle la 1ère).
Cependant ce n'est pas un problème :
Ce que je fais (et je ne dois pas être le seul), c'est de regarder le type que retourne la fonction dans l'éditeur, et modifier la fonction jusqu'à ce qu'elle retourne le type voulu. Ou alors ne pas la modifier et garder le type retourné (ça arrive très souvent que le type "naturel" soit mieux que ce que je pensais vouloir à l'origine)
Quand tu écris une autre fonction qui appelle la première, peu importe que le message d'erreur apparaisse au niveau de la fonction appelée ou appelante : tu dois de toute façon décider si le mieux est de modifier la fonction appelée ou appelante (par exemple dans l'exemple du journal, si tu préfères que la fonction getTemp retourne un nombre ou si tu vas te débrouiller avec la chaine retournée).
il est très simple d'écrire une signature de fonction aussi générique soit-elle. Au lieu d'utiliser Int ou String, tu utilises a par exemple
Ce n'est pas parce que c'est simple à écrire que c'est simple à remarquer (il y a par exemple le cas fréquent d'une fonction sur les chaînes de caractères qui s'appliquent souvent aussi à des tableaux, mais il y a énormément de cas moins évidents).
Haskell est un des langages avec la meilleure inférence de types, c'est dommage de faire le boulot du compilateur à la main et en moins bien.
Il se trouve qu'en Haskell ou en Elm (les deux langages en FP que j'ai pu pratiquer jusqu'à présent), on écrit tout de même les signatures des fonctions et donc les types des arguments
En Haskell (je ne connais pas Elm) ce n'est pas nécessaire. Tu as d'ailleurs intérêt à ne pas le faire car cela te permet de te rendre compte que ta fonction est plus générique que ce que tu pensais en l'écrivant.
principalement parce que le code est destiné à être lu par des êtres humains, qui n'ont pas toujours le temps ou l'envie d'analyser le corps d'une fonction pour inférer eux-même les types "manuellement".
Elle est affichée au survol dans ton éditeur ou avec les commandes :info ou :type dans ghci.
C'est quand même mieux de faire une requête simple (ta requête deux posts plus haut est déjà plus longue que celle avec ON CONFLICT) que plusieurs, non ?
Pour le point 2), pour éviter les phantom reads il faut utiliser le niveau d'isolation serializable, niveau où les requêtes peuvent échouer. Ça complique encore plus la chose (alors que l'upsert est atomique).
[^] # Re: 1 worker process pour nginx ?
Posté par n_e . En réponse au journal Le TapTempo du web, mais plus rapide. Évalué à 1.
Selon vmstat, avec varnish je suis à 100%, en rust à 90%, java à 50% (avec 6 cœurs).
wrk utilise entre 0.7 et 1.5 cœurs selon les benchs, et j'ai des trucs divers qui utilise 0.7 cœurs.
Tu as 16 cœurs ?
[^] # Re: Version Java multi-thread
Posté par n_e . En réponse au journal Le TapTempo du web, mais plus rapide. Évalué à 1.
Tel que je comprends la doc, il n'y a que les handlers de requête qui sont parallélisés. Du coup à partir du moment où tout ce qui n'est pas handler utilise 100% de CPU, ça ne sert à rien d'ajouter davantage de parallélisme.
[^] # Re: Varnish vs nginx et biais de configuration
Posté par n_e . En réponse au journal Le TapTempo du web, mais plus rapide. Évalué à 2.
nginx aussi compile, il utilise LuaJIT :)
Concernant Varnish, ton lien indique :
Cela dit c'est complètement possible que j'aie raté des choses.
[^] # Re: 1 worker process pour nginx ?
Posté par n_e . En réponse au journal Le TapTempo du web, mais plus rapide. Évalué à 3.
Vu que dans les programmes déjà existants Java était single-threadé et Rust multi-threadé la question se posait, et j'ai préféré prendre ce qui était le plus performant dans une configuration relativement standard.
Avec node.js en mode cluster on arrive à :
soit 4.5x la perf. monothreadée, donc un scaling quasi-linéaire (sur les 6 CPUs, 1 CPU était pris par wrk, et 0.7 par d'autres process).
# Conteneurs
Posté par n_e . En réponse au journal Golang, oops you did it again. Évalué à 5.
Je dirais qu'ils servent surtout à gérer les conteneurs et les fonctions qui opèrent dessus en ignorant le contenu de façon simple, type-safe et sans codegen.
Par exemple, le package
sort
permet de trier un slice en utilisant la fonction https://pkg.go.dev/sort#Slice qui n'est pas vraiment ergonomique (je dirais même pas sûre).Avec les génériques on pourrait avoir
func Slice[T any](x []T, less func(xi, xj *T) bool)
à la place.Pour prendre un exemple plus compliqué, l'outil dataloaden permet de générer du code pour un pattern "dataloader", ie. permettre d'avoir une fonction
fn(T): U
dans l'interface publique, et d'avoir le traitement implémenté dans une fonctionfn2(T[]): U[]
, qui est appelée beaucoup moins de fois que fn (et donc optimiser plus facilement le traitement). Aujourd'hui c'est fait avec de la codegen, et ce serait beaucoup plus ergonomique avec des génériques (compilation plus rapide que la codegen, pas besoin de déclarer la liste des variantes à générer, facile d'adapter le code, etc.)[^] # Re: Et la version courte ?
Posté par n_e . En réponse au journal Exercices de programmation et benchmarks. Évalué à 3.
La même chose en Haskell :
[^] # Re: Tu tiens pas tes promesses
Posté par n_e . En réponse au journal recherche-totoz en JavaScript. Évalué à 3.
En quoi utiliser des fonctions avec l'arrow syntax est plus court et élégant que ne pas en utiliser du tout ?
Absolument pas. Non seulement c'est possible de ne pas déclarer de variables temporaires avec async/await, mais surtout en déclarer n'est pas une erreur de programmation et rend le code plus clair.
Quand on écrit
const xml = await res.text()
ouconst totozList = obj.totozes.totoz.length ? obj.totozes.totoz : [obj.totozes.totoz]
, il suffit de lire le nom de la variable pour savoir ce qui est retourné.Je ne vois absolument pas en quoi une structure de type
ajoute quoi que ce soit en complexité par rapport à
f3(f2(f1()))
.Dans l'exemple, tout est déclaré avec le mot-clé const, rien ne peut être écrasé. Par ailleurs rien n'empêche d'appliquer les bonnes pratiques comme donner des noms clairs aux variables et écrire des fonctions courtes en utilisant async/await.
# Chipotage
Posté par n_e . En réponse au journal Le microprocesseur, ce monstre de puissance qui passe son temps à attendre. Évalué à 10.
Le cas le plus favorable, c'est un SSD NVMe, qui communique directement sur le bus PCI Express. Le format M.2 permet d'utiliser des SSD NVMe mais aussi SATA.
C'est aussi possible d'utiliser des SSD NVMe sans port M.2, qui se branchent sur un connecteur PCI Express classique.
[^] # Re: Définition implicites ?
Posté par n_e . En réponse au journal Non, l'inférence de types n'est pas du typage faible. Oui, elle rend les programmes plus lisibles. Évalué à 3.
Effectivement, comme tu ne tapes pas le type de retour, tu n'auras pas de message d'erreur si le type de retour n'est pas celui que tu t'imaginais dans ta tête (tant que tu n'auras pas écrit une autre fonction qui appelle la 1ère).
Cependant ce n'est pas un problème :
[^] # Re: Oui mais
Posté par n_e . En réponse au journal Non, l'inférence de types n'est pas du typage faible. Oui, elle rend les programmes plus lisibles. Évalué à 0.
Ce n'est pas parce que c'est simple à écrire que c'est simple à remarquer (il y a par exemple le cas fréquent d'une fonction sur les chaînes de caractères qui s'appliquent souvent aussi à des tableaux, mais il y a énormément de cas moins évidents).
Haskell est un des langages avec la meilleure inférence de types, c'est dommage de faire le boulot du compilateur à la main et en moins bien.
[^] # Re: Oui mais
Posté par n_e . En réponse au journal Non, l'inférence de types n'est pas du typage faible. Oui, elle rend les programmes plus lisibles. Évalué à 2.
En Haskell (je ne connais pas Elm) ce n'est pas nécessaire. Tu as d'ailleurs intérêt à ne pas le faire car cela te permet de te rendre compte que ta fonction est plus générique que ce que tu pensais en l'écrivant.
Elle est affichée au survol dans ton éditeur ou avec les commandes :info ou :type dans ghci.
[^] # Re: Remarque
Posté par n_e . En réponse au journal Vérifiez vos types avec TypeScript et io-ts. Évalué à 1.
Tu as raison, ça aurait en plus permis de voir que je ne cache pas de boilerplate ou de complexité en ne mettant qu'un bout de fichier.
Oui, c'est volontaire, le type se définit "tout seul" vu la façon dont io-ts est lui-même déclaré.
C'est possible de définir une interface sans tout retaper en faisant comme ça :
Et évidemment si tu utilises VSCode ou autres tu vois tout le détail des types.
[^] # Re: not exists
Posté par n_e . En réponse au journal UPSERT dans PostgreSQL ça déchire. Évalué à 2.
C'est quand même mieux de faire une requête simple (ta requête deux posts plus haut est déjà plus longue que celle avec ON CONFLICT) que plusieurs, non ?
Pour le point 2), pour éviter les phantom reads il faut utiliser le niveau d'isolation serializable, niveau où les requêtes peuvent échouer. Ça complique encore plus la chose (alors que l'upsert est atomique).