Superbe entretien avec Cliff Click, le père de la compilation JIT en Java

Superbe entretien avec Cliff Click, le père de la compilation JIT en JavaClic sur la falaise — CTO de Cratus (capteurs IoT pour l'amélioration des processus), 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 le TRS Z-80) ! Il est surtout connu pour ses travaux sur C2 en Java (le Sea of ​​​​Nodes IR). Ce compilateur a montré au monde que JIT pouvait produire du code de haute qualité, ce qui a été l'un des facteurs de l'émergence de Java comme l'une des principales plates-formes logicielles modernes. Ensuite, Cliff a 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 Go en 10 millisecondes. En général, Cliff a réussi à travailler sur tous les aspects de la JVM.

 
Cet Habrapost est une excellente interview de Cliff. Nous parlerons des sujets suivants :

  • Transition vers des optimisations de bas niveau
  • Comment faire une grande refactorisation
  • Modèle de coût
  • Formation d'optimisation de bas niveau
  • Exemples pratiques d’amélioration des performances
  • 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 les multicœurs
  • Le plus grand défi de la vie

L'entretien est mené par :

  • Andreï Satarine d'Amazon Web Services. Au cours de sa carrière, il a réussi à travailler sur des projets complètement différents : il a testé la base de données distribuée NewSQL dans Yandex, un système de détection de cloud dans Kaspersky Lab, un jeu multijoueur dans Mail.ru et un service de calcul des prix des changes chez Deutsche Bank. Intéressé par les tests de systèmes backend et distribués à grande échelle.
  • Vladimir Sitnikov de Netcracker. Dix ans de travail sur les performances et l'évolutivité de NetCracker OS, logiciel utilisé par les opérateurs télécoms pour automatiser les processus de gestion des réseaux et des équipements réseaux. Intéressé par les problèmes de performances de Java et Oracle Database. Auteur de plus d'une douzaine d'améliorations de performances dans le pilote officiel PostgreSQL JDBC.

Transition vers des optimisations de bas niveau

Andrew: Vous êtes un grand nom dans le monde de la compilation JIT, de Java et du travail de performance en général, n'est-ce pas ? 

falaise: C'est comme ça!

Andrew: Commençons par quelques questions générales sur le travail de performance. Que pensez-vous du choix entre des optimisations de haut niveau et de bas niveau, comme travailler au niveau du CPU ?

falaise: Oui, tout est simple ici. Le code le plus rapide est celui qui ne s'exécute jamais. Il faut donc toujours partir d'un niveau élevé, travailler sur des algorithmes. Une meilleure notation O battra une moins bonne notation O, à moins que des constantes suffisamment grandes n'interviennent. Les choses de bas niveau durent. En règle générale, si vous avez suffisamment bien optimisé le reste de votre pile et qu'il reste encore des éléments intéressants, c'est un niveau faible. Mais comment partir d’un haut niveau ? Comment savez-vous qu’un travail de haut niveau suffisant a été effectué ? Eh bien... pas question. Il n’existe pas de recettes toutes faites. Vous devez comprendre le problème, décider de ce que vous allez faire (afin de ne pas prendre de mesures inutiles à l'avenir), puis découvrir le profileur qui peut dire quelque chose d'utile. À un moment donné, vous réalisez vous-même que vous vous êtes débarrassé des choses inutiles et qu’il est temps de procéder à quelques ajustements de bas niveau. Il s’agit certainement d’un type d’art particulier. Beaucoup de gens font des choses inutiles, mais avancent si vite qu’ils n’ont pas le temps de se soucier de la productivité. Mais cela jusqu’à ce que la question se pose sans ambages. Habituellement, 99 % du temps, personne ne se soucie de ce que je fais, jusqu'au moment où une chose importante arrive sur le chemin critique et dont personne ne se soucie. Et là, tout le monde commence à vous harceler en vous demandant « pourquoi cela n’a pas fonctionné parfaitement dès le début ». En général, il y a toujours quelque chose à améliorer en termes de performances. Mais 99 % du temps, vous n’avez aucune piste ! Vous essayez simplement de faire fonctionner quelque chose et, ce faisant, vous découvrez ce qui est important. Vous ne pouvez jamais savoir à l’avance que cette pièce doit être parfaite, donc en fait, vous devez être parfait en tout. Mais c’est impossible et vous ne le faites pas. Il y a toujours beaucoup de choses à réparer, et c'est tout à fait normal.

Comment faire une grande refactorisation

Andrew: Comment travaillez-vous sur une performance ? Il s’agit d’un problème transversal. Par exemple, avez-vous déjà dû travailler sur des problèmes résultant de l’intersection de nombreuses fonctionnalités existantes ?

falaise: J'essaie de l'éviter. Si je sais que les performances seront un problème, j'y réfléchis avant de commencer à coder, notamment avec les structures de données. Mais souvent, on découvre tout cela très tard. Et puis il faut prendre des mesures extrêmes et faire ce que j’appelle « réécrire et conquérir » : il faut s’emparer d’un morceau suffisamment gros. Une partie du code devra encore être réécrite en raison de problèmes de performances ou autre. Quelle que soit la raison de la réécriture du code, il est presque toujours préférable de réécrire un morceau plus gros qu'un morceau plus petit. À ce moment-là, tout le monde commence à trembler de peur : « oh mon Dieu, tu ne peux pas toucher à autant de code ! » Mais en fait, cette approche fonctionne presque toujours bien mieux. Vous devez immédiatement vous attaquer à un gros problème, tracer un grand cercle autour et dire : je vais tout réécrire à l'intérieur du cercle. La bordure est beaucoup plus petite que le contenu qui doit être remplacé. Et si une telle délimitation des limites vous permet de faire parfaitement le travail à l'intérieur, vos mains sont libres, faites ce que vous voulez. Une fois que vous avez compris le problème, le processus de réécriture est beaucoup plus facile, alors prenez-y une grosse bouchée !
Dans le même temps, lorsque vous effectuez une réécriture importante et réalisez que les performances vont être un problème, vous pouvez immédiatement commencer à vous en inquiéter. Cela se traduit généralement par des choses simples comme « ne copiez pas les données, gérez les données aussi simplement que possible, réduisez-les ». Dans les réécritures volumineuses, 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é des modèles de coûts dans le contexte de la productivité. Pouvez-vous expliquer ce que vous entendiez par là ?

