Grande interview avec Cliff Click — le père de la compilation JIT en Java

Grande interview avec Cliff Click — le père de la compilation JIT en JavaCliff Click — CTO de Cratus (capteurs IoT pour améliorer les processus), fondateur et cofondateur de plusieurs startups (y compris 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)! Il est surtout connu pour son travail sur C2 en Java (the Sea of Nodes IR). Ce compilateur a montré au monde que le JIT pouvait produire du code de qualité, ce qui a été un des facteurs contribuant à faire de Java l'une des principales plates-formes logicielles modernes. Par la suite, Cliff a aidé l'entreprise Azul Systems à construire un mainframe à 864 cœurs avec un logiciel écrit en Java pur, qui maintenait des pauses GC sur un tas de 500 Go en moins de 10 millisecondes. En général, Cliff a eu l'occasion de travailler sur tous les aspects de la JVM.

 
Cet article sur Habr est une grande interview avec Cliff. Nous allons aborder les sujets suivants :

  • Passer aux optimisations de bas niveau
  • Comment effectuer un grand refactoring
  • Modèle de coût
  • Apprendre les optimisations de bas niveau
  • Exemples pratiques d'amélioration de la performance
  • Pourquoi créer son propre langage de programmation
  • Carrière d'ingénieur en performance
  • Défis techniques
  • Un peu sur l'allocation des registres et le multithreading
  • Le plus grand défi de la vie

L'interview est conduite par :

  • Andrey Sataryan de Amazon Web Services. Dans sa carrière, il a travaillé sur des projets très variés : il a testé une base de données distribuée NewSQL chez Yandex, un système de détection dans le cloud au sein de Kaspersky Lab, un jeu multijoueur chez Mail.ru et un service de calcul des prix des devises chez Deutsche Bank. Il s'intéresse aux tests de systèmes back-end à grande échelle et distribués.
  • Vladimir Sitnikov de Netcracker. Il travaille depuis dix ans sur la performance et l'évolutivité de NetCracker OS — un logiciel utilisé par les opérateurs de télécommunications pour automatiser les processus de gestion des réseaux et du matériel réseau. Il s'intéresse aux questions de performance de Java et d'Oracle Database. Il est l'auteur de plus d'une douzaine d'améliorations de la performance dans le pilote JDBC PostgreSQL officiel.

Passer aux optimisations de bas niveau

Andrey: Vous êtes une personne reconnue dans le monde de la compilation JIT, en Java et dans le domaine de la performance en général, n'est-ce pas? 

Cliff: C'est exactement ça!

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

Cliff: Ici, tout est simple. Le code le plus rapide est celui qui ne s'exécute jamais. Il est donc toujours nécessaire de commencer par un niveau élevé et de travailler sur les algorithmes. Une meilleure notation O bat une moins bonne notation O, sauf si des constantes suffisamment grandes interviennent. Les éléments de bas niveau sont les derniers à être pris en compte. En général, si vous avez optimisé tout le reste de la pile correctement et qu'il reste encore quelque chose d'intéressant, c'est à ce moment-là que vous atteignez le niveau bas. Mais comment commencer par un niveau élevé ? Comment savoir que vous avez fait suffisamment de travail à un niveau élevé ? Eh bien… il n'y a pas de recettes toutes faites. Il faut comprendre le problème, décider ce que vous allez faire (pour ne pas faire des étapes inutiles par la suite), et c'est alors que vous pouvez déployer un profileur qui peut dire quelque chose d'utile. À un certain moment, vous réalisez vous-même que vous vous êtes débarrassé des éléments inutiles et qu'il est temps de se concentrer sur le réglage fin du bas niveau. C'est sans aucun doute un 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 performance. C'est jusqu'à ce que la question se pose. En général, 99% du temps, personne ne s'intéresse à ce que je fais, jusqu'à ce qu'une chose critique apparaisse sur le chemin, une chose à laquelle quelqu'un pourrait se soucier. Et c'est là que tout le monde commence à vous critiquer sur pourquoi ça ne fonctionnait pas parfaitement dès le départ. En gros, il y a toujours quelque chose à améliorer dans la performance. Mais 99% du temps, vous n'avez pas d'indices ! Vous essayez juste de faire en sorte que quelque chose fonctionne et, dans ce processus, vous comprenez ce qui est important. On ne peut jamais savoir à l'avance que ce morceau doit être parfait, donc, en fait, il faut être parfait dans tout. Et c'est impossible et vous ne le faites pas. Il y a toujours plein de choses à réparer - et c'est tout à fait normal.

Comment effectuer un grand refactoring

Andrey: Comment travaillez-vous sur la performance ? C'est un problème transversal. Par exemple, avez-vous déjà dû traiter des problèmes résultant de l'interaction d'une grande quantité de fonctionnalités déjà existantes ?

Cliff: J'essaie d'éviter cela. Si je sais que la performance va poser problème, je réfléchis avant de commencer à coder, surtout concernant les structures de données. Mais souvent, on découvre tout cela beaucoup plus tard. Et alors, il faut prendre des mesures extrêmes et faire ce que j'appelle « réécrire et régner » : il faut s'emparer d'un assez grand morceau. Une partie du code devra tout de même être réécrite à cause de problèmes de performance ou pour d'autres raisons. Quelle que soit la raison qui pousse à réécrire le code, il est presque toujours préférable de réécrire un plus grand morceau plutôt qu'un plus petit. À ce moment-là, tout le monde commence à trembler de peur : « Oh mon dieu, on ne peut pas toucher autant de code ! ». Mais, en réalité, cette approche fonctionne presque toujours beaucoup mieux. Il faut s'attaquer à un gros problème dès le départ, tracer un grand cercle autour de lui et dire : tout ce qui se trouve à l'intérieur du cercle, je vais le réécrire. La frontière est beaucoup plus petite que le contenu à l'intérieur, qui doit être remplacé. Et si ce contour permet de travailler parfaitement à l'intérieur – tu es libre, fais ce que tu veux. Une fois que tu as compris le problème, le processus de réécriture devient beaucoup plus simple, alors n'hésite pas à prendre un gros morceau !
En même temps, lorsque tu fais une réécriture en gros et que tu réalises que la performance sera un problème, tu peux commencer à t'en préoccuper tout de suite. Cela se transforme généralement en choses simples comme « ne copie pas les données, gère les données aussi simplement que possible, fais-les plus petites ». Dans de grandes réécritures, il existe des méthodes standard pour améliorer la performance. Et elles tournent presque toujours autour des données.

Modèle de coût

Andrey: Dans l'un des podcasts, vous parliez des modèles de coût en lien avec la performance. Pouvez-vous expliquer ce que cela signifie ?

