Gran entrevista a Cliff Click, el padre de la compilación JIT en Java

Gran entrevista a Cliff Click, el padre de la compilación JIT en JavaClic en el acantilado — CTO de Cratus (sensores IoT para mejora de procesos), fundador y cofundador de varias startups (incluidas Rocket Realtime School, Neurensic y H2O.ai) con varias salidas exitosas. ¡Cliff escribió su primer compilador a los 15 años (Pascal para el TRS Z-80)! Es mejor conocido por su trabajo en C2 en Java (Sea of ​​​​Nodes IR). Este compilador mostró al mundo que JIT podía producir código de alta calidad, lo que fue uno de los factores del surgimiento de Java como una de las principales plataformas de software modernas. Luego, Cliff ayudó a Azul Systems a construir una computadora central de 864 núcleos con software Java puro que admitía pausas de GC en un montón de 500 gigabytes en 10 milisegundos. En general, Cliff logró trabajar en todos los aspectos de la JVM.

 
Este habrapost es una gran entrevista con Cliff. Hablaremos de los siguientes temas:

  • Transición a optimizaciones de bajo nivel
  • Cómo hacer una gran refactorización
  • modelo de costos
  • Entrenamiento de optimización de bajo nivel
  • Ejemplos prácticos de mejora del rendimiento
  • Por qué crear tu propio lenguaje de programación
  • Carrera de ingeniero de rendimiento
  • Desafíos técnicos
  • Un poco sobre asignación de registros y multinúcleos.
  • El mayor desafío de la vida.

Las entrevistas son realizadas por:

  • Andrey Satarin de los servicios web de Amazon. En su carrera, logró trabajar en proyectos completamente diferentes: probó la base de datos distribuida NewSQL en Yandex, un sistema de detección de nubes en Kaspersky Lab, un juego multijugador en Mail.ru y un servicio para calcular los precios de divisas en Deutsche Bank. Interesado en probar sistemas backend y distribuidos a gran escala.
  • Vladímir Sitnikov de Netcracker. Diez años de trabajo en el rendimiento y escalabilidad de NetCracker OS, software utilizado por operadores de telecomunicaciones para automatizar procesos de gestión de redes y equipos de red. Interesado en problemas de rendimiento de bases de datos Java y Oracle. Autor de más de una docena de mejoras de rendimiento en el controlador JDBC oficial de PostgreSQL.

Transición a optimizaciones de bajo nivel

Andrés: Eres un gran nombre en el mundo de la compilación JIT, Java y el trabajo de rendimiento en general, ¿verdad? 

acantilado: ¡Es así!

Andrés: Comencemos con algunas preguntas generales sobre el trabajo escénico. ¿Qué opinas sobre la elección entre optimizaciones de alto y bajo nivel, como trabajar a nivel de CPU?

acantilado: Sí, aquí todo es sencillo. El código más rápido es el que nunca se ejecuta. Por lo tanto, siempre es necesario comenzar desde un nivel alto, trabajar en algoritmos. Una notación O mejor superará a una notación O peor a menos que intervengan algunas constantes lo suficientemente grandes. Las cosas de bajo nivel son las últimas. Normalmente, si has optimizado el resto de tu pila lo suficientemente bien y todavía quedan algunas cosas interesantes, ese es un nivel bajo. ¿Pero cómo empezar desde un nivel alto? ¿Cómo sabes que se ha realizado suficiente trabajo de alto nivel? Bueno... de ninguna manera. No hay recetas preparadas. Debe comprender el problema, decidir qué va a hacer (para no tomar medidas innecesarias en el futuro) y luego podrá descubrir el generador de perfiles, que puede decir algo útil. En algún momento, usted mismo se da cuenta de que se ha deshecho de cosas innecesarias y es hora de hacer algunos ajustes de bajo nivel. Este es definitivamente un tipo de arte especial. Hay mucha gente que hace cosas innecesarias, pero avanza tan rápido que no tiene tiempo para preocuparse por la productividad. Pero esto es hasta que la pregunta surge sin rodeos. Por lo general, el 99% de las veces a nadie le importa lo que hago, hasta el momento en que aparece algo importante en el camino crítico que a nadie le importa. Y aquí todo el mundo empieza a regañarte acerca de "por qué no funcionó perfectamente desde el principio". En general, siempre hay algo que mejorar en el rendimiento. ¡Pero el 99% de las veces no tienes clientes potenciales! Simplemente intentas hacer que algo funcione y en el proceso descubres lo que es importante. Nunca puedes saber de antemano que esta pieza tiene que ser perfecta, así que, de hecho, tienes que ser perfecto en todo. Pero esto es imposible y no lo haces. Siempre hay muchas cosas que arreglar, y eso es completamente normal.

Cómo hacer una gran refactorización

Andrés: ¿Cómo trabajas en una actuación? Este es un problema transversal. Por ejemplo, ¿alguna vez ha tenido que trabajar en problemas que surgen de la intersección de muchas funciones existentes?

acantilado: Trato de evitarlo. Si sé que el rendimiento será un problema, lo pienso antes de empezar a codificar, especialmente con estructuras de datos. Pero a menudo todo esto lo descubres mucho más tarde. Y luego hay que tomar medidas extremas y hacer lo que yo llamo “reescribir y conquistar”: hay que coger una pieza lo suficientemente grande. Parte del código aún tendrá que reescribirse debido a problemas de rendimiento o algo más. Cualquiera que sea el motivo para reescribir el código, casi siempre es mejor reescribir una parte más grande que una más pequeña. En ese momento, todos empiezan a temblar de miedo: “¡Dios mío, no puedes tocar tanto código!” Pero, de hecho, este enfoque casi siempre funciona mucho mejor. Debes abordar inmediatamente un gran problema, dibujar un círculo grande a su alrededor y decir: reescribiré todo dentro del círculo. El borde es mucho más pequeño que el contenido que contiene y que debe reemplazarse. Y si tal delimitación de límites te permite hacer el trabajo interior a la perfección, tus manos están libres, haz lo que quieras. Una vez que comprendas el problema, el proceso de reescritura será mucho más fácil, ¡así que dale un gran mordisco!
Al mismo tiempo, cuando realiza una reescritura importante y se da cuenta de que el rendimiento va a ser un problema, puede empezar a preocuparse inmediatamente por ello. Esto generalmente se convierte en cosas simples como "no copie datos, administre los datos de la manera más simple posible, hágalo pequeño". En reescrituras grandes, existen formas estándar de mejorar el rendimiento. Y casi siempre giran en torno a datos.

modelo de costos

Andrés: En uno de los podcasts habló de modelos de costes en el contexto de la productividad. ¿Puedes explicar qué quisiste decir con esto?