falaise: Certainement. Je suis né à une époque où les performances du processeur étaient extrêmement importantes. Et cette époque revient à nouveau - le destin n'est pas sans ironie. J'ai commencé à vivre à l'époque des machines à huit bits ; mon premier ordinateur fonctionnait avec 256 octets. Exactement des octets. Tout était très petit. Les instructions devaient être comptées, et à mesure que nous commencions à gravir les échelons de la pile des langages de programmation, les langages en prenaient de plus en plus. Il y avait Assembler, puis Basic, puis C, et C s'occupait de nombreux détails, comme l'allocation des registres et la sélection des instructions. Mais tout y était assez clair, et si je faisais un pointeur vers une instance d'une variable, alors j'obtiendrais une charge et le coût de cette instruction est connu. Le matériel produit un certain nombre de cycles machine, donc la vitesse d'exécution de différentes choses peut être calculée simplement en additionnant toutes les instructions que vous allez exécuter. Chaque comparaison/test/branche/appel/chargement/magasin pourrait être additionné et dire : c'est le temps d'exécution pour vous. Lorsque vous travaillez sur l'amélioration des performances, vous ferez certainement attention aux nombres qui correspondent aux petits cycles chauds. 
Mais dès que vous passez à Java, Python et autres choses similaires, vous vous éloignez très rapidement du matériel de bas niveau. Quel est le coût de l’appel d’un getter en Java ? Si JIT dans HotSpot est correct en ligne, il se chargera, mais s'il ne l'a pas fait, ce sera un appel de fonction. Puisque l'appel est sur une boucle chaude, il remplacera toutes les autres optimisations de cette boucle. Le coût réel sera donc bien plus élevé. 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 et de cache utilisés. Tout cela ne devient intéressant que si l’on se met vraiment dans la performance.
Nous nous retrouvons désormais dans une situation où les vitesses des processeurs n’ont pratiquement pas augmenté depuis une décennie. Le bon vieux temps est de retour ! Vous ne pouvez plus compter sur de bonnes performances en monothread. Mais si vous vous lancez soudainement dans l’informatique parallèle, c’est incroyablement difficile, tout le monde vous regarde comme James Bond. Ici, des accélérations décuplées se produisent généralement dans des endroits où quelqu'un a gâché quelque chose. La concurrence nécessite beaucoup de travail. Pour obtenir cette accélération XNUMXx, vous devez comprendre le modèle de coût. Quoi et combien ça coûte ? Et pour ce faire, vous devez comprendre comment la languette s’adapte au matériel sous-jacent.
Martin Thompson a choisi un bon mot pour son blog Sympathie mécanique! Vous devez comprendre ce que le matériel va faire, comment il le fera exactement et pourquoi il fait ce qu'il fait en premier lieu. Grâce à cela, il est assez facile de commencer à compter les instructions et de déterminer où va 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 tout le temps des gens optimiser leurs performances sans savoir ce qu'ils font. Ils souffrent beaucoup et ne font pas beaucoup de progrès. Et quand je prends le même morceau de code, que j'y glisse quelques petits hacks et que j'obtiens une accélération cinq ou dix fois supérieure, ils me disent : eh bien, ce n'est pas juste, nous savions déjà que vous étiez meilleur. Incroyable. De quoi je parle... le modèle de coût concerne le type de code que vous écrivez et la vitesse à laquelle il s'exécute en moyenne dans son ensemble.

Andrew: Et comment peux-tu garder un tel volume dans ta tête ? Est-ce que cela est réalisé avec plus d'expérience, ou ? D’où vient une telle expérience ?

falaise: Eh bien, je n’ai pas acquis mon expérience de la manière la plus simple. J'ai programmé en Assembly à l'époque où l'on pouvait comprendre chaque instruction. Cela semble stupide, mais depuis, le jeu d'instructions du Z80 est toujours resté dans ma tête, dans ma mémoire. Je ne me souviens pas des noms des gens une minute après avoir parlé, mais je me souviens du code écrit il y a 40 ans. C'est drôle, ça ressemble à un syndrome"scientifique idiot».

Formation d'optimisation de bas niveau

Andrew: Y a-t-il un moyen plus simple d'entrer ?

falaise: Oui et non. Le matériel que nous utilisons tous n’a pas beaucoup changé au fil du temps. Tout le monde utilise x86, à l'exception des smartphones Arm. Si vous ne faites pas une sorte d'intégration hardcore, vous faites la même chose. D'accord, ensuite. Les instructions n’ont pas non plus changé depuis des siècles. Vous devez aller écrire quelque chose dans Assembly. Pas grand-chose, mais suffisamment pour commencer à comprendre. Vous souriez, mais je parle tout à fait sérieusement. Vous devez comprendre la correspondance entre le langage et le matériel. Après cela, vous devez écrire un peu et créer un petit compilateur jouet pour un petit langage jouet. S'apparentant à un jouet signifie qu'il doit être fabriqué dans un délai raisonnable. Cela peut être super simple, mais cela doit générer des instructions. Le fait de générer une instruction vous aidera à comprendre le modèle de coût du pont entre le code de haut niveau que tout le monde écrit et le code machine qui s'exécute sur le matériel. Cette correspondance sera gravée dans le cerveau au moment où le compilateur sera écrit. Même le compilateur le plus simple. Après cela, vous pouvez commencer à examiner Java et le fait que son gouffre sémantique est beaucoup plus profond et qu'il est beaucoup plus difficile de construire des ponts dessus. 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 ce qui ne le fera pas. Mais vous avez besoin d'une sorte de point de départ où vous regardez le code et comprenez : « ouais, ce getter devrait être intégré à chaque fois. » Et puis il s'avère que cela se produit parfois, sauf dans le cas où la méthode devient trop volumineuse et où le JIT commence à tout intégrer. Les performances de ces lieux peuvent être prédites instantanément. Habituellement, les getters fonctionnent bien, mais ensuite vous regardez les grandes boucles chaudes et vous réalisez qu'il y a des appels de fonction qui circulent là-bas 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 sont pas intégrés est qu'il n'est pas clair s'ils sont des getters. Si vous avez une très petite base de code, vous pouvez simplement vous en souvenir et dire ensuite : ceci est un getter, et ceci 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% du temps sur certaines boucles et pour comprendre ce que fait cette boucle, nous devons examiner chaque fonction qu'elle contient. Il est impossible de comprendre cela sans étudier la fonction, ce qui ralentit considérablement le processus de compréhension. C'est pourquoi je n'utilise pas de getters et de setters, j'ai atteint 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. Créer un petit compilateur sera le meilleur moyen de comprendre le modèle de coût et de l'adapter à votre propre tête. Un petit compilateur qui conviendrait à la programmation d'un 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, cela devrait suffire. Toutes ces choses comme analyser une chaîne que vous avez comme une sorte d'expression algébrique, en extraire des instructions pour des opérations mathématiques dans le bon ordre, prendre les valeurs correctes des registres - tout cela se fait en même temps. Et pendant que vous le ferez, cela s’imprimera dans votre cerveau. Je pense que tout le monde sait ce que fait un compilateur. Et cela donnera une compréhension du modèle de coût.

Exemples pratiques d’amélioration des performances

Andrew: À quoi d'autre devez-vous faire attention lorsque vous travaillez sur la productivité ?

falaise: Structures de données. D'ailleurs, oui, je n'ai pas donné ces cours depuis longtemps... École de fusée. C'était amusant, mais cela demandait beaucoup d'efforts, et j'ai aussi une vie ! D'ACCORD. Ainsi, dans l'un des cours les plus importants et les plus intéressants, « Où vont vos performances », j'ai donné un exemple aux étudiants : deux gigaoctets et demi de données fintech ont été lus à partir d'un fichier CSV, puis ils ont dû calculer le nombre de produits vendus. . Données régulières sur le marché des ticks. Paquets UDP convertis au format texte depuis les années 70. Chicago Mercantile Exchange – toutes sortes de choses comme le beurre, le maïs, le soja, des choses comme ça. Il fallait compter ces produits, le nombre de transactions, le volume moyen de mouvement de fonds et de biens, etc. C'est un calcul commercial assez simple : trouvez le code du produit (soit 1 à 2 caractères dans la table de hachage), obtenez le montant, ajoutez-le à l'un des ensembles d'échanges, ajoutez du volume, ajoutez de la valeur et quelques autres choses. Mathématiques très simples. L'implémentation du jouet a été très simple : tout est dans un fichier, je lis le fichier et je le parcours, divisant les enregistrements individuels en chaînes Java, recherchant les éléments nécessaires et les additionnant selon les mathématiques décrites ci-dessus. Et cela fonctionne à faible vitesse.

Avec cette approche, ce qui se passe est évident, et le calcul parallèle n'aidera pas, n'est-ce pas ? Il s’avère qu’une multiplication par cinq des performances peut être obtenue simplement 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 de ne pas effectuer d'allocation 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 surbrillance « une fois dans X » lorsque X est suffisamment grand. Lorsque X fait deux gigaoctets et demi, vous ne devriez rien allouer « une fois par lettre », ou « une fois par ligne », ou « une fois par champ », quelque chose comme ça. C'est là que l'on passe du temps. Comment est-ce que ça marche ? Imagine que je passe un appel String.split() ou BufferedReader.readLine(). Readline crée une chaîne à partir d'un ensemble d'octets transitant par le réseau, une fois pour chaque ligne, pour chacune des centaines de millions de lignes. Je prends cette ligne, l'analyse et la jette. Pourquoi est-ce que je le jette - eh bien, je l'ai déjà traité, c'est tout. Ainsi, pour chaque octet lu à partir de ces 2.7G, deux caractères seront écrits dans la ligne, c'est-à-dire déjà 5.4G, et je n'en ai plus besoin pour rien d'autre, ils sont donc jetés. Si vous regardez la bande passante mémoire, nous chargeons 2.7G qui traversent la mémoire et le bus mémoire du processeur, puis deux fois plus sont envoyés à la ligne située en mémoire, et tout cela s'effiloche lorsque chaque nouvelle ligne est créée. Mais j’ai besoin de le lire, le matériel le lit, même si tout s’effiloche par la suite. Et je dois l'écrire car j'ai créé une ligne et les caches sont pleins - le cache ne peut pas accueillir 2.7G. Ainsi, pour chaque octet que je lis, je lis deux octets supplémentaires et j'écris deux octets supplémentaires, et au final, ils ont un rapport de 4:1 - dans ce rapport, nous gaspillons de la bande passante mémoire. Et puis il s'avère que si je le fais String.split() – ce n'est pas la dernière fois que je fais ça, il peut y avoir encore 6 à 7 champs à l'intérieur. Ainsi, le code classique consistant à lire CSV puis à analyser les chaînes entraîne un gaspillage de bande passante mémoire d'environ 14: 1 par rapport à ce que vous aimeriez réellement avoir. Si vous jetez ces sélections, vous pouvez obtenir une accélération quintuplée.

Et ce n'est pas si difficile. Si vous regardez le code sous le bon angle, tout devient assez simple une fois que vous réalisez le problème. Vous ne devriez pas arrêter complètement d'allouer de la mémoire : le seul problème est que vous allouez quelque chose et il meurt immédiatement, et en cours de route, il brûle une ressource importante, 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 cycles du processeur, mais ici, vous avez brûlé toute la mémoire beaucoup plus tôt. La solution consiste à réduire la quantité de rejets. 
L'autre partie du problème est que si vous exécutez le profileur lorsque la bande de mémoire est épuisée, juste au moment où cela se produit, vous attendez généralement que le cache revienne parce qu'il est plein de déchets que vous venez de produire, toutes ces lignes. Par conséquent, chaque opération de chargement ou de stockage devient lente, car elle entraîne des échecs de cache - l'ensemble du cache est devenu lent, attendant que les déchets le quittent. Par conséquent, le profileur affichera simplement un bruit aléatoire chaud réparti tout au long de la boucle entière - il n'y aura pas d'instruction chaude ou d'endroit séparé dans le code. Seulement du bruit. Et si vous regardez les cycles GC, ils sont 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 il les coupe, et les coupe, et les coupe encore. Tout cela arrive très vite. Il s'avère qu'il existe des cycles GC bon marché, un bruit chaud tout au long du cycle, mais nous voulons obtenir une accélération 5x. À ce moment-là, quelque chose devrait se fermer dans votre tête et sonner : « pourquoi ça ?! » Le dépassement de bande mémoire n'est pas affiché dans le débogueur classique, vous devez exécuter le débogueur du compteur de performances matérielles et le constater vous-même et directement. Mais cela ne peut être directement suspecté à partir de ces trois symptômes. Le troisième symptôme est que lorsque vous regardez ce que vous mettez en surbrillance, demandez au profileur, et il répond : « Vous avez créé un milliard de lignes, mais le GC a fonctionné gratuitement. Dès que cela se produit, vous réalisez que vous avez créé trop d’objets et brûlé tout le chemin de la mémoire. Il existe un moyen de comprendre cela, mais ce n'est pas évident. 

Le problème réside dans la structure des données : la structure nue sous-jacente à tout ce qui se passe, elle est trop grande, elle fait 2.7 Go sur le disque, donc faire une copie de cette chose est très indésirable - vous voulez la charger immédiatement à partir du tampon d'octets du réseau dans les registres, afin de ne pas lire-écrire sur la ligne cinq fois. Malheureusement, Java ne vous propose pas par défaut une telle bibliothèque dans le cadre du JDK. Mais c'est trivial, non ? Essentiellement, il s'agit de 5 à 10 lignes de code qui seront utilisées pour implémenter votre propre chargeur de chaîne tamponné, qui répète le comportement de la classe de chaîne, 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 les pointeurs vers le tampon s'y déplacent, et les octets bruts ne sont copiés nulle part, et donc les mêmes tampons sont réutilisés encore et encore, et le système d'exploitation est heureux de prendre en charge les choses pour lesquelles il a été conçu, comme la double mise en mémoire tampon cachée de ces tampons d'octets, et vous n'êtes plus en train de parcourir un flux infini de données inutiles. Au fait, comprenez-vous qu'en travaillant avec GC, il est garanti que chaque allocation de mémoire ne sera pas visible par le processeur après le dernier cycle GC ? Par conséquent, tout cela ne peut pas être dans le cache, et alors un échec garanti à 100 % se produit. Lorsque vous travaillez avec un pointeur, sur x86, soustraire un registre de la mémoire prend 1 à 2 cycles d'horloge, et dès que cela se produit, vous payez, payez, payez, car toute la mémoire est allumée NEUF caches – et c'est le coût de l'allocation de mémoire. Valeur réelle.

En d’autres termes, les structures de données sont la chose la plus difficile à modifier. Et une fois que vous réalisez que vous avez choisi la mauvaise structure de données qui nuira aux performances plus tard, il y a généralement beaucoup de travail à faire, mais si vous ne le faites pas, les choses empireront. Tout d’abord, vous devez penser aux structures de données, c’est important. Le coût principal ici repose sur les grosses structures de données, qui commencent à être utilisées dans le style de « J'ai copié la structure de données X dans la structure de données Y parce que j'aime mieux la forme de Y ». Mais l'opération de copie (qui semble bon marché) gaspille en réalité de la bande passante mémoire et c'est là que tout le temps d'exécution perdu est enterré. Si j'ai une chaîne géante de JSON et que je veux la transformer en une arborescence DOM structurée de POJO ou quelque chose comme ça, l'opération d'analyse de cette chaîne et de construction du POJO, puis d'accéder à nouveau au POJO plus tard, entraînera des coûts inutiles - c'est pas cher. Sauf si vous courez autour des POJO beaucoup plus souvent que sur une chaîne. À la place, vous pouvez essayer de décrypter la chaîne et d'en extraire uniquement ce dont vous avez besoin, sans la transformer en POJO. Si tout cela se produit sur un chemin à partir duquel des performances maximales sont requises, pas de POJO pour vous, vous devez d'une manière ou d'une autre creuser directement dans la ligne.

Pourquoi créer votre propre langage de programmation

Andrew: Vous avez dit que pour comprendre le modèle de coût, il fallait écrire son propre petit langage...

falaise: Pas un langage, mais un compilateur. Un langage et un compilateur sont deux choses différentes. La différence la plus importante est dans votre tête. 

Andrew: Au fait, pour autant que je sache, vous expérimentez la création de vos propres langages. Pour quoi?

falaise: Parce que je peux! Je suis semi-retraité, c'est donc mon passe-temps. J'ai mis en œuvre les langues des autres toute ma vie. J'ai également beaucoup travaillé sur mon style de codage. Et aussi parce que je vois des problèmes dans d'autres langues. Je vois qu'il existe de meilleures façons de faire des choses familières. Et je les utiliserais. Je suis juste fatigué de voir des problèmes en moi-même, en Java, en Python, dans n'importe quel autre langage. J'écris maintenant en React Native, JavaScript et Elm comme passe-temps qui ne concerne pas la retraite, mais le travail actif. 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 elles possèdent toutes des fonctionnalités intéressantes. Chacun est bon à sa manière et vous pouvez essayer de réunir toutes ces fonctionnalités. Donc, j'étudie des choses qui m'intéressent, le comportement du langage, en essayant de trouver une sémantique raisonnable. Et jusqu'à présent, j'ai réussi ! Pour le moment, j'ai du mal avec la sémantique de la mémoire, car je veux l'avoir comme en C et Java, et obtenir un modèle de mémoire et une sémantique de mémoire solides pour les charges et les magasins. En même temps, ayez une inférence de type automatique comme dans Haskell. Ici, j'essaie de mélanger l'inférence de type Haskell avec le travail de mémoire en C et Java. C’est ce que je fais depuis 2-3 mois par exemple.

Andrew: Si vous construisez un langage qui prend de meilleurs aspects d'autres langages, pensez-vous que quelqu'un fera le contraire : prendre vos idées et les utiliser ?

falaise: C'est exactement ainsi que de nouvelles langues apparaissent ! Pourquoi Java est-il similaire au C ? Parce que C avait une bonne syntaxe que tout le monde comprenait et que Java s'est inspiré de cette syntaxe, en ajoutant la sécurité des types, la vérification des limites des tableaux, le GC, et ils ont également amélioré certaines choses de C. Ils ont ajouté les leurs. Mais ils ont été beaucoup inspirés, non ? Tout le monde s’appuie sur les épaules des géants qui vous ont précédés : c’est ainsi que l’on progresse.

Andrew: Si je comprends bien, votre langue sera en sécurité en mémoire. Avez-vous pensé à implémenter quelque chose comme un vérificateur d'emprunt de Rust ? L'avez-vous regardé, que pensez-vous de lui ?

falaise: Eh bien, j'écris du C depuis des lustres, avec tout ce malloc et gratuit, et je gère manuellement la durée de vie. Vous savez, 90 à 95 % de la durée de vie contrôlée manuellement a la même structure. Et c'est très, très pénible de le faire manuellement. J'aimerais que le compilateur vous dise simplement ce qui s'y passe et ce que vous avez réalisé grâce à vos actions. Pour certaines choses, le vérificateur d'emprunt le fait immédiatement. Et il devrait afficher automatiquement les informations, tout comprendre et ne même pas me charger de présenter cette compréhension. Il doit effectuer au moins une analyse d'échappement locale, et seulement en cas d'échec, il doit alors ajouter des annotations de type qui décriront la durée de vie - et un tel schéma est beaucoup plus complexe qu'un vérificateur d'emprunt, ou même n'importe quel vérificateur de mémoire existant. Le choix entre « tout va bien » et « je ne comprends rien » - non, il doit y avoir quelque chose de mieux. 
Donc, en tant que personne ayant écrit beaucoup de code en C, je pense que la prise en charge du contrôle automatique de la durée de vie est la chose la plus importante. J'en ai également marre de la quantité de mémoire utilisée par Java et le principal reproche concerne le GC. Lorsque vous allouez de la mémoire en Java, vous ne récupérerez pas la mémoire qui était locale lors du dernier cycle GC. Ce n'est pas le cas dans les langages dotés d'une gestion de la mémoire plus précise. Si vous appelez malloc, vous obtenez immédiatement la mémoire qui vient habituellement d'être utilisée. Habituellement, vous effectuez des tâches temporaires avec la mémoire et la restituez immédiatement. Et il retourne immédiatement au pool malloc, et le cycle malloc suivant le retire à nouveau. Par conséquent, l’utilisation réelle de la mémoire est réduite à l’ensemble des objets vivants à un instant donné, plus les fuites. Et si tout ne fuit pas de manière totalement indécente, l’essentiel de la mémoire finit dans les caches et le processeur, et cela fonctionne rapidement. Mais nécessite beaucoup de gestion manuelle de la mémoire avec malloc et free appelés dans le bon ordre, au bon endroit. Rust peut gérer cela correctement tout seul et, dans de nombreux cas, offrir des performances encore meilleures, puisque la consommation de mémoire est limitée au calcul en cours - au lieu d'attendre le prochain cycle 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 - je veux dire, j'ai fait de telles choses lors du traitement de données pour la fintech, et cela m'a permis d'obtenir une accélération d'environ cinq fois. C'est un gros coup de pouce, surtout dans un monde où les processeurs ne deviennent pas plus rapides et où nous attendons toujours des améliorations.

Carrière d’ingénieur de performance

Andrew: J'aimerais aussi poser des questions sur les carrières en général. Vous avez pris de l'importance grâce à votre travail JIT chez HotSpot, puis avez rejoint Azul, qui est également une société JVM. Mais nous travaillions déjà davantage sur le matériel que sur le logiciel. Et puis ils sont soudainement passés 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.

falaise: Je programme depuis assez longtemps et j'ai réussi à suivre beaucoup de cours différents. Et quand les gens disent : « oh, c'est toi qui as fait du JIT pour Java ! », c'est toujours drôle. Mais avant cela, je travaillais sur un clone de PostScript, le langage qu'Apple utilisait autrefois pour ses imprimantes laser. Et avant cela, j'ai fait une implémentation du langage 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 le développement de systèmes d'exploitation, qui au début étaient triviaux, mais qui sont devenus de plus en plus complexes au fil du temps. Mais le sujet principal reste le développement d’outils. Une grande partie de ma vie s'est déroulée entre Azul et Sun, et c'était autour de Java. Mais quand je me suis lancé dans le Big Data et l’apprentissage automatique, j’ai remis mon chapeau et j’ai dit : « Oh, maintenant nous avons un problème non trivial, et il se passe beaucoup de choses intéressantes et des gens font des choses. » C’est une excellente voie de développement à suivre.

Oui, j'aime vraiment l'informatique distribuée. Mon premier emploi était en tant qu'étudiant en C, sur un projet publicitaire. Il s'agissait d'un calcul distribué sur des puces Zilog Z80 qui collectaient des données pour l'OCR analogique, produites par un véritable analyseur analogique. C'était un sujet cool et complètement fou. Mais il y avait des problèmes, une partie n'était pas reconnue correctement, donc il fallait prendre une photo et la montrer à une personne qui pouvait déjà lire avec ses yeux et rapporter ce qu'elle disait, et donc il y avait des travaux avec des données, et ces travaux avaient leur propre langue. Il y avait un backend qui traitait tout cela - des Z80 fonctionnant en parallèle avec des terminaux vt100 fonctionnant - un par personne, et il y avait un modèle de programmation parallèle sur le Z80. Un élément de mémoire commun partagé par tous les Z80 dans une configuration en étoile ; Le fond de panier était également partagé, et la moitié de la RAM était partagée au sein du réseau, et l'autre moitié était privée ou allait à autre chose. Un système distribué parallèle significativement complexe avec une mémoire partagée... semi-partagée. Quand était-ce... Je ne m'en souviens même pas, quelque part au milieu des années 80. Il y a bien longtemps. 
Oui, supposons que 30 ans, c’est il y a assez longtemps. Les problèmes liés à l’informatique distribuée existent depuis assez longtemps ; les gens sont depuis longtemps en guerre contre La légende de Beowulf-groupes. De tels clusters ressemblent à... Par exemple : il y a Ethernet et votre x86 rapide est connecté à cet Ethernet, et maintenant vous voulez obtenir une fausse mémoire partagée, car personne ne pouvait alors faire du codage informatique distribué, c'était trop difficile et donc là était une fausse mémoire partagée avec des pages de mémoire de protection sur x86, et si vous écriviez sur cette page, nous disions aux autres processeurs que s'ils accédaient à la même mémoire partagée, elle devrait être chargée par vous, et donc quelque chose comme un protocole de prise en charge la cohérence du cache est apparue et un logiciel pour cela. Notion intéressante. Le vrai problème, bien entendu, était autre chose. Tout cela a fonctionné, mais vous avez rapidement eu des problèmes de performances, car personne ne comprenait les modèles de performances à un niveau suffisamment bon - quels étaient les modèles d'accès à la mémoire, comment s'assurer que les nœuds ne se cinglaient pas sans fin, etc.

Ce que j'ai découvert dans H2O, c'est que ce sont les développeurs eux-mêmes qui sont chargés de déterminer où le parallélisme est caché et où il ne l'est pas. J'ai proposé un modèle de codage qui rendait l'écriture de code haute performance facile et simple. Mais écrire du code lent est difficile, cela aura l'air mauvais. Vous devez sérieusement essayer d'écrire du code lent, vous devrez utiliser des méthodes non standard. Le code de freinage est visible au premier coup d'œil. Par conséquent, vous écrivez généralement du code qui s’exécute rapidement, mais vous devez savoir quoi faire dans le cas d’une mémoire partagée. Tout cela est lié aux grands tableaux et le comportement y est similaire à celui des 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, en conséquence, perd, et vous ne savez pas lequel est lequel. S'ils ne sont pas volatils, l'ordre peut être celui que vous voulez - et cela fonctionne très bien. Les gens se soucient vraiment de l'ordre des opérations, ils placent les volatiles aux bons endroits et s'attendent à des problèmes de performances liés à la mémoire aux bons endroits. Sinon, ils écriraient simplement du code sous forme de boucles de 1 à N, où N vaut quelques milliards, dans l'espoir que tous les cas complexes deviendront automatiquement parallèles - et cela ne fonctionne pas là-bas. 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 clair et similaire à l’écriture de code simple en C ou Java avec des boucles et des tableaux. Mais en même temps, la mémoire peut être traitée en téraoctets. J'utilise toujours H2O. Je l'utilise de temps en temps dans différents projets - et c'est toujours l'appareil le plus rapide, des dizaines de fois plus rapide que ses concurrents. Si vous faites du Big Data avec des données en colonnes, il est très difficile de battre H2O.

Défis techniques

Andrew: Quel a été votre plus grand défi tout au long de votre carrière ?

falaise: Discutons-nous de la partie technique ou non technique du problème ? Je dirais que les plus grands défis ne sont pas techniques. 
Quant aux défis techniques. Je les ai simplement vaincus. Je ne sais même pas quel était le plus gros, mais il y en avait quelques-uns assez intéressants qui ont pris pas mal de temps, de lutte mentale. Quand je suis allé chez Sun, j'étais sûr que je créerais un compilateur rapide, et un groupe de seniors ont répondu que je ne réussirais jamais. Mais j'ai suivi ce chemin, j'ai écrit un compilateur dans l'allocateur de registre, et c'était assez rapide. C'était aussi rapide que le C1 moderne, mais l'allocateur était beaucoup plus lent à l'époque, et avec le recul, il s'agissait d'un problème important de structure de données. J'en avais besoin pour écrire un allocateur de registre graphique et je ne comprenais pas le dilemme entre l'expressivité du code et la vitesse, qui existait à cette époque et était très important. Il s'est avéré que la structure des données dépasse généralement la taille du cache sur les x86 de cette époque, et par conséquent, si je supposais initialement que l'allocateur de registre travaillerait sur 5 à 10 % du temps de gigue total, alors en réalité, cela s'est avéré être 50 pourcent.

Au fil du temps, le compilateur est devenu plus propre et plus efficace, a cessé de générer du code épouvantable dans un plus grand nombre de cas et les performances ont commencé à ressembler de plus en plus à celles produites par un compilateur C. À moins, bien sûr, que vous écriviez des conneries que même C n'accélère pas . Si vous écrivez du code comme C, vous obtiendrez des performances comme C dans plus de cas. Et plus vous alliez loin, plus vous obteniez du code qui coïncidait asymptotiquement avec le niveau C, l'allocateur de registre commençait à ressembler à quelque chose de complet... que votre code s'exécute rapidement ou lentement. J'ai continué à travailler sur l'allocateur pour lui faire faire de meilleures sélections. Il est devenu de plus en plus lent, mais il a donné de meilleures performances dans les cas où personne d'autre ne pouvait y faire face. Je pourrais plonger dans un allocateur de registre, y enterrer un mois de travail, et tout à coup, tout le code commencerait à s'exécuter 5 % plus rapidement. Cela s'est produit à maintes reprises et le répartiteur de registre est devenu une sorte d'œuvre d'art - tout le monde l'a aimé ou détesté, et les gens de l'académie ont posé des questions sur le thème « pourquoi tout est fait de cette façon », pourquoi pas balayage de ligne, et quelle est la différence. La réponse est toujours la même : un allocateur basé sur la coloration des graphiques et un travail très minutieux avec le code tampon équivaut à une arme de victoire, la meilleure combinaison que personne ne puisse vaincre. Et c’est une chose plutôt peu évidente. Tout le reste que fait le compilateur là-bas est des choses assez bien étudiées, même si elles ont également été portées au niveau de l'art. J'ai toujours fait des choses censées transformer le compilateur en une œuvre d'art. Mais rien de tout cela n’avait quelque chose d’extraordinaire – à l’exception du répartiteur du registre. L'astuce est d'être prudent réduire sous charge et, si cela se produit (je peux l'expliquer plus en détail si vous êtes intéressé), cela signifie que vous pouvez vous aligner de manière plus agressive, sans risquer de tomber sur un problème dans le calendrier des performances. À cette époque, il y avait un tas de compilateurs à grande échelle, ornés de bibelots et de sifflets, dotés d'allocateurs de registre, mais personne d'autre ne pouvait le faire.

Le problème est que si vous ajoutez des méthodes soumises à l'inline, en augmentant et en augmentant la zone d'inline, l'ensemble des valeurs utilisées dépasse instantanément le nombre de registres et vous devez les couper. Le niveau critique survient généralement lorsque l'allocateur abandonne, et qu'un bon candidat pour un déversement en vaut un autre, vous vendrez des choses généralement folles. La valeur de l'inline ici est que vous perdez une partie des frais généraux, des frais généraux d'appel et de sauvegarde, vous pouvez voir les valeurs à l'intérieur et les optimiser davantage. Le coût de l'inline est qu'un grand nombre de valeurs réelles sont formées, et si votre allocateur de registre brûle plus que nécessaire, vous perdez immédiatement. Par conséquent, la plupart des répartiteurs ont un problème : lorsque l’inline franchit une certaine ligne, tout dans le monde commence à être réduit et la productivité peut être jetée dans les toilettes. Ceux qui implémentent le compilateur ajoutent quelques heuristiques : par exemple, arrêter l'inline, en commençant par une taille suffisamment grande, car les allocations gâcheront tout. C'est ainsi que se forme un coude dans le graphique de performances - vous inline, inline, les performances augmentent lentement - et puis boum ! – il tombe comme un cric rapide parce que vous avez trop aligné. C'est ainsi que tout fonctionnait avant l'avènement de Java. Java nécessite beaucoup plus d'inline, j'ai donc dû rendre mon allocateur beaucoup plus agressif pour qu'il se stabilise plutôt que de planter, et si vous en inlinez trop, il commence à se répandre, mais le moment « plus de déversement » arrive toujours. C’est une observation intéressante et elle m’est venue de nulle part, pas évidente, mais cela a bien payé. J'ai adopté l'inline agressive et cela m'a amené dans des endroits où les performances Java et C fonctionnent côte à côte. Ils sont très proches – je peux écrire du code Java qui est nettement plus rapide que le code C et des choses comme ça, mais en moyenne, dans l’ensemble, ils sont à peu près comparables. Je pense qu'une partie de ce mérite réside dans l'allocateur de registre, qui me permet d'intégrer le plus bêtement possible. J'intègre simplement tout ce que je vois. La question ici est de savoir si l’allocateur fonctionne bien, si le résultat est un code qui fonctionne intelligemment. C'était un grand défi : comprendre tout cela et faire en sorte que cela fonctionne.

Un peu sur l'allocation des registres et les multicœurs

Vladimir: Les problèmes tels que l'allocation des registres semblent être une sorte de sujet éternel et sans fin. Je me demande s’il y a déjà eu une idée qui semblait prometteuse et qui a ensuite échoué dans la pratique ?

falaise: Certainement! L'allocation de registre est un domaine dans lequel vous essayez de trouver des heuristiques pour résoudre un problème NP-complet. Et on ne peut jamais parvenir à une solution parfaite, n’est-ce pas ? C'est tout simplement impossible. Regardez, la compilation Ahead of Time - elle fonctionne également mal. La conversation ici porte sur quelques cas moyens. À propos des performances typiques, afin que vous puissiez mesurer quelque chose que vous pensez être une bonne performance typique - après tout, vous travaillez pour l'améliorer ! L'allocation de registres est un sujet entièrement axé sur les performances. Une fois que vous avez le premier prototype, il fonctionne et peint ce qui est nécessaire, le travail de performance commence. Il faut apprendre à bien mesurer. Pourquoi c'est important? Si vous disposez de données claires, vous pouvez regarder différents domaines et voir : oui, ça a aidé ici, mais c’est là que tout s’est cassé ! De bonnes idées surgissent, vous ajoutez de nouvelles heuristiques et tout d’un coup, tout commence à fonctionner un peu mieux en moyenne. Ou alors, il ne démarre pas. J'ai eu de nombreux cas où nous nous battions pour obtenir les cinq pour cent de performance qui différenciaient notre développement de l'allocateur précédent. Et à chaque fois, cela ressemble à ceci : quelque part vous gagnez, quelque part vous perdez. Si vous disposez de bons outils d’analyse des performances, vous pouvez trouver les idées perdantes et comprendre pourquoi elles échouent. Cela vaut peut-être la peine de tout laisser tel quel, ou peut-être d'adopter une approche plus sérieuse pour peaufiner le tout, ou de sortir et de réparer autre chose. C'est tout un tas de choses ! J'ai fait ce hack sympa, mais j'ai aussi besoin de celui-ci, et de celui-ci, et de celui-ci - et leur combinaison totale donne quelques améliorations. Et les solitaires peuvent échouer. C'est la nature du travail de performance sur les problèmes NP-complets.

Vladimir: On a le sentiment que des choses comme la peinture dans les répartiteurs sont un problème qui a déjà été résolu. Eh bien, c'est décidé pour vous, à en juger par ce que vous dites, alors est-ce que ça vaut le coup...