Cliff: Bien sûr. Je suis né à une époque où la performance du processeur était d'une importance cruciale. Et cette ère revient – le destin n'est pas exempt d'ironie. J'ai commencé à vivre à une époque de machines à huit bits, mon premier ordinateur fonctionnait avec 256 octets. Oui, des octets. Tout était très petit. Il fallait compter les instructions, et dès que nous avons commencé à progresser dans les langages de programmation, les langages prenaient de plus en plus en charge. Il y avait l'assembleur, puis le Basic, puis le C, et le C s'occupait de nombreux détails, comme la répartition des registres et le choix des instructions. Mais tout cela était assez clair, et si j'avais un pointeur sur une instance de variable, alors j'obtiendrais un chargement, et le coût de cette instruction est connu. Le matériel fournit un nombre connu de cycles machine, donc la vitesse d'exécution de différentes tâches peut être calculée simplement en additionnant toutes les instructions que vous prévoyez d'exécuter. Chaque compare/test/branche/appel/chargement/storation pouvait être additionnée et vous pouviez dire : voilà le temps d'exécution. En cherchant à améliorer la performance, vous remarquerez certainement ceux qui correspondent aux petits cycles chauds. 
Mais dès que vous passez à Java, Python et des choses similaires, vous vous éloignez très rapidement du matériel bas niveau. Quel est le coût d'un appel getter en Java ? Si le JIT dans HotSpot a tout correctement inliné, ce sera un chargement, mais s'il ne le fait pas – ce sera un appel de fonction. Comme l'appel se trouve dans une boucle chaude, il annulera toutes les autres optimisations dans cette boucle. Par conséquent, le coût réel sera bien plus élevé. Et vous perdez immédiatement la capacité de regarder un morceau de code et de comprendre ce qu'il vaut d'exécuter en termes de fréquence d'horloge du processeur, de mémoire utilisée et de cache. Tout cela devient intéressant seulement si vous vous plongez réellement dans la performance.
Nous nous trouvons dans une situation où la vitesse des processeurs n'a quasiment pas augmenté depuis une décennie. Les temps anciens reviennent ! Vous ne pouvez plus compter sur une bonne performance à un seul fil. Mais si jamais vous vous lancez dans le calcul parallèle – c'est incroyablement complexe, tout le monde vous regarde comme si vous étiez James Bond. Des accélérations décuplées se produisent généralement là où quelqu'un a manqué quelque chose. La parallélisation demande beaucoup de travail. Pour obtenir cette fameuse acceleration décuplée, il faut comprendre le modèle de coût. Ce qui coûte quoi et combien. Et pour cela, il faut comprendre comment le langage s'intègre au matériel sous-jacent.
Martin Thompson a trouvé un excellent mot pour son blog Sympathie Mécanique! Il est essentiel de comprendre ce que le matériel s'apprête à faire, comment il va le faire, et pourquoi il fait ce qu'il fait. En utilisant cela, il est assez simple de commencer à compter les instructions et de découvrir où le temps d'exécution s'en va. Si vous n'avez pas la formation appropriée, vous cherchez juste un chat noir dans une pièce sombre. Je vois constamment des gens optimiser la performance, qui n'ont aucune idée de ce qu'ils font réellement. Ils souffrent beaucoup et n'avancent que très peu. Et quand je prends le même morceau de code, j'y ajoute quelques petits hacks et je réalise un accélération de cinq ou dix fois, ils disent : eh bien, ce n'est pas juste, on savait déjà que tu étais meilleur. Incroyable. De quoi s'agissait-il... le modèle de coût – c’est une question de quel code vous écrivez et à quelle vitesse il fonctionne en moyenne dans le grand schéma.

Andrey: Et comment garder un tel volume en tête ? Cela s'acquiert avec beaucoup d'expérience, non ? Où trouve-t-on cette expérience ?

Cliff: Eh bien, mon expérience, je l'ai acquise de manière pas très simple. J'ai programmé en Assembleur à une époque où il était possible de comprendre chaque instruction séparément. Cela peut sembler absurde, mais depuis, j'ai dans ma tête, dans ma mémoire, un ensemble d'instructions Z80 qui restera à jamais. Je ne me souviens pas des noms des gens une minute après la conversation, mais je me souviens du code écrit il y a 40 ans. C'est drôle, cela ressemble au syndrome de «l'érudit idiot».

Apprendre les optimisations de bas niveau

Andrey: Y a-t-il un moyen plus simple d'entrer dans le domaine ?

Cliff: Oui et non. Le matériel que nous utilisons tous n'a pas vraiment changé au fil des ans. Tout le monde utilise x86, à l'exception des smartphones qui fonctionnent avec Arm. Si tu ne te lances pas dans un développement embarqué hardcore, tu as la même chose. Bien, continuons. Les instructions n'ont pas beaucoup changé non plus au fil des siècles. Il faut aller écrire quelque chose en Assembleur. Un peu, mais suffisamment pour commencer à comprendre. Vous souriez, mais je parle très sérieusement. Il est important de comprendre la correspondance entre le langage et le matériel. Ensuite, il faut s'asseoir, écrire un peu et créer un petit compilateur pour un petit langage. « Petit » signifie qu'il doit être réalisé dans un délai raisonnable. Il peut être super simple, mais il doit générer des instructions. L'acte de génération d'instructions permettra de comprendre le modèle de coût pour le passage entre le code de haut niveau, sur lequel tout le monde travaille, et le code machine qui s'exécute sur le matériel. Cette correspondance s'ancrera dans nos esprits au moment de la rédaction du compilateur. Même d'un compilateur très simple. Après cela, on peut commencer à regarder Java et le fait que son fossé sémantique est beaucoup plus profond, rendant la construction de ponts bien plus complexe. En Java, il est bien plus difficile de comprendre si notre pont est bon ou mauvais, ce qui va le faire s'effondrer ou non. Mais tu as besoin d'un point de départ lorsque tu examines le code et comprends : « ah, ce getter doit être inliné à chaque fois ». Et ensuite, il s'avère que cela se produit parfois, sauf lorsque la méthode devient trop grosse, et que le JIT commence à inliner tout. La performance de ces endroits peut être prédite instantanément. En général, les getters fonctionnent bien, mais ensuite tu observes de grandes boucles chaudes et réalises qu'il y a des appels de fonctions et qu'on ne sait pas ce qu'elles font. C'est le problème avec l'utilisation généralisée des getters, la raison pour laquelle ils ne s'inlinent pas - on ne sait pas si c'est un getter. Si tu as une base de code super petite, tu peux simplement te souvenir et dire : voici un getter, et voici un setter. Dans une grande base de code, chaque fonction a sa propre histoire, qui, au fond, n'est connue de personne. Le profileur dit que nous avons perdu 24 % de temps sur une boucle, et pour comprendre ce que fait cette boucle, il faut examiner chaque fonction à l'intérieur. Il est impossible de comprendre cela sans étudier la fonction, et cela ralentit sérieusement le processus de compréhension. C'est pourquoi je n'utilise pas de getters ni de setters, je suis passé à un autre niveau !
D'où obtenir un modèle de coût ? Eh bien, on peut lire quelque chose, bien sûr... Mais je pense que le meilleur moyen est d'agir. Faire un petit compilateur sera la meilleure façon de comprendre le modèle de coût et de l'intégrer dans sa propre tête. Un petit compilateur qui pourrait être utilisé pour programmer un micro-ondes — c'est un défi pour un débutant. Je veux dire, si tu as déjà des compétences en programmation, cela devrait suffire. Toutes ces choses comme analyser une chaîne qui sera une sorte d'expression algébrique, en extraire les instructions des opérations mathématiques dans le bon ordre, récupérer les bonnes valeurs depuis les registres — tout cela se fait facilement. Et pendant que tu fais cela, cela s'imprimera dans ton cerveau. Je pense que tout le monde sait ce qu'est un compilateur. Et ça, ça donnera une compréhension du modèle de coût.

Exemples pratiques d'amélioration de la performance

Andrey: Sur quoi d'autre faut-il faire attention quand on travaille sur la performance ?