acantilado: Ciertamente. Nací en una época en la que el rendimiento del procesador era extremadamente importante. Y esta era regresa nuevamente: el destino no está exento de ironía. Empecé a vivir en la época de las máquinas de ocho bits; mi primer ordenador funcionaba con 256 bytes. Exactamente bytes. Todo era muy pequeño. Había que contar las instrucciones y, a medida que empezamos a ascender en la pila de lenguajes de programación, los lenguajes adquirieron más y más. Estaba Assembler, luego Basic, luego C, y C se encargó de muchos de los detalles, como la asignación de registros y la selección de instrucciones. Pero todo estaba bastante claro allí, y si hacía un puntero a una instancia de una variable, obtenía la carga y se conocía el costo de esta instrucción. El hardware produce una cierta cantidad de ciclos de máquina, por lo que la velocidad de ejecución de diferentes cosas se puede calcular simplemente sumando todas las instrucciones que vas a ejecutar. Cada comparación/prueba/bifurcación/llamada/carga/almacenamiento podría sumarse y decir: ese es el tiempo de ejecución para usted. Cuando trabaje para mejorar el rendimiento, definitivamente prestará atención a qué números corresponden a pequeños ciclos de calor. 
Pero tan pronto como cambia a Java, Python y cosas similares, rápidamente se aleja del hardware de bajo nivel. ¿Cuál es el costo de llamar a un captador en Java? Si JIT en HotSpot es correcto en línea, se cargará, pero si no lo hizo, será una llamada de función. Dado que la llamada está en un bucle activo, anulará todas las demás optimizaciones en ese bucle. Por tanto, el coste real será mucho mayor. E inmediatamente pierde la capacidad de mirar un fragmento de código y comprender que debemos ejecutarlo en términos de la velocidad del reloj del procesador, la memoria y el caché utilizados. Todo esto se vuelve interesante sólo si realmente te sumerges en la actuación.
Ahora nos encontramos en una situación en la que las velocidades de los procesadores apenas han aumentado desde hace una década. ¡Los viejos tiempos han vuelto! Ya no se puede contar con un buen rendimiento de un solo subproceso. Pero si de repente te metes en la computación paralela, es increíblemente difícil, todo el mundo te mira como James Bond. Aquí, las aceleraciones diez veces mayores suelen ocurrir en lugares donde alguien ha estropeado algo. La concurrencia requiere mucho trabajo. Para obtener esa aceleración XNUMX veces mayor, es necesario comprender el modelo de costos. ¿Qué y cuánto cuesta? Y para hacer esto, es necesario comprender cómo encaja la lengüeta en el hardware subyacente.
Martin Thompson eligió una gran palabra para su blog. Simpatía mecánica! En primer lugar, es necesario comprender qué va a hacer el hardware, cómo lo hará exactamente y por qué hace lo que hace. Con esto, es bastante fácil comenzar a contar instrucciones y determinar adónde va el tiempo de ejecución. Si no tienes el entrenamiento adecuado, simplemente estás buscando un gato negro en una habitación oscura. Veo gente optimizando el rendimiento todo el tiempo sin tener idea de qué diablos están haciendo. Sufren mucho y no progresan mucho. Y cuando tomo el mismo fragmento de código, introduzco un par de pequeños trucos y obtengo una aceleración de cinco o diez veces, dicen: bueno, eso no es justo, ya sabíamos que eras mejor. Asombroso. De qué estoy hablando... el modelo de costos trata sobre qué tipo de código se escribe y qué tan rápido se ejecuta en promedio en el panorama general.

Andrés: ¿Y cómo puedes mantener semejante volumen en tu cabeza? ¿Esto se logra con más experiencia o? ¿De dónde viene tal experiencia?

acantilado: Bueno, no obtuve mi experiencia de la manera más fácil. Programé en ensamblador en los días en que se podía entender cada instrucción. Suena estúpido, pero desde entonces el conjunto de instrucciones del Z80 siempre ha estado en mi cabeza, en mi memoria. No recuerdo los nombres de las personas al minuto de hablar, pero recuerdo el código escrito hace 40 años. Es curioso, parece un síndrome "científico idiota".

Entrenamiento de optimización de bajo nivel

Andrés: ¿Existe una manera más fácil de entrar?

acantilado: Si y no. El hardware que todos utilizamos no ha cambiado mucho con el tiempo. Todo el mundo usa x86, a excepción de los teléfonos inteligentes Arm. Si no estás haciendo algún tipo de incrustación intensa, estás haciendo lo mismo. Bien, el siguiente. Las instrucciones tampoco han cambiado durante siglos. Tienes que ir y escribir algo en Asamblea. No mucho, pero lo suficiente para empezar a entender. Estás sonriendo, pero hablo completamente en serio. Es necesario comprender la correspondencia entre lenguaje y hardware. Después de eso, debes escribir un poco y hacer un pequeño compilador de juguetes para un pequeño lenguaje de juguetes. Parecer un juguete significa que debe fabricarse en un tiempo razonable. Puede ser súper simple, pero debe generar instrucciones. El acto de generar una instrucción le ayudará a comprender el modelo de costos para el puente entre el código de alto nivel que todos escriben y el código de máquina que se ejecuta en el hardware. Esta correspondencia quedará grabada en el cerebro en el momento en que se escriba el compilador. Incluso el compilador más simple. Después de eso, puedes empezar a mirar Java y el hecho de que su abismo semántico es mucho más profundo y es mucho más difícil tender puentes sobre él. En Java, es mucho más difícil entender si nuestro puente resultó bueno o malo, qué hará que se desmorone y qué no. Pero necesita algún tipo de punto de partida en el que mire el código y comprenda: "sí, este captador debe estar incluido siempre". Y luego resulta que a veces esto sucede, excepto en la situación en la que el método se vuelve demasiado grande y el JIT comienza a incorporar todo. El rendimiento de dichos lugares se puede predecir al instante. Por lo general, los captadores funcionan bien, pero luego observas grandes bucles activos y te das cuenta de que hay algunas llamadas a funciones flotando por ahí que no saben lo que están haciendo. Este es el problema con el uso generalizado de captadores, la razón por la que no están en línea es que no está claro si son captadores. Si tiene una base de código muy pequeña, simplemente puede recordarla y luego decir: este es un captador y este es un definidor. En una gran base de código, cada función vive su propia historia, que, en general, nadie conoce. El generador de perfiles dice que perdimos el 24% del tiempo en algún bucle y para comprender qué está haciendo este bucle, debemos observar cada función interna. Es imposible entender esto sin estudiar la función, y esto ralentiza seriamente el proceso de comprensión. Por eso no uso getters ni setters, ¡he alcanzado un nuevo nivel!
¿Dónde conseguir el modelo de costes? Bueno, puedes leer algo, por supuesto... Pero creo que la mejor manera es actuar. Hacer un pequeño compilador será la mejor manera de comprender el modelo de costos y adaptarlo a su propia cabeza. Un pequeño compilador que sea adecuado para programar un microondas es una tarea para principiantes. Bueno, quiero decir, si ya tienes habilidades de programación, entonces eso debería ser suficiente. Todas estas cosas, como analizar una cadena que tiene como una especie de expresión algebraica, extraer instrucciones para operaciones matemáticas de allí en el orden correcto, tomar los valores correctos de los registros, todo esto se hace a la vez. Y mientras lo haces, quedará grabado en tu cerebro. Creo que todo el mundo sabe lo que hace un compilador. Y esto le permitirá comprender el modelo de costos.

Ejemplos prácticos de mejora del rendimiento

Andrés: ¿A qué más debería prestar atención cuando trabaja en productividad?

acantilado: Estructuras de datos. Por cierto, sí, hace mucho que no doy estas clases... escuela de cohetes. Fue divertido, pero requirió mucho esfuerzo, ¡y yo también tengo una vida! DE ACUERDO. Entonces, en una de las clases más importantes e interesantes, "¿Adónde va tu desempeño?", les di a los estudiantes un ejemplo: se leyeron dos gigabytes y medio de datos fintech de un archivo CSV y luego tuvieron que calcular la cantidad de productos vendidos. . Datos periódicos del mercado de ticks. Paquetes UDP convertidos a formato de texto desde los años 70. Bolsa Mercantil de Chicago: todo tipo de cosas como mantequilla, maíz, soja y cosas así. Era necesario contar estos productos, el número de transacciones, el volumen medio de movimiento de fondos y bienes, etc. Son matemáticas comerciales bastante simples: busque el código del producto (que son 1 o 2 caracteres en la tabla hash), obtenga la cantidad, agréguela a uno de los conjuntos comerciales, agregue volumen, agregue valor y un par de cosas más. Matemáticas muy simples. La implementación del juguete fue muy sencilla: todo está en un archivo, leo el archivo y lo recorro, dividiendo los registros individuales en cadenas de Java, buscando lo necesario en ellos y sumándolos de acuerdo con las matemáticas descritas anteriormente. Y funciona a baja velocidad.

Con este enfoque, es obvio lo que está pasando y la computación paralela no ayudará, ¿verdad? Resulta que se puede lograr quintuplicar el rendimiento simplemente eligiendo las estructuras de datos adecuadas. ¡Y esto sorprende incluso a los programadores experimentados! En mi caso particular, el truco fue que no debías realizar asignaciones de memoria en un bucle activo. Bueno, esto no es toda la verdad, pero en general, no debes resaltar "una vez en X" cuando X es lo suficientemente grande. Cuando X tiene dos gigabytes y medio, no debes asignar nada “una vez por letra”, o “una vez por línea”, o “una vez por campo”, nada por el estilo. Aquí es donde se gasta el tiempo. ¿Cómo funciona esto? Imagínate haciendo una llamada String.split() o BufferedReader.readLine(). Readline crea una cadena a partir de un conjunto de bytes que llegaron a la red, una vez por cada línea, por cada uno de los cientos de millones de líneas. Tomo esta línea, la analizo y la tiro. ¿Por qué lo tiro? Bueno, ya lo procesé, eso es todo. Entonces, por cada byte leído de estos 2.7G, se escribirán dos caracteres en la línea, es decir, ya 5.4G, y no los necesito para nada más, por lo que los descarto. Si nos fijamos en el ancho de banda de la memoria, cargamos 2.7G que pasan por la memoria y el bus de memoria en el procesador, y luego se envía el doble a la línea que se encuentra en la memoria, y todo esto se desgasta cuando se crea cada nueva línea. Pero necesito leerlo, el hardware lo lee, incluso si todo se desgasta más tarde. Y tengo que anotarlo porque creé una línea y los cachés están llenos; el caché no puede acomodar 2.7G. Entonces, por cada byte que leo, leo dos bytes más y escribo dos bytes más, y al final tienen una proporción de 4:1; en esta proporción estamos desperdiciando ancho de banda de memoria. Y luego resulta que si lo hago String.split() – esta no es la última vez que hago esto, puede haber otros 6-7 campos dentro. Entonces, el código clásico de leer CSV y luego analizar las cadenas da como resultado un desperdicio de ancho de banda de memoria de aproximadamente 14:1 en relación con lo que realmente le gustaría tener. Si descarta estas selecciones, puede obtener una aceleración cinco veces mayor.

Y no es tan difícil. Si miras el código desde el ángulo correcto, todo se vuelve bastante simple una vez que te das cuenta del problema. No debes dejar de asignar memoria por completo: el único problema es que asignas algo y muere inmediatamente, y en el camino quema un recurso importante, que en este caso es el ancho de banda de la memoria. Y todo esto se traduce en una caída de la productividad. En x86 normalmente necesitas grabar activamente ciclos de procesador, pero aquí quemaste toda la memoria mucho antes. La solución es reducir la cantidad de secreción. 
La otra parte del problema es que si ejecuta el generador de perfiles cuando se agota la franja de memoria, justo cuando sucede, generalmente está esperando a que regrese el caché porque está lleno de basura que acaba de producir, todas esas líneas. Por lo tanto, cada operación de carga o almacenamiento se vuelve lenta, porque provocan errores de caché: todo el caché se vuelve lento, esperando que la basura salga de él. Por lo tanto, el generador de perfiles solo mostrará un ruido cálido y aleatorio extendido a lo largo de todo el ciclo; no habrá ninguna instrucción activa separada ni lugar en el código. Sólo ruido. Y si nos fijamos en los ciclos de GC, todos son de generación joven y súper rápidos: microsegundos o milisegundos como máximo. Después de todo, todo este recuerdo muere instantáneamente. Le asignas miles de millones de gigabytes y él los corta, los corta y los vuelve a cortar. Todo esto sucede muy rápidamente. Resulta que hay ciclos de GC baratos, ruido cálido durante todo el ciclo, pero queremos obtener una aceleración de 5 veces. En ese momento, algo debería cerrarse en tu cabeza y sonar: “¡¿Por qué es esto?!” El desbordamiento de la tira de memoria no se muestra en el depurador clásico; debe ejecutar el depurador del contador de rendimiento del hardware y verlo usted mismo y directamente. Pero esto no se puede sospechar directamente a partir de estos tres síntomas. El tercer síntoma es cuando miras lo que resaltas, le preguntas al perfilador y él responde: "Hiciste mil millones de filas, pero el GC funcionó gratis". Tan pronto como esto sucede, te das cuenta de que has creado demasiados objetos y has quemado todo el carril de la memoria. Hay una manera de resolver esto, pero no es obvia. 

El problema está en la estructura de datos: la estructura desnuda que subyace a todo lo que sucede es demasiado grande, tiene 2.7 G en el disco, por lo que hacer una copia de esto es muy indeseable; desea cargarlo desde el búfer de bytes de la red inmediatamente. en los registros, para no leer y escribir en la línea de un lado a otro cinco veces. Desafortunadamente, Java no ofrece dicha biblioteca como parte del JDK de forma predeterminada. Pero esto es trivial, ¿verdad? Esencialmente, estas son de 5 a 10 líneas de código que se usarán para implementar su propio cargador de cadenas en búfer, que repite el comportamiento de la clase de cadena, al mismo tiempo que envuelve el búfer de bytes subyacente. Como resultado, resulta que está trabajando casi como si fuera con cadenas, pero en realidad los punteros al búfer se mueven allí y los bytes sin procesar no se copian en ninguna parte y, por lo tanto, los mismos búferes se reutilizan una y otra vez, y el sistema operativo está feliz de encargarse de las cosas para las que está diseñado, como el doble buffer oculto de estos buffers de bytes, y usted ya no tendrá que procesar un flujo interminable de datos innecesarios. Por cierto, ¿entiendes que cuando se trabaja con GC, se garantiza que cada asignación de memoria no será visible para el procesador después del último ciclo de GC? Por lo tanto, no es posible que todo esto esté en el caché y entonces se produce un error 100% garantizado. Cuando se trabaja con un puntero, en x86, restar un registro de la memoria requiere de 1 a 2 ciclos de reloj, y tan pronto como esto sucede, paga, paga, paga, porque la memoria está toda encendida. NUEVE cachés – y este es el costo de la asignación de memoria. Valor real.

En otras palabras, las estructuras de datos son lo más difícil de cambiar. Y una vez que te das cuenta de que has elegido la estructura de datos incorrecta que acabará con el rendimiento más adelante, normalmente hay mucho trabajo por hacer, pero si no lo haces, las cosas empeorarán. En primer lugar, hay que pensar en las estructuras de datos, esto es importante. El costo principal aquí recae en las estructuras de datos gruesas, que están comenzando a usarse en el estilo de "copié la estructura de datos X en la estructura de datos Y porque me gusta más la forma de Y". Pero la operación de copia (que parece barata) en realidad desperdicia ancho de banda de memoria y ahí es donde se entierra todo el tiempo de ejecución desperdiciado. Si tengo una cadena gigante de JSON y quiero convertirla en un árbol DOM estructurado de POJO o algo así, la operación de analizar esa cadena y construir el POJO, y luego acceder al POJO nuevamente más tarde, resultará en un costo innecesario: es no es barato. Excepto si corre alrededor de POJO con mucha más frecuencia que alrededor de una cadena. De improviso, puedes intentar descifrar la cadena y extraer solo lo que necesitas de allí, sin convertirlo en ningún POJO. Si todo esto sucede en una ruta desde la cual se requiere el máximo rendimiento, no hay POJO para usted, de alguna manera necesita profundizar en la línea directamente.

Por qué crear tu propio lenguaje de programación

Andrés: Dijiste que para entender el modelo de costos, necesitas escribir tu propio lenguaje...

acantilado: No es un lenguaje, sino un compilador. Un lenguaje y un compilador son dos cosas diferentes. La diferencia más importante está en tu cabeza. 

Andrés: Por cierto, hasta donde yo sé, estás experimentando con la creación de tus propios lenguajes. ¿Para qué?

acantilado: ¡Porque puedo! Estoy semi-retirado, así que este es mi hobby. Llevo toda mi vida implementando lenguajes ajenos. También trabajé mucho en mi estilo de codificación. Y también porque veo problemas en otros idiomas. Veo que hay mejores maneras de hacer cosas familiares. Y yo los usaría. Simplemente estoy cansado de ver problemas en mí mismo, en Java, en Python, en cualquier otro lenguaje. Ahora escribo en React Native, JavaScript y Elm como un pasatiempo que no tiene que ver con la jubilación, sino con el trabajo activo. También escribo en Python y, muy probablemente, continuaré trabajando en aprendizaje automático para backends de Java. Hay muchos idiomas populares y todos tienen características interesantes. Cada uno es bueno a su manera y puedes intentar reunir todas estas características. Entonces, estoy estudiando cosas que me interesan, el comportamiento del lenguaje, tratando de encontrar una semántica razonable. ¡Y hasta ahora lo estoy logrando! En este momento estoy luchando con la semántica de la memoria, porque quiero tenerla como en C y Java, y obtener un modelo de memoria y una semántica de memoria sólidos para cargas y almacenes. Al mismo tiempo, tenga inferencia de tipos automática como en Haskell. Aquí, estoy tratando de combinar la inferencia de tipos tipo Haskell con el trabajo de memoria tanto en C como en Java. Esto es lo que he estado haciendo durante los últimos 2 o 3 meses, por ejemplo.

Andrés: Si construyes un lenguaje que toma mejores aspectos de otros lenguajes, ¿crees que alguien hará lo contrario: tomar tus ideas y usarlas?

acantilado: ¡Así es exactamente como aparecen los nuevos idiomas! ¿Por qué Java es similar a C? Porque C tenía una buena sintaxis que todos entendían y Java se inspiró en esta sintaxis, agregando seguridad de tipos, verificación de límites de matrices, GC, y también mejoraron algunas cosas de C. Agregaron las suyas propias. Pero se inspiraron bastante, ¿verdad? Todos se apoyan en los hombros de los gigantes que vinieron antes que usted: así es como se logra el progreso.

Andrés: Según tengo entendido, su idioma estará a salvo de la memoria. ¿Has pensado en implementar algo como un verificador de préstamos de Rust? ¿Lo has mirado, qué piensas de él?

acantilado: Bueno, he estado escribiendo C durante años, con todo este malloc y gratis, y administrando manualmente la vida útil. Ya sabes, el 90-95% de la vida útil controlada manualmente tiene la misma estructura. Y es muy, muy doloroso hacerlo manualmente. Me gustaría que el compilador simplemente le dijera qué está sucediendo allí y qué logró con sus acciones. Para algunas cosas, el verificador de préstamos hace esto de forma inmediata. Y debería mostrar información automáticamente, comprenderlo todo y ni siquiera cargarme con la presentación de esta comprensión. Debe realizar al menos un análisis de escape local, y sólo si falla, entonces necesita agregar anotaciones de tipo que describan la vida útil, y dicho esquema es mucho más complejo que un verificador de préstamos o, de hecho, cualquier verificador de memoria existente. La elección entre "todo está bien" y "no entiendo nada" - no, debe haber algo mejor. 
Entonces, como alguien que ha escrito mucho código en C, creo que lo más importante es tener soporte para el control automático de la vida útil. También estoy harto de la cantidad de memoria que utiliza Java y la principal queja es el GC. Cuando asigna memoria en Java, no recuperará la memoria que era local en el último ciclo de GC. Este no es el caso en lenguajes con una gestión de memoria más precisa. Si llama a malloc, obtendrá inmediatamente la memoria que normalmente se acaba de usar. Por lo general, haces algunas cosas temporales con la memoria y la devuelves inmediatamente. E inmediatamente regresa al grupo de malloc, y el siguiente ciclo de malloc lo saca nuevamente. Por lo tanto, el uso real de la memoria se reduce al conjunto de objetos vivos en un momento dado, más las fugas. Y si no todo se filtra de una forma completamente indecente, la mayor parte de la memoria acaba en las cachés y en el procesador, y funciona rápidamente. Pero requiere mucha gestión manual de la memoria con malloc y llamadas gratuitas en el orden correcto y en el lugar correcto. Rust puede manejar esto adecuadamente por sí solo y, en muchos casos, ofrece un rendimiento aún mejor, ya que el consumo de memoria se reduce solo al cálculo actual, en lugar de esperar al siguiente ciclo de GC para liberar memoria. Como resultado, obtuvimos una forma muy interesante de mejorar el rendimiento. Y bastante poderoso; quiero decir, hice esas cosas cuando procesaba datos para fintech, y esto me permitió acelerar aproximadamente cinco veces. Es un impulso bastante grande, especialmente en un mundo donde los procesadores no son cada vez más rápidos y todavía estamos esperando mejoras.

Carrera de ingeniero de rendimiento

Andrés: También me gustaría preguntar sobre carreras en general. Saltó a la fama con su trabajo JIT en HotSpot y luego se mudó a Azul, que también es una empresa de JVM. Pero ya estábamos trabajando más en hardware que en software. Y luego, de repente, pasaron a Big Data y Machine Learning, y luego a la detección de fraude. ¿Cómo pasó esto? Se trata de áreas de desarrollo muy diferentes.

acantilado: Llevo bastante tiempo programando y he logrado tomar muchas clases diferentes. Y cuando la gente dice: "¡Oh, tú eres el que hizo JIT para Java!", siempre es gracioso. Pero antes de eso, estaba trabajando en un clon de PostScript, el lenguaje que Apple alguna vez usó para sus impresoras láser. Y antes de eso hice una implementación del lenguaje Forth. Creo que un tema común para mí es el desarrollo de herramientas. Toda mi vida he estado creando herramientas con las que otras personas escriben sus interesantes programas. Pero también estuve involucrado en el desarrollo de sistemas operativos, controladores, depuradores a nivel de kernel, lenguajes para el desarrollo de sistemas operativos, que al principio eran triviales, pero con el tiempo se volvieron cada vez más complejos. Pero el tema principal sigue siendo el desarrollo de herramientas. Gran parte de mi vida transcurrió entre Azul y Sun, y se trataba de Java. Pero cuando entré en Big Data y Machine Learning, volví a ponerme mi elegante sombrero y dije: "Oh, ahora tenemos un problema no trivial, y están sucediendo muchas cosas interesantes y hay gente haciendo cosas". Este es un gran camino de desarrollo a seguir.

Sí, realmente amo la computación distribuida. Mi primer trabajo fue como estudiante de C, en un proyecto publicitario. Se trataba de computación distribuida en chips Zilog Z80 que recopilaban datos para OCR analógico, producidos por un analizador analógico real. Era un tema genial y completamente loco. Pero había problemas, alguna parte no se reconocía correctamente, entonces había que sacar una foto y mostrársela a una persona que ya sabía leer con los ojos y reportar lo que decía, y por eso había trabajos con datos, y estos trabajos tenían su propio idioma. Había un backend que procesaba todo esto (Z80 ejecutándose en paralelo con terminales vt100 ejecutándose), uno por persona, y había un modelo de programación paralela en el Z80. Alguna pieza de memoria común compartida por todos los Z80 dentro de una configuración en estrella; El backplane también se compartía y la mitad de la RAM se compartía dentro de la red y la otra mitad era privada o se destinaba a otra cosa. Un sistema distribuido paralelo significativamente complejo con memoria compartida... semicompartida. ¿Cuándo fue esto? Ni siquiera lo recuerdo, a mediados de los 80. Hace bastante tiempo. 
Sí, supongamos que 30 años es hace mucho tiempo. Los problemas relacionados con la computación distribuida existen desde hace bastante tiempo; la gente lleva mucho tiempo en guerra con Beowulf-racimos. Tales clusters se ven así... Por ejemplo: hay Ethernet y su rápido x86 está conectado a esta Ethernet, y ahora quiere obtener una memoria compartida falsa, porque nadie podía hacer codificación informática distribuida entonces, era demasiado difícil y por lo tanto era una memoria compartida falsa con páginas de memoria de protección en x86, y si escribías en esta página, entonces les decíamos a otros procesadores que si acceden a la misma memoria compartida, tendrías que cargarla desde ti y, por lo tanto, algo así como un protocolo para soportar Apareció la coherencia de caché y el software para esto. Interesante concepto. El verdadero problema, por supuesto, era otro. Todo esto funcionó, pero rápidamente surgieron problemas de rendimiento, porque nadie entendía los modelos de rendimiento a un nivel suficientemente bueno: qué patrones de acceso a la memoria había, cómo asegurarse de que los nodos no hicieran ping interminablemente entre sí, etc.

Lo que se me ocurrió en H2O es que son los propios desarrolladores los responsables de determinar dónde está oculto el paralelismo y dónde no. Se me ocurrió un modelo de codificación que hizo que escribir código de alto rendimiento fuera fácil y sencillo. Pero escribir código de ejecución lenta es difícil y se verá mal. Debe intentar seriamente escribir código lento; tendrá que utilizar métodos no estándar. El código de frenado es visible a primera vista. Como resultado, normalmente se escribe código que se ejecuta rápidamente, pero hay que averiguar qué hacer en el caso de la memoria compartida. Todo esto está ligado a matrices grandes y el comportamiento allí es similar al de las matrices grandes no volátiles en Java paralelo. Quiero decir, imagina que dos subprocesos escriben en una matriz paralela, uno de ellos gana y el otro, en consecuencia, pierde, y no sabes cuál es cuál. Si no son volátiles, entonces el orden puede ser el que quieras, y esto funciona muy bien. La gente realmente se preocupa por el orden de las operaciones, colocan volátiles en los lugares correctos y esperan problemas de rendimiento relacionados con la memoria en los lugares correctos. De lo contrario, simplemente escribirían código en forma de bucles de 1 a N, donde N es unos billones, con la esperanza de que todos los casos complejos se vuelvan automáticamente paralelos, y eso no funciona allí. Pero en H2O esto no es Java ni Scala; puedes considerarlo “Java menos menos” si quieres. Este es un estilo de programación muy claro y es similar a escribir código C o Java simple con bucles y matrices. Pero al mismo tiempo, la memoria se puede procesar en terabytes. Todavía uso H2O. Lo uso de vez en cuando en diferentes proyectos y sigue siendo el más rápido, decenas de veces más rápido que sus competidores. Si estás haciendo Big Data con datos en columnas, es muy difícil superar al H2O.

Desafíos técnicos

Andrés: ¿Cuál ha sido tu mayor desafío en toda tu carrera?

acantilado: ¿Estamos discutiendo la parte técnica o no técnica del tema? Yo diría que los mayores desafíos no son técnicos. 
En cuanto a los desafíos técnicos. Simplemente los derroté. Ni siquiera sé cuál fue el más grande, pero hubo algunos bastante interesantes que requirieron bastante tiempo, lucha mental. Cuando fui a Sun, estaba seguro de que haría un compilador rápido, y un grupo de personas mayores dijeron en respuesta que nunca lo lograría. Pero seguí este camino, escribí un compilador hasta el asignador de registros y fue bastante rápido. Era tan rápido como el C1 moderno, pero el asignador era mucho más lento en aquel entonces y, en retrospectiva, era un gran problema de estructura de datos. Lo necesitaba para escribir un asignador de registros gráfico y no entendía el dilema entre la expresividad del código y la velocidad, que existía en esa época y era muy importante. Resultó que la estructura de datos generalmente excede el tamaño de la caché en x86 de esa época y, por lo tanto, si inicialmente asumí que el asignador de registros calcularía entre el 5 y el 10 por ciento del tiempo total de fluctuación, en realidad resultó ser 50 por ciento.

A medida que pasó el tiempo, el compilador se volvió más limpio y eficiente, dejó de generar código terrible en más casos y el rendimiento comenzó a parecerse cada vez más a lo que produce un compilador de C. A menos, por supuesto, que escribas alguna tontería que ni siquiera C acelera. . Si escribe código como C, obtendrá un rendimiento como C en más casos. Y cuanto más avanzaba, más a menudo obtenía código que coincidía asintóticamente con el nivel C, el asignador de registros comenzaba a verse como algo completo... independientemente de si su código se ejecuta rápido o lento. Continué trabajando en el asignador para que hiciera mejores selecciones. Se volvió cada vez más lento, pero cada vez ofrecía un mejor desempeño en los casos en los que nadie más podía hacer frente. Podría sumergirme en un asignador de registros, enterrar un mes de trabajo allí y, de repente, todo el código comenzaría a ejecutarse un 5% más rápido. Esto sucedió una y otra vez y el asignador de registros se convirtió en una especie de obra de arte: todos lo amaron o lo odiaron, y la gente de la academia hizo preguntas sobre el tema "¿por qué se hace todo de esta manera?", ¿por qué no? escaneo de líneay cuál es la diferencia. La respuesta sigue siendo la misma: un asignador basado en el color del gráfico más un trabajo muy cuidadoso con el código del buffer es igual a un arma de victoria, la mejor combinación que nadie puede derrotar. Y esto es algo que no es nada obvio. Todo lo demás que hace el compilador allí está bastante bien estudiado, aunque también ha sido llevado al nivel del arte. Siempre hice cosas que se suponía que convertirían al compilador en una obra de arte. Pero nada de esto fue nada extraordinario, excepto el asignador de registros. El truco es tener cuidado reducir bajo carga y, si esto sucede (puedo explicarlo con más detalle si está interesado), significa que puede alinear de manera más agresiva, sin el riesgo de caer en un problema en el cronograma de desempeño. En aquellos días, había un grupo de compiladores a gran escala, cargados de chucherías y silbatos, que tenían asignadores de registros, pero nadie más podía hacerlo.

El problema es que si agrega métodos que están sujetos a inserción, aumento y aumento del área de inserción, el conjunto de valores utilizados supera instantáneamente el número de registros y debe eliminarlos. El nivel crítico generalmente llega cuando el asignador se da por vencido, y un buen candidato para un derrame vale otro, venderás algunas cosas generalmente locas. El valor de insertar aquí es que pierde parte de la sobrecarga, la sobrecarga de llamar y guardar, puede ver los valores dentro y optimizarlos aún más. El costo de la inserción es que se forma una gran cantidad de valores activos, y si su asignador de registros se quema más de lo necesario, inmediatamente pierde. Por lo tanto, la mayoría de los asignadores tienen un problema: cuando el inlining cruza una determinada línea, todo en el mundo comienza a reducirse y la productividad puede tirarse por el retrete. Quienes implementan el compilador añaden algunas heurísticas: por ejemplo, para dejar de insertar, comenzando con un tamaño suficientemente grande, ya que las asignaciones arruinarán todo. Así es como se forma una curvatura en el gráfico de rendimiento: estás en línea, en línea, el rendimiento crece lentamente, ¡y luego boom! – cae como un gato veloz porque te alineaste demasiado. Así funcionaba todo antes de la llegada de Java. Java requiere mucha más integración, por lo que tuve que hacer que mi asignador sea mucho más agresivo para que se nivele en lugar de fallar, y si se integra demasiado, comienza a derramarse, pero luego llega el momento de "no más derrames". Esta es una observación interesante que surgió de la nada, no es obvia, pero valió la pena. Tomé una inserción agresiva y me llevó a lugares donde el rendimiento de Java y C funcionan uno al lado del otro. Están muy cerca: puedo escribir código Java que es significativamente más rápido que el código C y cosas así, pero en promedio, en el panorama general, son más o menos comparables. Creo que parte de este mérito es el asignador de registros, que me permite alinear de la forma más estúpida posible. Simplemente alineo todo lo que veo. La pregunta aquí es si el asignador funciona bien, si el resultado es un código que funciona de manera inteligente. Este fue un gran desafío: entender todo esto y hacerlo funcionar.

Un poco sobre asignación de registros y multinúcleos.

Vladimir: Problemas como la asignación de registros parecen un tema eterno e interminable. Me pregunto si alguna vez ha existido una idea que parecía prometedora y luego fracasó en la práctica.

acantilado: ¡Ciertamente! La asignación de registros es un área en la que se intenta encontrar algunas heurísticas para resolver un problema NP completo. Y nunca podrás lograr una solución perfecta, ¿verdad? Esto es simplemente imposible. Mire, la compilación Ahead of Time también funciona mal. La conversación aquí trata sobre algunos casos promedio. Acerca del rendimiento típico, así que puedes ir y medir algo que creas que es un buen rendimiento típico; después de todo, ¡estás trabajando para mejorarlo! La asignación de registros es un tema que tiene que ver con el rendimiento. Una vez que se tiene el primer prototipo, se trabaja y pinta lo necesario, comienza el trabajo de actuación. Necesitas aprender a medir bien. ¿Por qué es importante? Si tienes datos claros, puedes mirar diferentes áreas y ver: sí, ayudó aquí, ¡pero ahí es donde todo se rompió! Surgen algunas buenas ideas, agregas nuevas heurísticas y de repente todo empieza a funcionar un poco mejor en promedio. O no arranca. Tuve un montón de casos en los que estábamos luchando por el rendimiento del cinco por ciento que diferenciaba nuestro desarrollo del asignador anterior. Y siempre se ve así: en algún lugar se gana, en algún lugar se pierde. Si tiene buenas herramientas de análisis de desempeño, puede encontrar las ideas perdedoras y comprender por qué fracasan. Tal vez valga la pena dejar todo como está, o tal vez tomar un enfoque más serio para realizar ajustes, o salir y arreglar algo más. ¡Son un montón de cosas! Hice este truco genial, pero también necesito este, y este, y este, y su combinación total ofrece algunas mejoras. Y los solitarios pueden fracasar. Ésta es la naturaleza del trabajo de desempeño en problemas NP-completos.

Vladimir: Uno tiene la sensación de que cosas como pintar en asignadores son un problema que ya se ha resuelto. Bueno, está decidido por ti, a juzgar por lo que estás diciendo, entonces, ¿vale la pena...?

acantilado: No se resuelve como tal. Eres tú quien debe convertirlo en “solucionado”. Hay problemas difíciles y es necesario resolverlos. Una vez hecho esto, es hora de trabajar en la productividad. Debe abordar este trabajo en consecuencia: realizar evaluaciones comparativas, recopilar métricas, explicar situaciones en las que, al volver a una versión anterior, su antiguo truco comenzó a funcionar nuevamente (o viceversa, se detuvo). Y no te rindas hasta lograr algo. Como ya dije, si hay ideas interesantes que no funcionaron, pero en el campo de la asignación de registros de ideas es casi infinito. Puede, por ejemplo, leer publicaciones científicas. Aunque ahora esta zona ha comenzado a avanzar mucho más lentamente y se ha vuelto más clara que en su juventud. Sin embargo, hay innumerables personas trabajando en este campo y vale la pena probar todas sus ideas, todas están esperando entre bastidores. Y no puedes saber lo buenos que son a menos que los pruebes. ¿Qué tan bien se integran con todo lo demás en su asignador? Porque un asignador hace muchas cosas y algunas ideas en su asignador específico no funcionarán, pero en otro asignador sí funcionarán fácilmente. La principal forma de ganar para el asignador es sacar el material lento fuera de la ruta principal y obligarlo a dividirse a lo largo de los límites de las rutas lentas. Entonces, si desea ejecutar un GC, tomar el camino lento, desoptimizar, lanzar una excepción, todo eso; usted sabe que estas cosas son relativamente raras. Y son realmente raros, lo comprobé. Haces trabajo extra y elimina muchas de las restricciones en estos caminos lentos, pero en realidad no importa porque son lentos y rara vez se recorren. Por ejemplo, un puntero nulo nunca sucede, ¿verdad? Es necesario tener varios caminos para diferentes cosas, pero no deben interferir con el principal. 

Vladimir: ¿Qué opinas de los núcleos múltiples, cuando hay miles de núcleos a la vez? ¿Es esto algo útil?

acantilado: ¡El éxito de la GPU demuestra que es bastante útil!

Vladimir: Son bastante especializados. ¿Qué pasa con los procesadores de propósito general?

acantilado: Bueno, ese era el modelo de negocio de Azul. La respuesta llegó en una época en la que a la gente realmente le encantaba el rendimiento predecible. En aquel entonces era difícil escribir código paralelo. El modelo de codificación H2O es altamente escalable, pero no es un modelo de propósito general. Quizás un poco más general que cuando se utiliza una GPU. ¿Estamos hablando de la complejidad de desarrollar algo así o de la complejidad de usarlo? Por ejemplo, Azul me enseñó una lección interesante, bastante poco obvia: los cachés pequeños son normales. 

El mayor desafío de la vida.

Vladimir: ¿Qué pasa con los desafíos no técnicos?

acantilado: El mayor desafío era no ser... amable y amable con la gente. Y como resultado, constantemente me encontraba en situaciones extremadamente conflictivas. Aquellos en los que sabía que las cosas iban mal, pero no sabía cómo seguir adelante con esos problemas y no podía manejarlos. De esta manera surgieron muchos problemas a largo plazo, que duraron décadas. El hecho de que Java tenga compiladores C1 y C2 es una consecuencia directa de esto. El hecho de que no haya compilación multinivel en Java durante diez años seguidos también es una consecuencia directa. Es obvio que necesitábamos un sistema así, pero no es obvio por qué no existía. Tuve problemas con un ingeniero... o un grupo de ingenieros. Érase una vez, cuando comencé a trabajar en Sun, yo estaba... Bueno, no sólo entonces, generalmente siempre tengo mi propia opinión sobre todo. Y pensé que era cierto que podías tomar esa verdad tuya y contarla de frente. Especialmente porque sorprendentemente tenía razón la mayor parte del tiempo. Y si no te gusta este enfoque... especialmente si estás obviamente equivocado y haciendo tonterías... En general, pocas personas podrían tolerar esta forma de comunicación. Aunque algunos podrían, como yo. He construido toda mi vida sobre principios meritocráticos. Si me muestras algo mal, inmediatamente me daré la vuelta y diré: dijiste tonterías. Al mismo tiempo, por supuesto, pido disculpas y todo eso, tomaré nota de los méritos, si los hay, y tomaré otras acciones correctas. Por otro lado, estoy sorprendentemente en lo cierto acerca de un porcentaje sorprendentemente grande del tiempo total. Y no funciona muy bien en las relaciones con las personas. No intento ser amable, pero hago la pregunta sin rodeos. “Esto nunca funcionará, porque uno, dos y tres”. Y ellos decían: "¡Oh!" Hubo otras consecuencias que probablemente sería mejor ignorar: por ejemplo, las que me llevaron al divorcio de mi esposa y a diez años de depresión después de eso.

El desafío es una lucha con las personas, con su percepción de lo que se puede o no hacer, lo que es importante y lo que no. Hubo muchos desafíos relacionados con el estilo de codificación. Todavía escribo mucho código, y en aquellos días incluso tuve que reducir el ritmo porque hacía demasiadas tareas paralelas y las hacía mal, en lugar de centrarme en una sola. Mirando hacia atrás, escribí la mitad del código para el comando Java JIT, el comando C2. El siguiente codificador más rápido escribió la mitad de lento, el siguiente la mitad de lento, y fue una disminución exponencial. La séptima persona de esta fila era muy, muy lenta, ¡eso siempre sucede! Toqué mucho código. Miré quién escribió qué, sin excepción, miré su código, revisé cada uno de ellos y aun así seguí escribiendo más yo que cualquiera de ellos. Este enfoque no funciona muy bien con las personas. A algunas personas no les gusta esto. Y cuando no pueden soportarlo, comienzan todo tipo de quejas. Por ejemplo, una vez me dijeron que dejara de codificar porque estaba escribiendo demasiado código y estaba poniendo en peligro al equipo, y todo me sonó como una broma: amigo, si el resto del equipo desaparece y sigo escribiendo código, tú Sólo perderemos la mitad de los equipos. Por otro lado, si sigo escribiendo código y pierdes la mitad del equipo, suena como una muy mala gestión. Realmente nunca pensé en ello, nunca hablé de ello, pero todavía estaba en algún lugar de mi cabeza. El pensamiento daba vueltas en el fondo de mi mente: “¿Están todos bromeando?” Entonces, el mayor problema era yo y mis relaciones con la gente. Ahora me entiendo mucho mejor a mí mismo, fui líder de equipo de programadores durante mucho tiempo y ahora le digo directamente a la gente: ya sabes, soy quien soy y tendrás que lidiar conmigo, ¿está bien si me quedo? ¿aquí? Y cuando empezaron a solucionarlo, todo funcionó. De hecho, no soy ni malo ni bueno, no tengo malas intenciones ni aspiraciones egoístas, es solo mi esencia y necesito vivir con ella de alguna manera.

Andrés: Hace poco todo el mundo empezó a hablar de la autoconciencia de los introvertidos y de las habilidades interpersonales en general. ¿Qué puedes decir acerca de esto?

acantilado: Sí, esa fue la idea y la lección que aprendí al divorciarme de mi esposa. Lo que aprendí del divorcio fue entenderme a mí mismo. Así comencé a entender a otras personas. Comprenda cómo funciona esta interacción. Esto llevó a descubrimientos uno tras otro. Había una conciencia de quién soy y de lo que represento. Qué estoy haciendo: o estoy preocupado por la tarea, o estoy evitando el conflicto, o algo más, y este nivel de autoconciencia realmente me ayuda a mantener el control. Después de esto todo va mucho más fácil. Una cosa que descubrí no sólo en mí, sino también en otros programadores es la incapacidad de verbalizar pensamientos cuando estás bajo estrés emocional. Por ejemplo, estás sentado codificando, en un estado de fluidez, y luego ellos vienen corriendo hacia ti y comienzan a gritar histéricos que algo está roto y que ahora se tomarán medidas extremas contra ti. Y no puedes decir una palabra porque estás en un estado de estrés emocional. El conocimiento adquirido te permite prepararte para este momento, sobrevivirlo y pasar a un plan de retirada, tras el cual podrás hacer algo. Así que sí, cuando empiezas a darte cuenta de cómo funciona todo, es un gran acontecimiento que cambia tu vida. 
Yo mismo no pude encontrar las palabras adecuadas, pero recordé la secuencia de acciones. La cuestión es que esta reacción es tanto física como verbal y necesitas espacio. Ese espacio, en el sentido zen. Esto es exactamente lo que hay que explicar y luego hacerse a un lado inmediatamente, alejarse puramente físicamente. Cuando guardo silencio verbalmente, puedo procesar la situación emocionalmente. Cuando la adrenalina llega a tu cerebro, te pone en modo de lucha o huida, ya no puedes decir nada, no, ahora eres un idiota, un ingeniero de azotes, incapaz de dar una respuesta decente o incluso detener el ataque, y el atacante es libre. atacar una y otra vez. Primero debes volver a ser tú mismo, recuperar el control, salir del modo “luchar o huir”.

Y para ello necesitamos espacio verbal. Sólo espacio libre. Si dices algo, entonces puedes decir exactamente eso y luego ir y encontrar realmente un "espacio" para ti: salir a caminar por el parque, encerrarte en la ducha, no importa. Lo principal es desconectar temporalmente de esa situación. Tan pronto como te desconectas durante al menos unos segundos, recuperas el control y empiezas a pensar con seriedad. "Está bien, no soy una especie de idiota, no hago estupideces, soy una persona bastante útil". Una vez que hayas podido convencerte, es hora de pasar a la siguiente etapa: comprender lo sucedido. Fuiste atacado, el ataque vino de donde no lo esperabas, fue una emboscada deshonesta y vil. Esto es malo. El siguiente paso es comprender por qué el atacante necesitaba esto. ¿Realmente por qué? ¿Quizás porque él mismo está furioso? ¿Por qué está enojado? Por ejemplo, ¿porque se equivocó y no puede aceptar la responsabilidad? Esta es la manera de manejar cuidadosamente toda la situación. Pero esto requiere margen de maniobra, espacio verbal. El primer paso es romper el contacto verbal. Evite la discusión con palabras. Cancélalo, aléjate lo más rápido posible. Si se trata de una conversación telefónica, simplemente cuelgue; esta es una habilidad que aprendí comunicándome con mi ex esposa. Si la conversación no va bien, simplemente di "adiós" y cuelga. Del otro lado del teléfono: “bla, bla, bla”, respondes: “¡sí, adiós!” y cuelga. Simplemente terminas la conversación. Cinco minutos después, cuando recuperas la capacidad de pensar con sensatez, te has calmado un poco, es posible pensar en todo, lo que pasó y lo que pasará después. Y comience a formular una respuesta reflexiva, en lugar de simplemente reaccionar por emoción. Para mí, el gran avance en la autoconciencia fue precisamente el hecho de que en caso de estrés emocional no puedo hablar. Salir de este estado, pensar y planificar cómo responder y compensar los problemas: estos son los pasos correctos en caso de que no puedas hablar. La forma más sencilla es huir de la situación en la que se manifiesta el estrés emocional y simplemente dejar de participar en este estrés. Después de eso te vuelves capaz de pensar, cuando puedes pensar, te vuelves capaz de hablar, y así sucesivamente.

Por cierto, en el tribunal el abogado contrario intenta hacerle esto; ahora está claro por qué. Porque tiene la capacidad de reprimirte hasta tal punto que ni siquiera puedes pronunciar tu nombre, por ejemplo. En un sentido muy real, no podrás hablar. Si esto le sucede a usted, y si sabe que se encontrará en un lugar donde se libran batallas verbales, en un lugar como un tribunal, entonces puede acudir con su abogado. El abogado te defenderá y detendrá el ataque verbal, y lo hará de forma completamente legal, y el espacio Zen perdido volverá a ti. Por ejemplo, tuve que llamar a mi familia un par de veces, el juez fue bastante amigable al respecto, pero el abogado de la parte contraria me gritó y me gritó, ni siquiera pude pronunciar una palabra. En estos casos, utilizar un mediador funciona mejor para mí. El mediador detiene toda esta presión que cae sobre ti en un flujo continuo, encuentras el espacio Zen necesario y con él vuelve la capacidad de hablar. Este es todo un campo de conocimiento en el que hay mucho que estudiar, mucho que descubrir dentro de uno mismo, y todo esto se convierte en decisiones estratégicas de alto nivel que son diferentes para diferentes personas. Algunas personas no tienen los problemas descritos anteriormente; por lo general, las personas que son vendedores profesionales no los tienen. Todas estas personas que se ganan la vida con las palabras: cantantes famosos, poetas, líderes religiosos y políticos, siempre tienen algo que decir. Ellos no tienen esos problemas, pero yo sí.

Andrés: Fue... inesperado. Genial, ya hemos hablado mucho y es hora de terminar esta entrevista. Definitivamente nos reuniremos en la conferencia y podremos continuar este diálogo. ¡Nos vemos en Hidra!

Puede continuar su conversación con Cliff en la conferencia Hydra 2019, que se llevará a cabo del 11 al 12 de julio de 2019 en San Petersburgo. Él vendrá con un informe. "La experiencia de Memoria Transaccional Hardware Azul". Los boletos se pueden comprar en el sitio web oficial.

Fuente: habr.com

Añadir un comentario