falaise: Ce n'est pas résolu en tant que tel. C'est vous qui devez le transformer en « résolu ». Il existe des problèmes difficiles et il faut les résoudre. Une fois cela fait, il est temps de travailler sur la productivité. Vous devez aborder ce travail en conséquence - effectuer des benchmarks, collecter des métriques, expliquer les situations dans lesquelles, lorsque vous êtes revenu à une version précédente, votre ancien hack a recommencé à fonctionner (ou vice versa, s'est arrêté). Et n'abandonnez pas tant que vous n'avez pas réussi quelque chose. Comme je l'ai déjà dit, s'il y a des idées sympas qui n'ont pas fonctionné, mais dans le domaine de la répartition des registres d'idées, c'est à peu près infini. Vous pouvez par exemple lire des publications scientifiques. Bien que maintenant, ce domaine ait commencé à évoluer beaucoup plus lentement et soit devenu plus clair que dans sa jeunesse. Cependant, d’innombrables personnes travaillent dans ce domaine et toutes leurs idées valent la peine d’être essayées, elles attendent toutes dans les coulisses. Et vous ne pouvez pas dire à quel point ils sont bons à moins de les essayer. Dans quelle mesure s'intègrent-ils bien à tout le reste de votre allocateur, car un allocateur fait beaucoup de choses et certaines idées ne fonctionneront pas dans votre allocateur spécifique, mais dans un autre allocateur, elles fonctionneront facilement. Le principal moyen de gagner pour l'allocateur est de retirer les éléments lents en dehors du chemin principal et de les forcer à se diviser le long des limites des chemins lents. Donc, si vous voulez exécuter un GC, emprunter le chemin lent, désoptimiser, lever une exception, tout ça - vous savez que ces choses sont relativement rares. Et ils sont vraiment rares, j'ai vérifié. Vous effectuez un travail supplémentaire et cela supprime de nombreuses restrictions sur ces chemins lents, mais cela n'a pas vraiment d'importance car ils sont lents et rarement parcourus. Par exemple, un pointeur nul – cela n’arrive jamais, n’est-ce pas ? Vous devez avoir plusieurs chemins pour différentes choses, mais ils ne doivent pas interférer avec le chemin principal. 

Vladimir: Que pensez-vous du multicœur, quand il y a des milliers de cœurs à la fois ? Est-ce une chose utile ?

falaise: Le succès du GPU montre qu'il est plutôt utile !

Vladimir: Ils sont assez spécialisés. Qu’en est-il des processeurs à usage général ?

falaise: Eh bien, c'était le modèle économique d'Azul. La réponse est revenue à une époque où les gens aimaient vraiment les performances prévisibles. Il était alors difficile d’écrire du code parallèle. Le modèle de codage H2O est hautement évolutif, mais il ne s’agit pas d’un modèle à usage général. Peut-être un peu plus général que lors de l'utilisation d'un GPU. Parlons-nous de la complexité de développer une telle chose ou de la complexité de son utilisation ? Par exemple, Azul m'a appris une leçon intéressante, mais pas évidente : les petites caches sont normales. 

Le plus grand défi de la vie

Vladimir: Qu’en est-il des défis non techniques ?

falaise: Le plus grand défi n'était pas d'être... gentil et gentil avec les gens. Et du coup, je me suis retrouvé constamment dans des situations extrêmement conflictuelles. Ceux où je savais que les choses allaient mal, mais je ne savais pas comment résoudre ces problèmes et je ne pouvais pas les gérer. De nombreux problèmes à long terme, qui ont duré des décennies, sont ainsi survenus. Le fait que Java dispose de compilateurs C1 et C2 en est une conséquence directe. Le fait qu’il n’y ait pas eu de compilation multi-niveaux en Java pendant dix années consécutives est également une conséquence directe. Il est évident que nous avions besoin d’un tel système, mais la raison pour laquelle il n’existait pas n’est pas évidente. J'ai eu des problèmes avec un ingénieur... ou un groupe d'ingénieurs. Il était une fois, quand j'ai commencé à travailler chez Sun, j'étais... D'accord, pas seulement à ce moment-là, j'ai généralement toujours ma propre opinion sur tout. Et je pensais qu'il était vrai que tu pouvais simplement prendre ta vérité et la dire de front. D’autant plus que j’avais incroyablement raison la plupart du temps. Et si vous n’aimez pas cette approche… surtout si vous vous trompez manifestement et faites des bêtises… En général, peu de gens pourraient tolérer cette forme de communication. Même si certains le pourraient, comme moi. J'ai construit toute ma vie sur des principes méritocratiques. Si vous me montrez quelque chose qui ne va pas, je me retournerai immédiatement et je dirai : vous avez dit des bêtises. En même temps, bien sûr, je m'excuse et tout ça, je noterai les mérites, le cas échéant, et prendrai d'autres mesures correctes. D’un autre côté, j’ai incroyablement raison sur un pourcentage incroyablement élevé du temps total. Et cela ne fonctionne pas très bien dans les relations avec les gens. Je n’essaie pas d’être gentil, mais je pose la question sans détour. "Cela ne marchera jamais, car un, deux et trois." Et ils disaient : « Oh ! Il y avait d’autres conséquences qu’il valait probablement mieux ignorer : par exemple, celles qui ont conduit au divorce d’avec ma femme et à dix ans de dépression par la suite.

Le défi est une lutte avec les gens, avec leur perception de ce que vous pouvez ou ne pouvez pas faire, de ce qui est important et de ce qui ne l'est pas. Il y avait de nombreux défis concernant le style de codage. J'écris encore beaucoup de code, et à cette époque, je devais même ralentir parce que je faisais trop de tâches parallèles et que je les faisais mal, au lieu de me concentrer sur une seule. Avec le recul, j'ai écrit la moitié du code de la commande Java JIT, la commande C2. Le codeur le plus rapide suivant écrivait moitié moins lentement, le suivant moitié moins lent, et ce fut un déclin exponentiel. La septième personne dans cette rangée était très, très lente – cela arrive toujours ! J'ai touché beaucoup de code. J'ai regardé qui a écrit quoi, sans exception, j'ai regardé leur code, j'ai examiné chacun d'eux et j'ai quand même continué à écrire plus moi-même que n'importe lequel d'entre eux. Cette approche ne fonctionne pas très bien avec les gens. Certaines personnes n'aiment pas ça. Et quand ils n’y parviennent pas, toutes sortes de plaintes commencent. Par exemple, on m'a dit un jour d'arrêter de coder parce que j'écrivais trop de code et que cela mettait l'équipe en danger, et tout cela m'a semblé être une blague : mec, si le reste de l'équipe disparaît et que je continue d'écrire du code, tu Je ne perdrai que la moitié des équipes. D'un autre côté, si je continue à écrire du code et que vous perdez la moitié de l'équipe, cela ressemble à une très mauvaise gestion. Je n’y ai jamais vraiment pensé, je n’en ai jamais parlé, mais c’était encore quelque part dans ma tête. La pensée tournait au fond de mon esprit : « Vous vous moquez de moi ? Donc le plus gros problème, c’était moi et mes relations avec les gens. Maintenant, je me comprends beaucoup mieux, j'ai longtemps été chef d'équipe de programmeurs, et maintenant je dis directement aux gens : vous savez, je suis qui je suis, et vous devrez vous occuper de moi - est-ce que ça va si je me tiens debout ici? Et quand ils ont commencé à s’en occuper, tout a fonctionné. En fait, je ne suis ni mauvais ni bon, je n’ai pas de mauvaises intentions ni d’aspirations égoïstes, c’est juste mon essence et je dois vivre avec d’une manière ou d’une autre.

Andrew: Tout récemment, tout le monde a commencé à parler de conscience de soi pour les introvertis et de compétences générales en général. Que pouvez-vous dire à ce sujet ?

falaise: Oui, c'est la perspicacité et la leçon que j'ai tirées de mon divorce avec ma femme. Ce que j'ai appris du divorce, c'est de me comprendre. C’est ainsi que j’ai commencé à comprendre les autres. Comprenez comment fonctionne cette interaction. Cela a conduit à des découvertes les unes après les autres. Il y avait une prise de conscience de qui je suis et de ce que je représente. Qu'est-ce que je fais : soit je suis préoccupé par la tâche, soit j'évite les conflits, soit autre chose - et ce niveau de conscience de soi m'aide vraiment à garder le contrôle. Après cela, tout devient beaucoup plus facile. Une chose que j'ai découverte non seulement chez moi, mais aussi chez d'autres programmeurs, c'est l'incapacité de verbaliser ses pensées lorsque l'on est dans un état de stress émotionnel. Par exemple, vous êtes assis là à coder, dans un état de flux, puis ils courent vers vous et commencent à crier de manière hystérique que quelque chose est cassé et que maintenant des mesures extrêmes seront prises contre vous. Et vous ne pouvez pas dire un mot parce que vous êtes dans un état de stress émotionnel. Les connaissances acquises permettent de se préparer à ce moment, d'y survivre et de passer à un plan de retraite, après quoi vous pourrez faire quelque chose. Alors oui, lorsque vous commencez à comprendre comment tout cela fonctionne, c’est un énorme événement qui change la vie. 
Je n'ai moi-même pas trouvé les mots justes, mais je me suis souvenu de la séquence d'actions. Le fait est que cette réaction est autant physique que verbale et que vous avez besoin d’espace. Un tel espace, au sens zen. C'est exactement ce qu'il faut expliquer, puis s'écarter immédiatement - s'éloigner purement physiquement. Lorsque je reste verbalement silencieux, je peux gérer la situation émotionnellement. Lorsque l'adrénaline atteint votre cerveau, vous fait passer en mode combat ou fuite, vous ne pouvez plus rien dire, non - maintenant vous êtes un idiot, un ingénieur fouet, incapable de réagir correctement ou même d'arrêter l'attaque, et l'attaquant est libre. pour attaquer encore et encore. Il faut d’abord redevenir soi-même, reprendre le contrôle, sortir du mode « combat ou fuite ».

Et pour cela nous avons besoin d’espace verbal. Juste de l'espace libre. Si vous dites n'importe quoi, alors vous pouvez dire exactement cela, puis aller vraiment trouver un « espace » pour vous-même : faire une promenade dans le parc, vous enfermer sous la douche - cela n'a pas d'importance. L’essentiel est de se déconnecter temporairement de cette situation. Dès que vous vous éteignez pendant au moins quelques secondes, le contrôle revient, vous commencez à réfléchir sobrement. "D'accord, je ne suis pas une sorte d'idiot, je ne fais pas de bêtises, je suis une personne plutôt utile." Une fois que vous avez réussi à vous convaincre, il est temps de passer à l’étape suivante : comprendre ce qui s’est passé. Vous avez été attaqué, l’attaque est venue d’où vous ne vous y attendiez pas, c’était une embuscade malhonnête et ignoble. C'est mauvais. L’étape suivante consiste à comprendre pourquoi l’attaquant en avait besoin. Vraiment pourquoi? Peut-être parce qu'il est lui-même furieux ? Pourquoi est-il fou ? Par exemple, parce qu'il s'est trompé et ne peut pas accepter ses responsabilités ? C’est la manière de gérer soigneusement l’ensemble de la situation. Mais cela nécessite une marge de manœuvre, un espace verbal. La toute première étape consiste à rompre le contact verbal. Évitez les discussions avec des mots. Annulez-le, partez le plus vite possible. S'il s'agit d'une conversation téléphonique, raccrochez - c'est une compétence que j'ai acquise en communiquant avec mon ex-femme. Si la conversation ne mène à rien de bon, dites simplement « au revoir » et raccrochez. De l’autre côté du téléphone : « bla bla bla », vous répondez : « ouais, au revoir ! et raccroche. Vous venez de mettre fin à la conversation. Cinq minutes plus tard, lorsque la capacité de penser raisonnablement vous revient, que vous vous êtes un peu refroidi, il devient possible de penser à tout, à ce qui s'est passé et à ce qui se passera ensuite. Et commencez à formuler une réponse réfléchie, plutôt que de simplement réagir par émotion. Pour moi, la percée dans la conscience de soi était précisément le fait qu'en cas de stress émotionnel, je ne pouvais pas parler. Sortir de cet état, réfléchir et planifier comment réagir et compenser les problèmes - ce sont les bonnes étapes dans le cas où vous ne pouvez pas parler. Le moyen le plus simple est de fuir la situation dans laquelle le stress émotionnel se manifeste et de simplement cesser de participer à ce stress. Après cela, vous devenez capable de penser, quand vous pouvez penser, vous devenez capable de parler, et ainsi de suite.

D'ailleurs, au tribunal, l'avocat adverse essaie de vous faire cela - on comprend maintenant pourquoi. Parce qu’il a la capacité de vous réprimer à un point tel que vous ne pouvez même pas prononcer votre nom, par exemple. Dans un sens très réel, vous ne pourrez pas parler. Si cela vous arrive, et si vous savez que vous vous retrouverez dans un endroit où les batailles verbales font rage, dans un lieu comme un tribunal, alors vous pouvez venir avec votre avocat. L'avocat prendra votre défense et mettra fin à l'attaque verbale, et le fera de manière tout à fait légale, et l'espace Zen perdu vous reviendra. Par exemple, j’ai dû appeler ma famille à plusieurs reprises, le juge s’est montré plutôt amical à ce sujet, mais l’avocat de la partie adverse m’a crié dessus, je n’ai même pas réussi à faire passer un mot. Dans ces cas-là, le recours à un médiateur me convient le mieux. Le médiateur arrête toute cette pression qui s'abat sur vous en flux continu, vous retrouvez l'espace zen nécessaire, et avec lui la capacité de parler revient. Il s'agit de tout un domaine de connaissances dans lequel il y a beaucoup à étudier, beaucoup à découvrir en soi, et tout cela se transforme en décisions stratégiques de haut niveau qui sont différentes selon les personnes. Certaines personnes n’ont pas les problèmes décrits ci-dessus ; généralement, les vendeurs professionnels n’en ont pas. Tous ces gens qui vivent des mots – chanteurs, poètes, chefs religieux et hommes politiques célèbres – ont toujours quelque chose à dire. Ils n'ont pas de tels problèmes, mais moi oui.

Andrew: C'était... inattendu. Super, nous avons déjà beaucoup parlé et il est temps de terminer cette interview. Nous nous retrouverons certainement à la conférence et pourrons poursuivre ce dialogue. Rendez-vous à Hydra!

Vous pouvez poursuivre votre conversation avec Cliff lors de la conférence Hydra 2019, qui se tiendra les 11 et 12 juillet 2019 à Saint-Pétersbourg. Il viendra avec un rapport "L'expérience de mémoire transactionnelle Azul Hardware". Les billets peuvent être achetés sur le site officiel.

Source: habr.com

Ajouter un commentaire