Cliff: Structures de données. D'ailleurs, ça fait longtemps que je n'ai pas animé ces cours... Rocket School. C'était amusant, mais cela nécessitait tant d'efforts, et j'ai aussi une vie ! Bon. Donc, lors d'un des grands et intéressants cours, "Où va votre performance", j'ai donné aux étudiants un exemple : deux milliards et demi de données fintech lues depuis un fichier CSV, et ensuite il fallait compter le nombre de produits vendus. Des données de marché classiques. Des paquets UDP, convertis en format texte, depuis les années 70. Chicago Mercantile Exchange – toutes sortes de choses comme le pétrole, le maïs, les fèves de soja, etc. Il fallait compter ces produits, le nombre de transactions, le volume moyen des échanges de fonds et de produits, etc. C'est des mathématiques de trading assez simples : trouver le code produit (c'est 1-2 symboles dans une table de hachage), obtenir la somme, l'ajouter à l'un des ensembles de transactions, ajouter le volume, ajouter le coût, et quelques autres éléments. Très mathématique. La réalisation simpliste était très directe : tout se trouve dans le fichier, je lis le fichier et j'avance à travers lui, en séparant les enregistrements individuels en chaînes Java, en cherchant les choses dont j'ai besoin et en additionnant selon la mathématique décrite ci-dessus. Et cela fonctionne avec une certaine lenteur.

Avec cette approche, il est évident ce qui se passe, et les calculs parallèles ne seront pas utiles, n'est-ce pas ? Il s'avère qu'un gain de performance multiplié par cinq peut être obtenu simplement en choisissant les bonnes structures de données. Cela surprend même les programmeurs expérimentés ! Dans mon cas particulier, le focus était de ne pas faire d'allocation de mémoire dans une boucle chaude. Bon, ce n'est pas toute la vérité, mais dans l'ensemble, il ne faut pas allouer « une fois tous les X », quand X est suffisamment grand. Quand X fait deux giga-octets et demi, il ne faut rien allouer « une fois par lettre », ou « une fois par ligne », ou « une fois par champ », rien de ce genre. C'est précisément là que le temps est perdu. Comment cela fonctionne-t-il au juste ? Imaginez que j'appelle String.split() ou BufferedReader.readLine(). Readline fait une chaîne à partir d'un ensemble de bytes reçus sur le réseau, une fois pour chaque ligne, pour chacune des centaines de millions de lignes. Je prends cette chaîne, je l'analyse et je la jette. Pourquoi je la jette ? Eh bien, je l'ai déjà traitée, c'est tout. Donc, pour chaque byte lu de ces 2.7G, deux caractères vont être écrits dans la chaîne, soit déjà 5.4G, et ils ne me servent à rien par la suite, donc ils sont jetés. Si l'on regarde la bande passante de la mémoire, nous chargeons 2.7G qui passent à travers la mémoire et la bus mémoire dans le processeur, et ensuite deux fois plus sont envoyés dans la chaîne, résidant en mémoire, et tout cela est réécrit pour chaque nouvelle chaîne. Mais je dois bien la lire, le matériel la lit, même si ensuite tout sera réécrit. Et je dois l'écrire parce que j'ai créé une chaîne et les caches débordent – le cache ne peut pas contenir 2.7G. En somme, pour chaque byte lu, je lis deux bytes supplémentaires et j'écris deux bytes supplémentaires, résultant en un ratio de 4:1 – ainsi, nous gaspillons inutilement la bande passante de la mémoire. Et puis il s'avère que si je fais String.split() – je ne le fais pas pour la dernière fois, à l'intérieur il peut y avoir encore 6-7 champs. Ainsi, le code classique de lecture de CSV suivi d'une analyse de chaînes entraîne une perte de bande passante mémoire d'environ 14:1 par rapport à ce que vous aimeriez vraiment avoir. Si on élimine ces allocations, on peut obtenir un gain de vitesse multiplié par cinq.

Ce n'est pas vraiment très compliqué. Si vous regardez le code sous le bon angle, tout cela devient assez simple, dès que vous avez compris le cœur du problème. Il ne faut surtout pas arrêter d'allouer de la mémoire : le problème est juste que vous allouez quelque chose et cela meurt immédiatement, et en chemin, cela consomme une ressource précieuse, qui dans ce cas est la bande passante mémoire. Tout cela entraîne une baisse de performance. Sur x86, il est généralement nécessaire de brûler activement des cycles processeur, tandis qu'ici, vous avez déjà épuisé toute la mémoire bien plus tôt. La solution consiste à réduire le nombre d'allocations. 
Une autre partie du problème est que, si vous lancez un profileur quand la bande passante mémoire est épuisée, au moment où cela se produit, vous attendez généralement un retour du cache, parce qu'il est plein de déchets que vous venez de générer avec toutes ces allocations. C'est pourquoi chaque opération de chargement ou de stockage devient lente, car elles entraînent des échecs de cache - tout le cache devient lent, attendant que les déchets soient éliminés. Ainsi, le profileur ne montrera qu'un bruit aléatoire tiède, étalé sur tout le cycle - il n'y aura pas d'instruction chaude ou d'endroit distinct dans le code. Juste du bruit. Et si vous regardez les cycles de GC, ils seront tous dans la Young Generation et extrêmement rapides - en microsecondes ou millisecondes au maximum. En effet, toute cette mémoire meurt instantanément. Vous allouez des milliards de gigaoctets, et il les coupe, et les coupe encore, et encore. Tout cela se produit très rapidement. Il en résulte des cycles de GC peu coûteux, un bruit tiède tout au long du cycle, mais nous voulons obtenir un accélérateur 5 fois plus rapide. À ce moment-là, il devrait se produire une illumination dans votre esprit et vous vous direz : « Pourquoi cela ?! ». Le débordement de la bande passante mémoire ne s'affiche pas dans le débogueur classique, il faut lancer un débogueur de compteurs de performance matériels et le voir par vous-même, directement. Sinon, on peut le soupçonner indirectement à partir de ces trois symptômes. Le troisième symptôme est lorsque vous regardez ce que vous allouez, demandez au profileur, et il répond : « Vous avez fait un milliard d'allocations, mais le GC a fonctionné gratuitement ». Dès que cela se produit, vous comprenez que vous avez généré trop d'objets et épuisé toute la bande passante mémoire. Il existe une façon de comprendre cela, mais elle n'est pas évidente. 

Le problème réside dans la structure des données : une structure brute derrière tout cela, elle est trop grande, c'est 2,7 Go sur le disque, donc faire une copie de cette chose est très indésirable – on souhaiterait la charger directement depuis le tampon de bytes réseau dans les registres, afin de ne pas lire et écrire en aller-retour cinq fois. Malheureusement, Java ne fournit pas par défaut une telle bibliothèque dans le JDK. Mais c'est en réalité trivial, n'est-ce pas ? En gros, cela représente 5 à 10 lignes de code qui serviront à implémenter votre propre chargeur de lignes tamponnées, qui réplique le comportement de la classe string, tout en étant une couche autour du tampon de bytes sous-jacent. Il s'avère donc que vous travaillez presque comme si vous utilisiez des chaînes, mais en réalité, ce sont des pointeurs qui se déplacent dans le tampon, donc des bytes bruts ne sont copiés nulle part, et ainsi les mêmes tampons sont réutilisés encore et encore, tandis que le système d'exploitation est heureux de gérer les éléments pour lesquels il est conçu, comme le double buffering caché de ces tampons de bytes, et vous ne broyez plus un flux infini de données inutiles. Au fait, vous comprenez que, lors du travail avec le GC, il est garanti que chaque allocation de mémoire ne sera pas visible par le processeur après le dernier cycle de GC ? Donc, tout cela ne peut pas se trouver dans le cache, et il en résulte un échec garanti à 100 %. En utilisant un pointeur, sur x86, lire un registre en mémoire prend 1 à 2 cycles, et dès que cela se produit, vous payez, payez, payez, parce que toute la mémoire est sur NINE caches – et c'est cela le coût de l'allocation mémoire. Le véritable coût.

En d'autres termes, les structures de données sont ce qui est le plus difficile à changer. Une fois que vous réalisez que vous avez choisi la mauvaise structure de données, qui nuira à la performance, il faut généralement effectuer un travail considérable pour corriger cela. Mais si ce n'est pas fait, la situation ne fera qu'empirer. Avant tout, il est crucial de réfléchir aux structures de données. Le coût principal ici est associé aux structures de données encombrantes, que l'on commence à utiliser dans le style "j'ai copié la structure de données X dans la structure de données Y parce que Y me semble plus attrayante". Mais l'opération de copie (qui semble peu coûteuse) consomme en réalité de la bande passante en mémoire et c'est ici que le temps d'exécution est perdu. Si j'ai une gigantesque chaîne JSON et que je veux la transformer en un arbre DOM structuré à partir de POJO ou quelque chose comme ça, l'opération de parsing de cette chaîne et de construction de POJO, puis l'accès ultérieur à POJO entraînera un coût supplémentaire – ce n'est pas bon marché. À moins que vous n'utilisiez les POJO beaucoup plus souvent que la chaîne. En gros, vous pourriez essayer de décoder la chaîne et d'en extraire seulement ce dont vous avez besoin, sans la transformer en POJO. Si tout cela se déroule là où une performance maximale est requise, oubliez les POJO – il faut fouiller directement dans la chaîne.

Pourquoi créer son propre langage de programmation

Andrey: Vous avez dit que pour comprendre le modèle de coût, il faut écrire votre propre petit langage...

Cliff: Ce n'est pas un langage, mais un compilateur. Un langage et un compilateur sont des choses différentes. La principale distinction se trouve dans votre esprit. 

Andrey: À propos, d'après ce que je sais, vous expérimentez la création de langages propres. Pourquoi ?

Cliff: Parce que je peux ! Je suis à moitié à la retraite, donc c'est mon hobby. J'ai passé ma vie à réaliser les langages de quelqu'un d'autre. J'ai également beaucoup travaillé sur le style de codage. En plus, je vois des problèmes dans d'autres langages. Je vois qu'il existe des manières meilleures de faire des choses habituelles. Et je les utiliserais. Je suis juste fatigué de voir des problèmes en moi, dans Java, dans Python, dans tout autre langage. En ce moment, j'écris en React Native, JavaScript et Elm comme hobby, qui n'est pas lié à la retraite, mais à un travail actif. Je programme aussi en Python et probablement, je vais continuer à travailler sur l'apprentissage automatique pour des backends Java. Il existe de nombreux langages populaires, chacun ayant des caractéristiques intéressantes. Chacun est bon à sa manière et on peut essayer de réunir toutes ces particularités. Donc, je m'engage dans l'étude de choses qui m'intéressent, le comportement des langages, en essayant de trouver une sémantique raisonnable. Et pour l'instant, ça fonctionne bien pour moi ! Actuellement, je lutte avec la sémantique de la mémoire, car je voudrais qu'elle soit comme en C et Java, et obtenir un modèle de mémoire fort et une sémantique de mémoire pour les chargements et les stockages. En même temps, j'aimerais avoir une inférence de type automatique comme en Haskell. Voilà, j'essaie de mélanger une inférence de type de style Haskell avec une mémoire fonctionnant comme en C et Java. Je m'y consacre depuis 2-3 mois, par exemple.

Andrey: Si vous construisez un langage qui prend le meilleur des aspects d'autres langages, avez-vous pensé que quelqu'un pourrait faire l'inverse : prendre vos idées et les utiliser ?

Cliff: C'est ainsi que de nouveaux langages apparaissent ! Pourquoi Java ressemble-t-il à C ? Parce que C avait une bonne syntaxe que tout le monde comprenait et que Java s'est inspiré de cette syntaxe, en y ajoutant la sécurité des types, les vérifications de limites de tableaux, le GC, et améliorant certaines choses de C. Ils ont ajouté leurs propres éléments. Mais ils se sont beaucoup inspirés, n'est-ce pas ? Tout le monde se tient sur les épaules de géants qui étaient là avant vous - c'est ainsi que progresse la science.

Andrey: D'après ce que je comprends, votre langage sera sûr en ce qui concerne l'utilisation de la mémoire. Envisagez-vous de mettre en œuvre quelque chose comme le borrow checker de Rust ? Vous l'avez regardé, qu'en pensez-vous ?

Cliff: Eh bien, j'écris en C depuis une éternité, avec tous ces malloc et free, et je gère manuellement le cycle de vie. Vous savez, 90-95 % du cycle de vie géré manuellement a la même structure. Et c'est très, très pénible de faire cela à la main. J'aimerais que le compilateur indique simplement ce qui se passe et ce que j'ai accompli grâce à mes actions. Pour certaines choses, le vérificateur d'emprunt le fait par défaut. De plus, il devrait automatiquement fournir des informations, tout comprendre et ne même pas me surcharger en exposant cette compréhension. Il doit faire au moins une analyse d'évasion locale, et uniquement si cela échoue, il faut alors ajouter des annotations de type qui décrivent le cycle de vie – et un tel schéma est bien plus complexe que le vérificateur d'emprunt, ou tout autre vérificateur de mémoire existant. Le choix entre « tout va bien » et « je n'ai rien compris » — non, il devrait y avoir quelque chose de mieux. 
En tant que personne ayant beaucoup programmé en C, je considère que le support de la gestion automatique du cycle de vie est d'une importance cruciale. De plus, je suis lassé de la manière dont Java utilise la mémoire, ma principale plainte étant liée au GC. Lors de l'allocation de mémoire en Java, la mémoire qui était locale lors du dernier cycle de GC n'est pas récupérée. Dans les langages avec une gestion de la mémoire plus précise, cela ne se passe pas ainsi. Si vous appelez malloc, vous obtenez immédiatement de la mémoire qui a généralement été utilisée juste avant. En général, vous faites des choses temporaires avec la mémoire et la rendez rapidement. Cela permet de la renvoyer immédiatement dans le pool de malloc, et le cycle de malloc suivant la récupère ensuite. Par conséquent, l'utilisation réelle de la mémoire est réduite à l'ensemble des objets vivants à un moment donné, plus les fuites. Et tant que vous n'avez pas de fuites de mémoire de manière flagrante, la majeure partie de la mémoire est retenue dans les caches et le processeur, ce qui fonctionne rapidement. Mais cela nécessite beaucoup de gestion manuelle de la mémoire à l'aide de malloc et free, appelés dans le bon ordre et au bon endroit. Rust peut gérer cela correctement et dans de nombreux cas, offrir même de meilleures performances, car la consommation de mémoire se réduit uniquement aux calculs en cours – contrairement à l'attente du prochain cycle de GC pour libérer de la mémoire. En fin de compte, nous avons un moyen très intéressant d'améliorer les performances. Et c'est assez puissant – en ce sens, j'ai travaillé sur ce genre de chose dans le traitement des données pour le fintech, et cela permettait un gain de performance d'environ cinq fois. C'est un gain considérable, surtout dans un monde où les processeurs ne deviennent pas plus rapides, alors que nous continuons à attendre des améliorations.

Carrière d'ingénieur en performance

Andrey: Je voudrais aussi poser quelques questions sur la carrière en général. Vous êtes devenu célèbre grâce à votre travail sur le JIT dans HotSpot, puis vous êtes passé chez Azul – qui est également une entreprise JVM. Mais vous vous êtes concentré davantage sur le matériel que sur le logiciel. Ensuite, vous avez soudainement pivoté vers le Big Data et l'apprentissage automatique, puis vers la détection de fraude. Comment cela s'est-il produit ? Ce sont des domaines de développement très différents.

Cliff: Je programme depuis assez longtemps et j'ai eu l'occasion de participer à des projets très variés. Et quand les gens disent : « oh, tu es celui qui a fait JIT pour Java ! », c'est toujours amusant. Avant cela, je travaillais sur un clone de PostScript – ce langage qu'Apple utilisait autrefois pour ses imprimantes laser. Et avant cela, j'avais réalisé le langage Forth. Je pense que le thème commun pour moi est le développement d'outils. J'ai toujours créé des outils permettant à d'autres personnes d'écrire leurs super programmes. Mais j'ai également travaillé sur le développement de systèmes d'exploitation, de pilotes, de débogueurs au niveau du noyau, de langages pour développer des systèmes d'exploitation, qui commençaient de façon triviale mais devenaient de plus en plus complexes avec le temps. Mais le sujet principal, tout de même, reste le développement d'outils. Une grande partie de ma vie s'est écoulée entre Azul et Sun, et cela concernait Java. Mais lorsque je me suis plongé dans le Big Data et le Machine Learning, j'ai de nouveau mis mon chapeau de gala et dit : « Oh, maintenant nous avons un problème non trivial, et il se passe vraiment plein de choses intéressantes avec des gens qui font quelque chose. » C'est une excellente voie de développement à explorer.

Oui, j'adore vraiment le calcul distribué. Mon premier emploi a été pendant mes études en C, sur un projet publicitaire. C'était du calcul distribué sur des puces Zilog Z80, qui collectaient des données pour la reconnaissance optique de caractères analogique réalisée par un véritable analyseur analogique. C'était un sujet fascinant et complètement atypique. Mais il y avait des problèmes, certaines parties n'étaient pas reconnues correctement, donc il fallait extraire l'image et la montrer à une personne qui pouvait déjà lire et indiquer ce qu'il y avait. Ainsi, il y avait des tâches de données et ces tâches avaient leur propre langage. Il y avait un backend qui traitait tout cela - des Z80 fonctionnant en parallèle avec des terminaux vt100 - un par personne, et il y avait un modèle de programmation parallèle sur Z80. Un morceau de mémoire partagé, que tous les Z80 partageaient au sein d'une configuration en étoile ; la carte de support était également partagée, et la moitié de la RAM était partagée au sein du réseau, tandis que l'autre moitié était privée ou utilisée ailleurs. Un système parallèle distribué complexe avec une mémoire… semi-partagée. Quand cela a-t-il eu lieu… Je ne me souviens déjà plus, quelque part au milieu des années 80. C'était il y a longtemps. 
Oui, considérons que 30 ans, c'est assez longtemps. Les tâches liées au calcul distribué existent depuis assez longtemps, les gens se battent depuis longtemps avec Beowulf-clusters. Ces clusters ressemblent à... Par exemple : il y a l'Ethernet et ton x86 rapide est connecté à cet Ethernet, et maintenant tu veux obtenir une fake shared memory, parce que personne ne pouvait à l'époque s'occuper de la programmation des calculs distribués, c'était trop compliqué et donc il y avait une fake shared memory avec protection des pages mémoire sur x86, et si tu écrivais dans cette page, nous disions aux autres processeurs que s'ils accédaient à la même shared memory, elle devait être chargée depuis toi, et c'est ainsi qu'un protocole de support de cohérence de cache et de logiciel pour cela est apparu. Concept intéressant. Le vrai problème, bien sûr, était ailleurs. Tout cela fonctionnait, mais tu rencontrais rapidement des problèmes de performance, car personne ne comprenait le modèle de performance à un niveau suffisamment bon – quels étaient les motifs d'accès à la mémoire, comment faire en sorte que les nœuds ne se pingent pas indéfiniment, et ainsi de suite.

Dans H2O, j'ai conçu une approche où les développeurs sont responsables de déterminer où se cache le parallélisme et où il n'est pas. J'ai élaboré un modèle de codage qui facilite l'écriture de code haute performance. En revanche, écrire du code qui fonctionne lentement est complexe et a une apparence peu attrayante. Il faut un véritable effort pour produire un code lent, notamment en recourant à des méthodes non standard. Un code ralentissant se remarque immédiatement. Par conséquent, le code est généralement écrit pour être rapide, mais il faut alors se pencher sur la gestion de la mémoire partagée. Tout cela est lié à de grands tableaux et le comportement est similaire à celui de grands tableaux non volatils dans le parallélisme en Java. Imaginez que deux threads écrivent dans un tableau parallèle, l'un d'eux l'emporte, et l'autre perd, sans que vous sachiez lequel est lequel. S'ils ne sont pas volatils, l'ordre peut être n'importe quel – et cela fonctionne réellement très bien. Les gens se soucient vraiment de l'ordre des opérations, ils positionnent correctement les mots-clés volatile et anticipent bien les problèmes de performance liés à la mémoire. Sinon, ils écriraient simplement du code sous la forme de boucles de 1 à N, où N représente des trillions, en espérant que tous les cas complexes deviennent automatiquement parallèles – mais cela ne fonctionne pas. Cependant, H2O ce n'est ni Java ni Scala, on peut considérer cela comme « Java moins moins », si on le veut. C'est un style de programmation très compréhensible, ressemblant à l'écriture de code simple en C ou Java avec des boucles et des tableaux. En même temps, la mémoire peut être manipulée par téraoctets. J'utilise toujours H2O. De temps en temps, je l'emploie dans différents projets – et c'est jusqu'à présent la solution la plus rapide, devançant la concurrence de plusieurs dizaines de fois. Si vous traitez des Big Data avec des données en colonnes, il est très difficile de surpasser H2O.

Défis techniques

Andrey: Quel a été le plus grand défi de votre carrière ?

Cliff: Discutons-nous de la partie technique ou non technique de la question ? Je dirais que les plus grands défis sont non techniques. 
En ce qui concerne les défis techniques. Je les ai simplement surmontés. Je ne sais même pas quel a été le plus grand, mais il y en a eu plusieurs plutôt intéressants qui ont demandé beaucoup de temps et de lutte mentale. Lorsque j'ai rejoint Sun, j'étais convaincu que je ferais un compilateur rapide, tandis qu'une multitude de seniors disait que je n'y arriverais jamais. Mais j'ai emprunté ce chemin, j'ai écrit un compilateur jusqu'à l'allocateur de registres, et il était assez rapide. Il était aussi rapide que le C1 moderne, mais à l'époque, l'allocateur était beaucoup plus lent, et en y repensant, c'était un problème de grande structure de données. J'en avais besoin pour écrire un allocateur de registres graphique et je ne comprenais pas le dilemme entre l'expressivité du code et la vitesse, qui était très important à cette époque. Il s'est avéré que la structure de données dépassait généralement la taille du cache sur les x86 de cette époque, donc, si je supposais au départ que l'allocateur de registres travaillerait 5-10 % du temps de compilation juste-à-temps (JIT), en réalité, cela se traduisait par 50 %.

Avec le temps, le compilateur devenait de plus en plus précis et performant, il cessait de générer du code horrible dans un plus grand nombre de cas, et la performance commençait de plus en plus à ressembler à celle d'un compilateur C. Sauf si, bien sûr, vous écriviez une sorte de code tellement mauvais que même C ne l'accélérerait pas. Si vous écriviez du code comme en C, vous aviez également des performances similaires à C dans la plupart des cas. Et de plus en plus souvent, le code obtenait une performance asymptotiquement équivalente à celle du C, et l'allocateur de registres commençait à ressembler à quelque chose de complet… peu importe si votre code s'exécutait rapidement ou lentement. Je continuais à travailler sur l'allocateur pour qu'il fasse de meilleures allocations. Il devenait de plus en plus lent, mais offrait des performances toujours meilleures dans les cas où personne n'y arrivait. Je pouvais plonger dans l'allocateur de registres, y enfouir un mois de travail, et soudain tout le code commençait à s'exécuter 5 % plus rapidement. Cela se produisait encore et encore et l'allocateur de registres était devenu une sorte d'œuvre d'art – tout le monde l'aimait ou le détestait, et les gens du milieu académique posaient des questions sur « pourquoi tout était fait de cette manière », pourquoi pas. balayage linéaire, et quelle est la différence. La réponse est la même : un allocate basé sur le coloriage du graphe plus un code tampon très soigné équivaut à un instrument de victoire, la meilleure combinaison que personne ne peut battre. Et c'est une chose assez subtile. Tout le reste que le compilateur fait – ce sont des choses plutôt bien connues, même si elles ont été portées à un niveau artistique. J'ai toujours fait des choses qui devaient transformer le compilateur en œuvre d'art. Mais rien de tout cela n'était extraordinaire – à l'exception de l'allocateur de registres. Le point est qu'il faut le faire avec précaution couper sous charge et, si cela se produit (je peux expliquer plus en détail si cela vous intéresse), cela signifie qu'il est possible de procéder à une intégration plus agressive, sans risque de dépasser la rupture de la courbe de performance. À l'époque, il y avait plein de compilateurs complets, ornés de gadgets et de sifflets, qui avaient des allocateurs de registres, mais personne n'a réussi à faire mieux depuis.

Le problème est que, si vous ajoutez des méthodes pouvant être intégrées, en augmentant sans cesse le champ d'intégration, l'ensemble des valeurs utilisées dépasse immédiatement le nombre de registres, et vous devez faire du spilling. Le niveau critique survient généralement lorsque l'allocateur abandonne, et un bon candidat pour le spilling vaut un autre, vous faites du spilling sur des choses totalement étranges. La valeur de l'inlining réside dans le fait que vous perdez une partie de l'overhead, l'overhead d'appel et de sauvegarde, vous pouvez voir les valeurs à l'intérieur et vous pouvez continuer à les optimiser. Le coût de l'inlining réside dans le fait qu'un grand nombre de valeurs vivantes se forment, et si votre allocateur de registres fait plus de spilling que nécessaire, vous perdez immédiatement. Ainsi, la plupart des allocateurs ont ce problème : quand l'inlining dépasse un certain seuil, tout commence à faire du spilling, et les performances peuvent être réduites à néant. Ceux qui réalisent des compilateurs ajoutent certaines heuristiques : par exemple, pour arrêter l'inlining à partir d'une taille suffisante, car les allocations peuvent tout gâcher. Cela crée une rupture dans le graphique de performance : vous insérez, insérez, la performance augmente lentement – puis boum ! – elle chute comme un marteau, parce que vous avez intégré trop de choses. Ainsi, tout fonctionnait jusqu'à l'arrivée de Java. Java exige beaucoup plus d'inlining, donc j'ai dû rendre mon allocateur beaucoup plus agressif pour qu'il s'ajuste plutôt que de tomber, et si vous avez trop intégré – il commence à faire du spilling, mais arrive néanmoins le moment où il n'y a plus de spilling. C'est une observation intéressante et elle m'est venue de nulle part, non évidente, mais bien rentable. J'ai adopté un inlining agressif et cela m'a conduit à des endroits où les performances de Java et C sont côte à côte. Ils sont réellement proches – je peux écrire du code en Java qui sera significativement plus rapide que le code en C et ainsi de suite, mais en moyenne, dans la grande image des choses, ils sont à peu près comparables. Il semble qu'une partie de ce mérite provienne de l'allocateur de registres, qui me permet d'intégrer de manière maximale. Je fais simplement de l'inlining sur tout ce que je vois. La question ici est de savoir si l'allocateur fonctionne bien, si le code résultant fonctionne de manière raisonnable. Cela a été un grand défi : comprendre tout cela et réussir à le faire fonctionner.

Un peu sur l'allocation des registres et le multithreading

Vladimir: Les problèmes comme l'allocation des registres semblent être un sujet éternel. Est-ce qu'il est possible qu'une idée ait semblé prometteuse et qu'en pratique, elle ait échoué ?

Cliff: Bien sûr ! L'allocation des registres est un domaine où, pour résoudre un problème NP-complet, tu essaies de trouver des heuristiques. Et tu ne pourras jamais obtenir une solution parfaite, n'est-ce pas ? C'est juste impossible. Regardez, la compilation Ahead of Time – elle fonctionne aussi mal. On parle ici de cas moyens. De performances typiques, afin que tu puisses aller et mesurer quelque chose que tu considères comme une bonne performance typique – après tout, tu travailles pour l'améliorer ! L'allocation des registres est un sujet entièrement dédié à la performance. Dès que tu as un premier prototype, il fonctionne et fait ce qu'il doit, le travail sur la performance commence. Il faut apprendre à bien mesurer. Pourquoi est-ce important ? S'il y a des données claires, on peut regarder différentes parties et voir : ah, cela a aidé ici, mais là, tout a échoué ! Il y a de bonnes idées qui apparaissent, tu ajoutes une nouvelle heuristique et soudainement, tout fonctionne un peu mieux en moyenne. Ou pas. J'ai eu beaucoup de cas où nous avons lutté pour cinq pourcents de performance qui distinguaient notre développement de l'allocateur précédent. Et chaque fois, ça ressemble à ça : on gagne quelque part, on perd ailleurs. Si tu as de bons outils d'analyse de performance, tu peux trouver les idées perdantes et comprendre pourquoi elles échouent. Peut-être qu'il vaut mieux laisser les choses telles qu'elles sont, ou alors s'attaquer sérieusement à l'optimisation fine, ou aller réparer autre chose. C'est tout un ensemble de choses ! J'ai fait ce super hack, mais j'ai besoin de celui-ci, et de celui-là, et de celui-ci – et leur combinaison totale apporte quelques améliorations. Et les solutions isolées peuvent échouer. C'est la nature du travail sur la performance des problèmes NP-complets.

Vladimir: On a l'impression que des choses comme la coloration dans les allocateurs sont des tâches déjà résolues. Eh bien, elles le sont pour vous, vu ce que vous racontez, alors cela vaut-il vraiment la peine de...

Cliff: Elle n'est pas résolue en tant que telle. C'est à toi de la transformer en « résolue ». Il existe des tâches difficiles à accomplir et il faut s'y attaquer. Une fois cela fait, il est temps de se concentrer sur la performance. Il faut aborder ce travail de manière appropriée – faire des benchmarks, collecter des métriques, expliquer les situations dans lesquelles, après un retour à une version antérieure, ton ancien hack a recommencé à fonctionner (ou au contraire, a cessé de fonctionner). Et ne pas reculer jusqu'à ce que tu aies quelque chose à montrer. Comme je l'ai déjà dit, si des idées intéressantes n'ont pas fonctionné, il y a environ une infinité d'idées dans le domaine de l'allocation de registres. On peut par exemple lire des publications scientifiques. Bien que ce domaine ait commencé à avancer beaucoup plus lentement et soit devenu plus clair qu'à ses débuts. Néanmoins, une infinité de personnes travaille dans ce domaine, et toutes leurs idées méritent d'être testées, elles attendent toutes leur tour. Et tu ne peux pas dire à quel point elles sont bonnes si tu ne les essayes pas. À quel point elles s'intègrent bien à tout le reste dans ton allocateur, car l'allocateur effectue de nombreuses tâches, et certaines idées dans ton allocateur en particulier peuvent ne pas fonctionner, alors que dans un autre, ça pourrait très bien marcher. Le principal moyen de victoire pour un allocateur est d'extraire les éléments lents hors du chemin principal et de forcer le fractionnement aux frontières des chemins lents. Donc, si tu veux exécuter le GC, suivre le chemin lent, se déoptimiser, lancer une exception, tout ça – tu sais que ces choses sont relativement rares. Et elles sont vraiment rares, j'ai vérifié. Tu fais un travail supplémentaire et, grâce à cela, de nombreuses limitations sur ces chemins lents disparaissent, mais ce n'est pas très important, car ils sont lents et peu fréquentés. Par exemple, un pointeur nul, ça n'arrive jamais, n'est-ce pas ? Il faut avoir plusieurs chemins pour différentes choses, mais ils ne doivent pas interférer avec le principal. 

Vladimir: Que penses-tu de la multiprocession, quand il y a des milliers de cœurs à la fois ? C'est quelque chose d'utile ?

Cliff: Le succès des GPU montre que c'est plutôt utile !

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

Cliff: Eh bien, c'était le modèle commercial d'Azul. La réponse est venue à une époque où les gens aimaient beaucoup la performance prévisible. À l'époque, il était difficile d'écrire du code parallèle. Le modèle de codage H2O se prête bien à l'évolutivité, mais ce n'est pas un modèle à usage général. À moins qu'il soit légèrement plus général que l'utilisation du GPU. Parlons-nous de la complexité du développement de ce genre de chose ou de sa complexité d'utilisation ? Par exemple, une leçon intéressante qu'Azul m'a apprise, assez subtile : les petits caches, c'est acceptable. 

Le plus grand défi de la vie

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

Cliff: Le plus grand défi a été de ne pas être… gentil et bon avec les gens. En conséquence, je me retrouvais constamment dans des situations conflictuelles extrêmes. Des situations où je savais que tout allait de travers, mais je ne savais pas comment avancer dans la résolution de ces problèmes et je ne pouvais pas les gérer. De nombreux problèmes persistants, qui durent des décennies, sont apparus de cette manière. Le fait qu'il y ait des compilateurs C1 et C2 en Java en est une conséquence directe. L'absence de compilation multi-niveaux en Java pendant une décennie en est également une conséquence directe. Il est évident qu'un tel système était nécessaire, mais il n'est pas évident pourquoi il n'existait pas. J'ai eu des problèmes avec un ingénieur… ou un groupe d'ingénieurs. Il y a longtemps, quand j'ai commencé à travailler chez Sun, j'étais… Bon, pas seulement à cette époque, j'ai toujours eu mon propre avis sur tout. Et je croyais fermement qu'il suffisait de dire cette vérité en face. Surtout que j'avais souvent raison de manière choquante. Et si ce type d'approche ne te convient pas… surtout si tu es manifestement dans l'erreur et que tu dis des bêtises… En gros, peu de gens pouvaient tolérer ce mode de communication. Pourtant, certains pouvaient, comme moi par exemple. J'ai construit ma vie sur des principes méritocratiques. Si tu me montres quelque chose de faux, je me retournerai immédiatement et dirai : tu racontes des bêtises. Tout en m'excusant bien sûr, en reconnaissant les mérites s'il y en a, et en faisant d'autres actions correctes. D'un autre côté, j'ai souvent raison de manière choquante une grande partie du temps. Et cela ne fonctionne pas très bien dans les relations avec les gens. Je n'essaie même pas d'être aimable, je pose la question de manière directe. « Cela ne fonctionnera jamais, parce qu'une, deux et trois ». Et ils réagissent : « Oh ! ». Il y a eu d'autres conséquences, que j'aurais probablement intérêt à omettre : par exemple, celles qui ont conduit à mon divorce et à une décennie de dépression qui a suivi.

Le défi est une lutte contre les perceptions des gens sur ce que vous pouvez ou ne pouvez pas faire, sur ce qui est important et ce qui ne l'est pas. Il y a eu de nombreux défis concernant le style de codage. J'écris encore beaucoup de code et à l'époque, j'ai même dû ralentir, car je faisais trop de tâches en parallèle et je les faisais mal, au lieu de me concentrer sur une seule. En réfléchissant, j'ai écrit la moitié du code de l'équipe Java JIT, l'équipe C2. Le prochain coder le plus rapide écrivait deux fois plus lentement, le suivant était encore plus lent, et c'était une chute exponentielle. La septième personne de cette chaîne était très, très lente – cela arrive toujours ! J'ai touché à une foule de codes. Je regardais qui écrivait quoi, sans exceptions, je scrutais leur code, je révisais chacun d'eux, et je continuais à écrire plus que n'importe lequel d'entre eux. Avec les gens, cette approche ne fonctionne pas très bien. Certains n'aiment pas cela. Et quand ils ne peuvent pas gérer, toutes sortes de plaintes commencent. Par exemple, un jour, on m'a dit d'arrêter d'écrire du code parce que j'en écrivais trop, mettant en péril l'équipe, et tout cela me semblait une blague : si toute l'équipe restante disparaît et que je continue à écrire du code, tu ne perds que la moitié de l'équipe. D'un autre côté, si je continue à coder et que tu perds la moitié de l'équipe, cela semble être une très mauvaise gestion. Je n'y ai jamais vraiment réfléchi, je n'en ai jamais parlé, mais c'était quand même quelque part dans mon esprit. À l'arrière-plan de ma conscience, je pensais : « Vous rigolez, non ? ». Donc, le plus grand problème était moi et mes relations avec les gens. Maintenant, je me comprends beaucoup mieux, j'ai longtemps été lead technique chez les programmeurs, et maintenant je dis directement aux gens : tu sais, je suis comme je suis, et vous devrez faire avec – est-ce que ça vous dérange si je reste ici ? Et quand ils ont commencé à gérer cela, tout a fonctionné. En fait, je ne suis ni mauvais ni bon, je n'ai aucune mauvaise intention ou aspiration égoïste, c'est juste ma nature, et il faut juste vivre avec ça.

Andrey: Récemment, tout le monde a commencé à parler de la conscience de soi pour les introvertis, et en général des soft skills. Que peut-on en dire ?

CliffOui, c'était une prise de conscience et une leçon que j'ai tirée de mon divorce avec ma femme. Ce que j'ai retenu de ce divorce, c'est une meilleure compréhension de moi-même. Ainsi, j'ai commencé à comprendre les autres. Comprendre comment cette interaction fonctionne. Cela a entraîné des découvertes les unes après les autres. J'ai pris conscience de qui je suis et de ce que je représente. Ce que je fais : soit je me concentre sur la tâche, soit j'évite le conflit, ou autre chose – et ce niveau de conscience de soi m'aide vraiment à garder mon calme. Après cela, tout devient beaucoup plus simple. Une chose que j'ai remarquée non seulement en moi, mais aussi chez d'autres programmeurs, c'est l'incapacité à verbaliser des pensées lorsque l'on est sous stress émotionnel. Par exemple, tu es en train de coder, tu es dans un état de flux, et là on arrive en courant et commence à crier dans une crise en disant que quelque chose s'est cassé, et que des mesures extrêmes vont être prises. Et tu ne peux même pas dire un mot car tu es dans un état de stress émotionnel. Les connaissances acquises permettent de se préparer à ce moment, de le vivre et de passer à un plan de secours, après quoi on peut déjà agir. Donc oui, lorsque tu commences à réaliser comment tout cela fonctionne – c'est un événement énorme qui change la vie. 
Je n'ai pas pu trouver les bons mots moi-même, mais je me souviens de la séquence des actions. L'essence est que cette réaction est aussi physique que verbale, et tu as besoin d'espace. Un tel espace, dans le sens zen. C'est précisément cela qu'il faut expliquer, puis immédiatement se retirer – se retirer physiquement. Quand je suis silencieux, je peux traiter la situation sur le plan émotionnel. Au fur et à mesure que l'adrénaline atteint le cerveau, te passant en mode « fuir ou combattre », tu ne peux plus rien dire, non – maintenant tu es un idiot, un ingénieur sur qui on peut s'acharner, incapable de donner une réponse digne ou même d'arrêter l'attaque, et l'attaquant peut librement continuer à attaquer encore et encore. Il faut d'abord redevenir soi-même, restaurer le contrôle, sortir du mode « fuir ou combattre ».

Et pour cela, il est nécessaire d'avoir un espace verbal. Juste un espace libre. Si jamais on veut dire quelque chose, on peut simplement Affirmer cela, puis aller réellement trouver son « espace » : se promener dans un parc, se enfermer sous la douche - peu importe. L'essentiel est de se déconnecter temporairement de cette situation. Dès que tu te déconnectes, même pendant quelques secondes, le contrôle revient, tu commences à penser clairement. « Bien, je ne suis pas un idiot, je ne fais pas de choses stupides, je suis une personne plutôt utile ». Une fois que tu as réussi à te convaincre, il est temps de passer à l'étape suivante : comprendre ce qui s'est passé. Tu as été attaqué, l'attaque est venue d'où tu ne t'y attendais pas, c'était une embuscade lâche et injuste. C'est mal. La prochaine étape consiste à comprendre pourquoi l'attaquant avait besoin de cela. En effet, pourquoi ? Peut-être parce qu'il est lui-même en colère ? Pourquoi est-il en colère ? Par exemple, parce qu'il a raté quelque chose et ne peut pas accepter la responsabilité ? C'est par ce moyen qu'il faut traiter toute la situation avec précaution. Mais pour cela, il faut de l'espace pour manœuvrer, un espace verbal. La toute première étape est de rompre le contact verbal. Éviter la discussion verbale. L'annuler, s'en éloigner le plus rapidement possible. Si c'est une conversation téléphonique, repose simplement le combiné - c'est une compétence que j'ai acquise lors de mes échanges avec mon ex-femme. Si la conversation n'aboutit à rien de bon, dis simplement « au revoir » et raccroche. De l'autre côté du combiné : « blabla », tu réponds : « d'accord, salut ! » et tu raccroches. Tu interromps simplement la conversation. Cinq minutes plus tard, quand ta capacité à penser clairement te revient, que tu as légèrement refroidi, il devient possible de réfléchir à ce qui s'est réellement passé et ce qui va suivre. Et commencer à formuler une réponse réfléchie, plutôt que de simplement réagir émotionnellement. Pour moi, le véritable tournant dans la prise de conscience de soi a été que dans des situations stressantes émotionnellement, je ne peux pas parler. Sortir de cet état, réfléchir et planifier comment répondre et compenser les problèmes - ce sont les bonnes étapes lorsque tu ne peux pas parler. La façon la plus simple est de fuir la situation dans laquelle se manifeste le stress émotionnel et simplement de cesser d'y participer. Après cela, tu acquiers la capacité de penser, lorsque tu peux penser, la possibilité de parler, et ainsi de suite.

En fait, devant le tribunal, l'avocat de la partie adverse essaie de faire ça avec toi – maintenant, c'est clair pourquoi. Parce qu'il a la possibilité de te submerger au point que tu ne peux même plus prononcer ton nom, par exemple. Dans le sens le plus direct, tu ne peux pas parler. Si cela t'arrive et que tu sais que tu te retrouveras dans un endroit où des batailles verbales font rage, comme un tribunal, tu peux venir avec ton avocat. L'avocat se battra pour toi et mettra fin à l'attaque verbale, et ce sera tout à fait légal, et tu retrouveras ton espace zen perdu. Par exemple, il m'est arrivé plusieurs fois de devoir appeler ma famille, le juge a été assez amical à ce sujet, mais l'avocat de l'autre partie criait et criait sur moi, je ne pouvais même pas en placer une. Dans de tels cas, l'utilisation d'un intermédiaire fonctionne le mieux pour moi. L'intermédiaire interrompt toute cette pression qui s'abat sur toi de manière continue, tu retrouves cet espace zen nécessaire, et avec lui, la capacité de parler revient. C'est un domaine de connaissance dans lequel il faut beaucoup apprendre, beaucoup découvrir en soi, et tout cela se transforme en décisions stratégiques de haut niveau, différentes pour différentes personnes. Certains n'ont pas les problèmes décrits ci-dessus, en général, ils n'en ont pas chez les professionnels de la vente. Toutes ces personnes qui gagnent leur vie avec des mots – des chanteurs célèbres, des poètes, des figures religieuses et des politiciens, ils ont toujours quelque chose à dire. Ils n'ont pas ces problèmes, mais moi, je les ai.

Andrey: C'était... inattendu. Très bien, nous avons déjà beaucoup discuté et il est temps de conclure cette interview. Nous nous rencontrerons certainement à la conférence et pourrons poursuivre ce dialogue. À la rencontre sur Hydra !

Tu pourras continuer à discuter avec Cliff à la conférence Hydra 2019, qui se tiendra les 11 et 12 juillet 2019 à Saint-Pétersbourg. Il viendra avec une présentation «L'expérience de la mémoire transactionnelle matérielle d'Azul». Tickets disponibles à l'achat sur le site officiel.

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster