Cliff Click est le CTO de Cratus (capteurs IoT pour l'amélioration des processus), le fondateur et co-fondateur de plusieurs startups (dont Rocket Realtime School, Neurensic et H2O.ai) avec plusieurs sorties réussies. Cliff a écrit son premier compilateur à 15 ans (Pascal pour TRS Z-80)! Mieux connu pour avoir travaillé sur C2 en Java (le Sea of Nodes IR). Ce compilateur a montré au monde que JIT peut produire du code de haute qualité, qui est devenu l'un des facteurs qui font de Java l'une des principales plates-formes logicielles modernes. Cliff a ensuite aidé Azul Systems à construire un ordinateur central à 864 cœurs avec un logiciel Java pur prenant en charge les pauses GC sur un tas de 500 gigaoctets pendant 10 millisecondes. En général, Cliff a réussi à travailler sur tous les aspects de la JVM.
Ce hubrapost est une excellente interview avec Cliff. Nous parlerons des sujets suivants:
- Transition vers des optimisations de bas niveau
- Comment faire beaucoup de refactoring
- Modèle de coût
- Formation d'optimisation de bas niveau
- Études de cas d'amélioration de la productivité
- Pourquoi créer votre propre langage de programmation
- Carrière d'ingénieur de performance
- Défis techniques
- Un peu sur l'allocation des registres et le multicœur
- Le plus grand défi de la vie
Entretiens réalisés par:
- Andrey Satarin d'Amazon Web Services. Au cours de sa carrière, il a réussi à travailler dans des projets complètement différents: il a testé la base de données distribuée NewSQL dans Yandex, le système de détection de cloud dans Kaspersky Lab, le jeu multi-utilisateurs dans Mail.ru et le service de calcul de change de devises dans la Deutsche Bank. Il souhaite tester des systèmes backend et distribués à grande échelle.
- Vladimir Sitnikov de Netcracker. Depuis dix ans, il travaille sur les performances et l'évolutivité de NetCracker OS, un logiciel utilisé par les opérateurs télécoms pour automatiser les processus de gestion des réseaux et des équipements réseaux. Il s'intéresse aux problèmes de performances Java et Oracle Database. L'auteur de plus d'une douzaine d'améliorations des performances du pilote JDBC PostgreSQL officiel.
Transition vers des optimisations de bas niveau
Andrei : Vous êtes une personne célèbre dans le monde de la compilation JIT, en Java et travaillez sur la performance en général, non?
Cliff : C'est ça!
Andrew : Commençons par des questions générales sur le travail sur les performances. Que pensez-vous du choix entre les optimisations de haut niveau et de bas niveau comme le travail au niveau du processeur?
Cliff : C'est facile. Le code le plus rapide est celui qui ne s'exécute jamais. Par conséquent, vous devez toujours partir d'un niveau élevé, travailler sur des algorithmes. Une meilleure notation O battra une pire notation O, à moins que des constantes assez grandes n'interviennent. Les choses de bas niveau sont les dernières. Habituellement, si vous optimisez assez bien le reste de la pile, et qu'il reste encore quelque chose d'intéressant - c'est le niveau bas. Mais comment partir d'un haut niveau? Comment savoir que suffisamment de travail a été effectué à un niveau élevé? Eh bien ... pas question. Il n'y a pas de recettes toutes faites. Vous devez comprendre le problème, décider de ce que vous allez faire (afin de ne pas faire des étapes inutiles à l'avenir) et ensuite vous pouvez découvrir un profileur qui peut dire quelque chose d'utile. À un moment donné, vous comprenez vous-même que vous vous êtes débarrassé des choses inutiles et il est temps de régler avec précision le niveau bas. Il s'agit certainement d'un type d'art particulier. Beaucoup de gens font des choses inutiles, mais vont si vite qu'ils n'ont pas le temps de se soucier de la performance. Mais c'est tant que la question ne tient pas debout. Habituellement, 99% du temps, personne ne se soucie de ce que je fais, jusqu'au moment où une chose importante dont quelqu'un se soucie ne se trouve pas sur le chemin critique. Et ici, tout le monde commence à vous harceler sur le sujet "pourquoi cela n'a pas fonctionné parfaitement dès le début." En général, il y a toujours quelque chose à améliorer dans les performances. Mais 99% du temps, vous n'avez pas de pistes! Vous essayez simplement de faire fonctionner quelque chose et, ce faisant, vous comprenez ce qui est important. Vous ne pouvez jamais savoir à l'avance que cette pièce doit être rendue parfaite, donc, en substance, vous devez être parfait en tout. Et c'est impossible, et vous ne le faites pas. Il y a toujours beaucoup de choses à corriger - et c'est parfaitement normal.
Comment faire beaucoup de refactoring
Andrew : Comment travaillez-vous sur la performance? Il s'agit d'une question transversale. Par exemple, avez-vous dû travailler sur des problèmes résultant de l'intersection d'une grande quantité de fonctionnalités existantes?
Cliff : J'essaye d'éviter cela. Si je sais que les performances deviendront un problème, j'y pense avant de commencer à coder, en particulier sur les structures de données. Mais souvent, vous découvrez tout cela beaucoup plus tard. Et puis vous devez prendre des mesures extrêmes et faire ce que j'appelle «réécrire et conquérir»: vous devez saisir une pièce assez grande. Une partie du code devra encore être réécrite en raison de problèmes de performances ou d'autre chose. Quelle que soit la raison de la réécriture du code, il est presque toujours préférable de réécrire un bloc plus grand qu'un bloc plus petit. En ce moment, tout le monde commence à trembler de peur: "Oh mon Dieu, vous ne pouvez pas toucher autant de code!" Mais, en fait, cette approche fonctionne presque toujours beaucoup mieux. Vous devez immédiatement résoudre le gros problème, dessiner un grand cercle autour de lui et dire: je vais tout réécrire à l'intérieur du cercle. La bordure est beaucoup plus petite que le contenu à l'intérieur qui doit être remplacé. Et si une telle délimitation des frontières vous permet de faire le travail à l'intérieur parfaitement - vous avez les mains déliées, faites ce que vous voulez. Une fois que vous avez compris le problème, le processus de réécriture est beaucoup plus facile, alors mordez un gros morceau!
Dans le même temps, lorsque vous réécrivez en gros morceaux et que vous comprenez que les performances deviendront un problème, vous pouvez immédiatement commencer à vous en préoccuper. Habituellement, cela se transforme en des choses simples comme «ne copiez pas de données, gérez les données aussi simplement que possible, réduisez-les». Dans les grandes réécritures, il existe des moyens standard d'améliorer les performances. Et ils tournent presque toujours autour des données.
Modèle de coût
Andrew : Dans l'un des podcasts, vous avez parlé de modèles de coûts dans le contexte de la productivité. Pouvez-vous expliquer ce que cela voulait dire?
Cliff : Bien sûr. Je suis né à une époque où les performances du processeur étaient extrêmement importantes. Et cette ère est de retour - le destin n'est pas sans ironie. J'ai commencé à vivre à l'époque des machines huit bits; mon premier ordinateur fonctionnait avec 256 octets. Ce sont des octets. Tout était très petit. Nous avons dû lire les instructions et dès que nous avons commencé à monter dans la pile des langages de programmation, les langages ont pris de plus en plus. Il y avait Assembler, puis Basic, puis C, et C a repris le travail avec de nombreux détails, tels que l'allocation des registres et la sélection des instructions. Mais tout était assez clair là-bas, et si j'ai fait un pointeur vers une instance d'une variable, alors j'obtiendrai la charge, et le coût est connu pour cette instruction. Le fer produit un nombre connu de cycles machine, de sorte que la vitesse d'exécution de différentes pièces peut être calculée simplement en ajoutant toutes les instructions que vous étiez sur le point d'exécuter. Chaque comparaison / test / branche / appel / chargement / magasin pourrait être plié et dit: ici, vous avez le délai d'exécution. Lorsque vous améliorez les performances, vous serez certainement attentif au type de chiffres correspondant aux petits cycles chauds.
Mais dès que vous passez à Java, Python et des choses similaires, vous vous éloignez très rapidement du fer de bas niveau. Combien coûte un appel getter en Java? Si le JIT dans HotSpot est correctement inséré , il sera chargé, mais si ce n'est pas le cas, ce sera un appel de fonction. Comme le défi réside dans la boucle chaude, il annulera toutes les autres optimisations de cette boucle. Par conséquent, la valeur réelle sera beaucoup plus élevée. Et vous perdez immédiatement la possibilité de regarder un morceau de code et de comprendre que nous devons l'exécuter en termes de vitesse d'horloge du processeur, de mémoire utilisée et de cache. Tout cela ne devient intéressant que si vous êtes vraiment ivre de performances.
Nous sommes maintenant dans une situation où la vitesse des processeurs n'a presque pas augmenté depuis une décennie. Les temps anciens sont de retour! Vous ne pouvez plus compter sur de bonnes performances monothread. Mais si vous vous lancez soudainement dans l'informatique parallèle - c'est incroyablement difficile, tout le monde vous regarde comme James Bond. Une accélération décuplée se produit généralement dans les endroits où quelqu'un gifle quelque chose. La concurrence nécessite beaucoup de travail. Pour obtenir la même accélération décuplée, vous devez comprendre le modèle de coût. Quoi et combien cela coûte. Et pour cela, vous devez comprendre comment la langue repose sur le fer sous-jacent.
Martin Thompson a un grand mot pour son blog Mechanical Sympathy ! Vous devez comprendre ce que le fer va faire, comment exactement il le fera et pourquoi il fait généralement ce qu'il fait. En utilisant cela, il est assez simple de commencer à lire les instructions et de découvrir où se déroule le temps d'exécution. Si vous n'avez pas la formation appropriée, vous cherchez simplement un chat noir dans une pièce sombre. Je vois constamment des gens qui optimisent les performances et qui n'ont aucune idée de ce qu'ils font. Ils sont très tourmentés et ne vont pas vraiment quelque part. Et quand je prends le même morceau de code, y glisse quelques petits hacks et obtiens une accélération cinq ou dix fois, ils sont comme ça: eh bien, c'est tellement malhonnête, nous savions déjà que vous allez mieux. C’est incroyable. De quoi je parle ... le modèle de coût concerne le code que vous écrivez et la vitesse à laquelle il fonctionne en moyenne dans l'image globale.
Andrew : Et comment garder un tel volume dans votre tête? Est-ce atteint par plus d'expérience, ou? Où une telle expérience est-elle acquise?
Cliff : Eh bien, mon expérience n'a pas été la plus simple. J'ai programmé dans Assembler à une époque où il était possible de comprendre chaque instruction individuelle. Cela peut paraître idiot, mais depuis lors, dans ma tête, dans ma mémoire, le jeu d'instructions Z80 est resté pour toujours. Je ne me souviens pas du nom des personnes une minute après la conversation, mais je me souviens du code écrit il y a 40 ans. C'est drôle, ça ressemble à un syndrome de "l' idiot savant ".
Formation d'optimisation de bas niveau
Andrew : Existe-t-il un moyen plus simple de se lancer en affaires?
Cliff : Oui et non. Le fer que nous utilisons tous n'a pas tellement changé pendant cette période. Tout le monde utilise x86, à l'exception des smartphones Arm. Si vous ne faites pas d’incorporation hardcore, vous avez la même chose. Ok, ensuite. Les instructions, elles aussi, n'ont pas changé depuis des siècles. Vous devez aller écrire quelque chose dans Assembler. Un peu, mais assez pour commencer à comprendre. Vous souriez, mais je suis absolument sérieux. Il est nécessaire de comprendre la correspondance du langage et du fer. Après cela, vous devez aller faire pipi un peu et faire un petit compilateur de jouets pour un petit langage de jouets. «Jouet» signifie que vous devez le faire dans un délai raisonnable. Cela peut être super simple, mais il doit générer des instructions. Le fait de générer des instructions nous permettra de comprendre le modèle de coût du pont entre le code de haut niveau sur lequel tout le monde écrit et le code machine qui s'exécute sur le matériel. Cette correspondance sera brûlée dans le cerveau au moment de la rédaction du compilateur. Même le compilateur le plus simple. Après cela, vous pouvez commencer à regarder Java et le fait qu'il a un fossé sémantique plus profond, et construire des ponts par-dessus est beaucoup plus difficile. En Java, il est beaucoup plus difficile de comprendre si notre pont s'est avéré bon ou mauvais, ce qui le fera s'effondrer et non. Mais vous avez besoin d'un point de départ lorsque vous regardez le code et comprenez: «oui, ce getter doit être en ligne à chaque fois». Et puis il s'avère que cela arrive parfois, à l'exception de la situation où la méthode devient trop volumineuse et le JIT commence à tout aligner. La performance de ces lieux peut être prédite instantanément. Habituellement, les getters fonctionnent bien, mais ensuite vous regardez les grandes boucles chaudes et vous vous rendez compte qu'il existe des appels de fonctions flottants qui ne savent pas ce qu'ils font. C'est le problème de l'utilisation généralisée des getters, la raison pour laquelle ils ne s'alignent pas - il n'est pas clair s'il s'agit d'un getter. Si vous avez une base de code très petite, vous pouvez simplement vous en souvenir et dire: c'est un getter, mais c'est un setter. Dans une grande base de code, chaque fonction vit sa propre histoire, qui, en général, n'est connue de personne. Le profileur dit que nous avons perdu 24% de notre temps sur une sorte de cycle, et pour comprendre ce que fait ce cycle, nous devons regarder chaque fonction à l'intérieur. Il est impossible de comprendre cela sans étudier la fonction, ce qui ralentit sérieusement le processus de compréhension. C'est pourquoi je n'utilise pas de getters et setters, je suis passé à un nouveau niveau!
Où trouver le modèle de coût? Eh bien, vous pouvez lire quelque chose, bien sûr ... Mais je pense que la meilleure façon est d'agir. Faites un petit compilateur et ce sera le meilleur moyen de réaliser le modèle de coût et de l'adapter à votre propre tête. Un petit compilateur qui fonctionnerait pour la programmation micro-ondes est une tâche pour un débutant. Eh bien, je veux dire, si vous avez déjà des compétences en programmation, elles devraient suffire. Toutes ces choses sont comme analyser une chaîne, dont vous aurez une sorte d'expression algébrique, extraire les instructions des opérations mathématiques à partir de là dans le bon ordre, prendre les valeurs correctes des registres - tout cela se fait à la fois. Et pendant que vous le ferez, il sera imprimé dans le cerveau. Je pense que tout le monde sait ce que fait le compilateur. Et cela donnera une compréhension du modèle de coût.
Études de cas d'amélioration de la productivité
Andrew : À quoi d'autre vaut-il attention lorsque vous travaillez sur la performance?
Cliff : structures de données. Soit dit en passant, oui, je n'ai pas enseigné ces cours depuis longtemps ... Rocket School . C'était drôle, mais il a fallu tellement d'efforts pour investir, et j'ai aussi la vie! D'accord. Donc, dans l'une des grandes et intéressantes classes, «Où vont vos performances», j'ai donné un exemple aux étudiants: deux gigaoctets et demi de données fintech ont été lues à partir d'un fichier CSV, puis nous avons dû calculer le nombre de produits vendus. Données de marché des ticks régulières. Paquets UDP convertis au format texte depuis les années 70. Le Chicago Mercantile Exchange regroupe toutes sortes de choses comme le beurre, le maïs, le soja, etc. Il a fallu compter ces produits, le nombre de transactions, le volume moyen des mouvements de fonds et de marchandises, etc. C'est un calcul commercial assez simple: trouvez le code produit (ce sont 1-2 caractères dans la table de hachage), obtenez le montant, ajoutez-le à l'un des ensembles d'opérations, ajoutez du volume, ajoutez de la valeur et quelques autres choses. Mathématiques très simples. L'implémentation du jouet était très simple: tout se trouve dans le fichier, je lis le fichier et je le déplace, séparant les entrées individuelles en chaînes Java, recherchant les éléments nécessaires en elles et les pliant selon les mathématiques décrites ci-dessus. Et cela fonctionne à une faible vitesse.
Avec cette approche, tout ce qui se passe est évident, et le calcul parallèle n'aidera pas ici, non? Il s'avère qu'une multiplication par cinq de la productivité ne peut être obtenue qu'en choisissant les bonnes structures de données. Et cela surprend même les programmeurs expérimentés! Dans mon cas particulier, l'astuce était que vous ne devriez pas faire d'allocations de mémoire dans une boucle chaude. Eh bien, ce n'est pas toute la vérité, mais en général - vous ne devriez pas mettre en évidence "une fois dans X" lorsque X est assez grand. Lorsque X est de deux gigaoctets et demi, vous ne devez rien allouer «une fois par lettre», «une fois par ligne» ou «une fois par champ», rien de tout cela. C’est exactement ce qui prend du temps. Comment ça marche même? Imaginez String.split()
un appel à String.split()
ou BufferedReader.readLine()
. Readline
crée une ligne à partir d'un ensemble d'octets venant sur le réseau, une fois pour chaque ligne, pour chacune des centaines de millions de lignes. Je prends cette ligne, j'analyse et je la jette. Pourquoi le jeter - eh bien, je l'ai déjà traité, c'est tout. Donc, pour chaque octet lu à partir de ces 2.7G, deux caractères seront écrits dans la ligne, c'est-à-dire 5.4G déjà, et je n'en ai plus besoin, donc ils sont rejetés. Si vous regardez la bande passante mémoire, nous chargeons 2,7 G, qui passent par la mémoire et le bus mémoire du processeur, puis deux fois plus sont envoyés à la ligne située dans la mémoire, et tout cela est effacé lors de la création de chaque nouvelle ligne. Mais j'ai besoin de le lire, le fer le lit, même si alors tout sera frotté. Et je dois l'écrire, car j'ai créé la ligne et les caches étaient pleins - le cache ne peut pas contenir 2,7 G. Au total, pour chaque octet lu, je lis deux autres octets et j'écris deux octets supplémentaires, et en conséquence, ils ont un rapport 4: 1 - dans ce rapport, nous gaspillons la bande passante mémoire. Et puis il s'avère que si je fais String.split()
, alors je ne le fais pas la dernière fois, il peut y avoir encore 6-7 champs à l'intérieur. Par conséquent, le code de lecture CSV classique suivi d'une analyse de ligne entraîne une perte de bande passante mémoire de l'ordre de 14: 1 par rapport à ce que vous aimeriez vraiment avoir. Si vous jetez ces sécrétions, vous pouvez obtenir une accélération quintuple.
Et ce n'est pas si difficile. Si vous regardez le code sous le bon angle, tout devient assez simple, dès que vous vous rendez compte de l'essence du problème. N'arrêtez même pas d'allouer de la mémoire: le seul problème est que vous allouez quelque chose et il meurt immédiatement et brûle une ressource importante en cours de route, qui dans ce cas est la bande passante mémoire. Et tout cela se traduit par une baisse de productivité. Sur x86, vous devez généralement graver activement les horloges du processeur, et ici, vous avez brûlé toute la mémoire beaucoup plus tôt. Solution - vous devez réduire la quantité de décharge.
Une autre partie du problème est que si vous démarrez le profileur à la fin de la bande de mémoire, juste au moment où cela se produit, vous attendez généralement le retour du cache, car il est plein de déchets que vous venez d'apparaître avec toutes ces lignes. Par conséquent, chaque opération de chargement ou de stockage devient lente, car elles entraînent des échecs dans le cache - le cache entier est devenu lent, attendant que les ordures le quittent. Par conséquent, le profileur n'affichera que du bruit aléatoire chaud étalé tout au long du cycle - il n'y aura pas d'instruction chaude séparée ni de place dans le code. Juste le bruit. Et si vous regardez les cycles GC, ils seront tous de jeune génération et ultra-rapides - microsecondes ou millisecondes maximum. Après tout, toute cette mémoire meurt instantanément. Vous allouez des milliards de gigaoctets, et cela les coupe, et les coupe, et les coupe à nouveau. Tout cela se passe très rapidement. Il s'avère qu'il existe des cycles GC bon marché, du bruit chaud tout au long du cycle, mais nous voulons obtenir une accélération 5x. À ce moment, quelque chose devrait se refermer dans ma tête et sonner: "pourquoi donc?!" Le débordement de bande passante n'apparaît pas dans le débogueur classique, vous devez exécuter le débogueur du compteur de performances matérielles et le voir vous-même et directement. Et pas directement, on peut soupçonner ces trois symptômes. Le troisième symptôme est quand vous regardez ce que vous mettez en évidence, demandez au profileur, et il répond: "Vous avez fait un milliard de lignes, mais le GC a fonctionné gratuitement." Dès que cela s'est produit, vous vous rendez compte que vous avez généré trop d'objets et brûlé toute la bande de mémoire. Il existe un moyen de comprendre cela, mais ce n'est pas évident.
Le problème est dans la structure des données: la structure nue derrière tout ce qui se passe, elle est trop grande, elle est de 2,7 G sur le disque, donc faire une copie de cette chose est très indésirable - je veux la charger du tampon d'octets réseau immédiatement dans les registres afin de ne pas lire-écrire dans la chaîne d'avant en arrière cinq fois. Malheureusement, Java par défaut ne vous offre pas une telle bibliothèque dans le cadre du JDK. Mais c'est trivial, non? En fait, ce sont 5 à 10 lignes de code qui seront utilisées pour implémenter votre propre chargeur de ligne en mémoire tampon, qui répète le comportement de la classe de ligne, tout en étant un wrapper autour du tampon d'octets sous-jacent. En conséquence, il s'avère que vous travaillez presque comme avec des chaînes, mais en fait, il y a des pointeurs vers le tampon, et les octets bruts ne sont copiés nulle part, et donc les mêmes tampons sont réutilisés, maintes et maintes fois, et le système d'exploitation est heureux de prendre en charge des choses auxquelles il est destiné, comme la double mise en mémoire tampon cachée de ces tampons d'octets, et vous-même ne broyez plus un flux sans fin de données inutiles. Par ailleurs, vous comprenez, lorsque vous travaillez avec le GC, il est garanti que chaque allocation de mémoire ne sera pas visible pour le processeur après le dernier cycle du GC? Par conséquent, tout cela ne peut en aucun cas être dans le cache, puis un échec garanti à 100% se produit. Lorsque vous travaillez avec un pointeur sur x86, la soustraction d'un registre de la mémoire prend 1-2 cycles, et dès que cela se produit, vous payez, payez, payez, car la mémoire est entièrement sur neuf caches - et c'est le coût d'allocation de mémoire. Valeur actuelle.
En d'autres termes, les structures de données sont les plus difficiles à modifier. Et dès que vous réalisez que vous avez choisi la mauvaise structure de données qui tuera la productivité à l'avenir, vous devez généralement accélérer le travail essentiel, mais si vous ne le faites pas, ce sera pire. Tout d'abord, vous devez penser aux structures de données, c'est important. Le coût principal réside ici dans les structures de données en gras, qu'ils commencent à utiliser dans le style "J'ai copié la structure de données X dans la structure de données Y, parce que j'aime mieux la forme." Mais l'opération de copie (qui semble bon marché) dépense en fait une bande de mémoire et ici tout le temps d'exécution perdu est enterré. Si j'ai une chaîne géante avec JSON et que je veux la transformer en un arbre DOM structuré à partir de POJO ou quelque chose comme ça, l'opération d'analyse de cette chaîne et de construction d'un POJO, puis un nouvel appel à POJO à l'avenir se révélera sans valeur - ce n'est pas une chose chère. Sauf si vous courrez sur POJO beaucoup plus souvent que sur une ligne. À la place, vous pouvez essayer de déchiffrer la chaîne et de n'en extraire que ce dont vous avez besoin, sans la transformer en POJO. Si tout cela se produit sur le chemin à partir duquel des performances maximales sont requises, pas de POJO pour vous - vous devez en quelque sorte creuser directement dans la ligne.
Pourquoi créer votre propre langage de programmation
Andrei : Vous avez dit que pour comprendre le modèle de coût, vous devez écrire votre propre petite langue ...
Cliff : Pas un langage, mais un compilateur. Le langage et le compilateur sont deux choses différentes. La différence la plus importante est dans votre tête.
Andrei : Au fait, pour autant que je sache, vous expérimentez la création de vos propres langues. Pourquoi?
Cliff : Parce que je peux! Je suis à moitié retraité, c'est donc mon hobby. J'ai implémenté les langues de quelqu'un d'autre toute ma vie. J'ai également travaillé dur sur le style de codage. Et aussi parce que je vois des problèmes dans d'autres langues. Je vois qu'il y a de meilleures façons de faire les choses habituelles. Et je les utiliserais. J'en ai juste assez de voir des problèmes en moi, en Java, en Python, dans n'importe quel autre langage. J'écris sur React Native, JavaScript et Elm comme passe-temps, qui ne concerne pas la retraite, mais le travail actif. Et j'écris également en Python et, très probablement, je continuerai à travailler sur l'apprentissage automatique pour les backends Java. Il existe de nombreuses langues populaires et toutes ont des fonctionnalités intéressantes. Tout le monde est bon dans quelque chose qui lui est propre et vous pouvez essayer de rassembler tous ces jetons. Donc, j'étudie les choses qui m'intéressent, le comportement du langage, j'essaye de trouver une sémantique raisonnable. Et jusqu'ici je le fais! En ce moment, je lutte avec la sémantique de la mémoire, car je veux l'avoir à la fois en C et en Java, et obtenir un modèle de mémoire et une sémantique de mémoire solides pour les charges et les magasins. Dans le même temps, avoir une inférence de type automatique comme dans Haskell. Ici, j'essaie de mélanger l'inférence de type Haskell avec la mémoire fonctionnant à la fois en C et en Java. Je fais cela depuis 2-3 mois, par exemple.
Andrei : Si vous construisez une langue qui prend de meilleurs aspects des autres langues, pensiez-vous que quelqu'un ferait le contraire: prenez vos idées et utilisez-les?
Cliff : C'est comme ça que de nouvelles langues apparaissent! Pourquoi Java est-il similaire à C? Parce que C avait une bonne syntaxe que tout le monde comprenait et Java s'est inspiré de cette syntaxe, ajoutant la sécurité des types, vérifiant les limites des tableaux, GC, et ils ont également amélioré certaines choses de C. Ils ont ajouté les leurs. Mais ils ont été un peu inspirés, non? Tout le monde se tient sur les épaules des géants qui vous ont précédé - c'est ainsi que les progrès sont réalisés.
Andrew : Si je comprends bien, votre langue sera en sécurité concernant l'utilisation de la mémoire. Avez-vous déjà pensé à implémenter quelque chose comme le vérificateur d'emprunt de Rust? Tu l'as regardé, comment t'aimait-il?
Cliff : Eh bien, j'écris du C depuis des lustres, avec tous ces malloc et gratuitement, et je gère manuellement la durée de vie. Vous savez, 90 à 95% d'une durée de vie gérée manuellement a la même structure. Et c'est très, très douloureux de le faire manuellement. J'aimerais que le compilateur dise simplement ce qui se passe là-bas et ce que vous avez réalisé avec vos actions. Pour certaines choses, un vérificateur d'emprunt le fait hors de la boîte. Et il devrait afficher automatiquement les informations, tout comprendre et ne pas me surcharger pour affirmer cette compréhension. Il doit faire au moins une analyse d'échappement locale, et seulement s'il échoue, vous devez ajouter des annotations de type qui décrivent la durée de vie - et un tel schéma est beaucoup plus compliqué qu'un vérificateur d'emprunt ou tout vérificateur de mémoire existant. Le choix entre "tout est en ordre" et "je n'ai rien compris" - non, il doit y avoir quelque chose de mieux.
Donc, en tant que personne qui a écrit beaucoup de code C, je pense que la prise en charge du contrôle automatique de la durée de vie est la chose la plus importante. Et je me suis lassé de la quantité de mémoire utilisée par Java et la principale plainte concerne GC. Lors de l'allocation de mémoire en Java, vous ne retournerez pas la mémoire qui était locale sur la dernière boucle GC. Dans les langues avec une gestion de mémoire plus précise, ce n'est pas le cas. Si vous appelez malloc, vous obtenez immédiatement la mémoire qui vient d'être utilisée. Habituellement, vous faites des choses temporaires avec votre mémoire et vous les ramenez immédiatement. Et elle retourne immédiatement à la piscine malloc, et le prochain cycle malloc la sort de nouveau. Par conséquent, l'utilisation réelle de la mémoire est réduite à un ensemble d'objets vivants à un moment donné, plus les fuites. Et si tout ne se déroule pas de manière indécente, la majeure partie de la mémoire s'installe dans les caches et le processeur, et cela fonctionne rapidement. Mais cela nécessite beaucoup de gestion manuelle de la mémoire avec malloc et gratuit, appelé dans le bon ordre, au bon endroit. La rouille elle-même peut gérer cela correctement et dans un tas de cas donner des performances encore plus élevées, car la consommation de mémoire est limitée uniquement aux calculs actuels - au lieu d'attendre le prochain cycle de GC pour libérer de la mémoire. En conséquence, nous avons obtenu un moyen très intéressant d'améliorer les performances. Et assez puissant - dans le sens, j'ai fait de telles choses lors du traitement des données pour la fintech, et cela m'a permis d'obtenir une accélération cinq fois. Il s'agit d'une accélération assez importante, en particulier dans un monde où les processeurs ne vont pas plus vite, et nous continuons tous d'attendre des améliorations.
Andrew : Je voudrais également poser des questions sur la carrière dans son ensemble. Vous êtes devenu célèbre pour avoir travaillé chez JIT à HotSpot puis avoir déménagé à Azul - et c'est également une entreprise JVM. Mais ils étaient déjà engagés dans plus de fer que de logiciels. Et puis, tout à coup, je suis passé au Big Data et au Machine Learning, puis à la détection des fraudes. Comment est-ce arrivé? Ce sont des domaines de développement très différents.
Cliff : Je programme depuis un certain temps maintenant et j'ai réussi à m'enregistrer dans des classes très différentes. Et quand les gens disent: «Oh, c'est toi qui a fait JIT pour Java!», C'est toujours drôle. Mais avant cela, j'étais engagé dans le clone PostScript - le langage qu'Apple utilisait autrefois pour ses imprimantes laser. Et avant cela, il a fait la mise en œuvre de la langue Forth. Je pense que le thème commun pour moi est le développement d'outils. Toute ma vie, j'ai créé des outils avec lesquels d'autres personnes écrivent leurs programmes sympas. Mais j'ai également été impliqué dans le développement de systèmes d'exploitation, de pilotes, de débogueurs au niveau du noyau, de langages pour développer le système d'exploitation, qui a commencé trivialement, mais au fil du temps, tout est devenu compliqué et compliqué. Mais le sujet principal est néanmoins le développement d'outils. Un gros morceau de vie s'est passé entre Azul et Sun, et il s'agissait de Java. Mais quand j'ai commencé le Big Data et le Machine Learning, j'ai mis mon chapeau de nouveau et j'ai dit: "Oh, et maintenant nous avons un problème non trivial, et ici beaucoup de choses intéressantes et de gens qui font quelque chose" se produisent. C'est un excellent chemin de développement qui mérite d'être suivi.
Oui, j'aime vraiment l'informatique distribuée. Mon premier travail a été étudiant en C, sur un projet publicitaire. Celles-ci étaient réparties sur des puces Zilog Z80, qui collectaient des données pour la reconnaissance optique de texte analogique produites par un véritable analyseur analogique. C'était un sujet cool et totalement anormal. Mais il y avait des problèmes, une partie n'était pas reconnue correctement, il était donc nécessaire d'obtenir une photo et de la montrer à une personne qui lisait déjà avec ses yeux et informait ce qui s'y disait, et donc il y avait des jongleurs de données, et ce travail avait sa propre langue . Il y avait un backend qui gérait tout cela - fonctionnant parallèlement au Z80 avec des terminaux vt100 en cours d'exécution - un par personne, et il y avait un modèle de programmation parallèle sur le Z80. Un certain morceau de mémoire commun partagé par tous les Z80 à l'intérieur d'une configuration en étoile; le fond de panier était partagé, et la moitié de la RAM était partagée au sein du réseau, et une autre moitié était privée ou dépensée pour autre chose. Un système distribué parallèle significativement complexe avec une mémoire partagée ... semi-partagée. Quand c'était ... Déjà à ne pas me souvenir, quelque part au milieu des années 80. Il y a très longtemps.
Oui, nous supposerons que 30 ans, c'est assez long. Les tâches associées à l'informatique distribuée existent depuis longtemps, les gens ont longtemps combattu avec les clusters Beowulf . De tels clusters ressemblent à ... Par exemple: il y a Ethernet et votre x86 rapide est connecté à cet Ethernet, et maintenant vous voulez obtenir de la fausse mémoire partagée, car personne ne pouvait alors faire le codage de l'informatique distribuée, c'était trop compliqué et donc c'était de la fausse mémoire partagée avec protection pages de mémoire x86, et si vous avez écrit sur cette page, nous avons dit aux autres processeurs que s'ils avaient accès à la même mémoire partagée, elle devrait être téléchargée de vous, et donc quelque chose comme un protocole de prise en charge de la cohérence du cache est apparu et un logiciel pour cela. Concept intéressant. Le vrai problème, bien sûr, était différent. Tout cela a fonctionné, mais vous avez rapidement rencontré des problèmes de performances, car personne ne comprenait les modèles de performances à un niveau suffisamment bon - quels modèles d'accès à la mémoire sont là, comment s'assurer que les nœuds ne se pingent pas sans fin, etc.
Dans H2O, j'ai trouvé ceci: les développeurs eux-mêmes sont responsables de déterminer où le parallélisme est caché et où il ne l'est pas. J'ai créé un tel modèle de codage que l'écriture de code haute performance était facile et simple. Mais l'écriture de code à exécution lente est difficile, elle sera mauvaise. Vous devez sérieusement essayer d'écrire du code lent, vous devez utiliser des méthodes non standard. Le code de freinage est visible en un coup d'œil. Par conséquent, le code est généralement écrit et fonctionne rapidement, mais vous devez savoir quoi faire dans le cas de la mémoire partagée. Tout cela est lié aux grands tableaux et le comportement y est similaire aux grands tableaux non volatils en Java parallèle. Je veux dire, imaginez que deux threads écrivent dans un tableau parallèle, l'un d'eux gagne, et l'autre, respectivement, perd, et vous ne savez pas lequel d'entre eux est qui. S'ils ne sont pas volatils, l'ordre peut être n'importe quoi - et cela fonctionne vraiment bien. Les gens se soucient vraiment de l'ordre des opérations, ils définissent correctement la volatilité et ils s'attendent à des problèmes de mémoire aux bons endroits. Sinon, ils écriraient simplement du code sous la forme de cycles de 1 à N, où N représente quelques milliers de milliards, dans l'espoir que tous les cas complexes deviendront automatiquement parallèles - et là cela ne fonctionne pas. Mais dans H2O ce n'est ni Java ni Scala, vous pouvez le considérer comme «Java moins moins» si vous le souhaitez. Il s'agit d'un style de programmation très compréhensible et similaire à l'écriture de code C ou Java simple avec des boucles et des tableaux. Mais en même temps, la mémoire peut être traitée avec des téraoctets. J'utilise toujours H2O. – , . Big Data , H2O.
: ?
: ? , – .
. . , , , , . Sun, , , , . , , . , C1, , – . , . , x86- , , 5-10 , 50 .
, , , , C. , , - , C . C, C . , , C, - … , . , . , , . , , 5% . - – , « », , . : , , . . , – , . , . - – . , , ( , ), , , . , , , .
, , , , , , . , , , - . , , , . , , , , . , : , . , , - : , , - , . – , , – ! – , . Java. Java , , , , – , « ». , , . , Java C . – Java, C , , , . , – , . , . , , . : .
: - . , , - , ?
: ! – , NP- - . , ? . , Ahead of Time – . - . , , – , ! – , . , , . . ? , : , , - ! - , . . , , . : - , - . , , . , , , , - . ! , , , – . . NP- .
: , – . , , , , …
: . «». . , . – , , , ( , ). , - . , , , . , , . , . , , . , , . , , - , – . – . , GC, , , , – , . , . , , . , – , ? , .
: , ? ?
: GPU , !
: . ?
: , - Azul. , . . H2O , . , GPU. ? , Azul, : – .
: ?
: , … . , . , , , , . , , . , Java C1 C2 – . , Java – . , , – . … . - , Sun, … , , . , . , . … … , . , , . . - , : . , , , , , , . , . . , . « , , ». : «!». , , , : , .
– , , , . . , , , , . , Java JIT, C2. , – . , – ! . , , , , , , . . . , . , , , , : , , . , – . , , - . : « ?». , . , , : , , – ? , . , , , , , , - .
: , -. ?
: , , . – . . , . . . : , , - – . . , , – , . , , , , - , . , . , , - . , , – , .
, . , – , , . , . , – . , . , , « », , – , , , , . , , « ».
. . - , , «»: , – . – . , , . «, -, , ». , : , . , , . . – , . , ? , ? ? , ? . , . – . . , . – – , . , « » . : «--», : «, !» . . , , , , . , . , . , – , . – , . , , , .
, – , . , , . , . , , , , . , , . , , , , . . , , , . , , , , . , , , . , – , , , . , .
: … . , . . Hydra!
Hydra 2019, 11-12 2019 -. «The Azul Hardware Transactional Memory experience» . .