Una gran entrevista con Cliff Click, el padre de la compilación JIT en Java

Una gran entrevista con Cliff Click, el padre de la compilación JIT en JavaCliff Click — CTO de Cratus (sensores IoT para mejorar procesos), fundador y cofundador de varias startups (incluyendo Rocket Realtime School, Neurensic y H2O.ai) con varios éxitos. Cliff escribió su primer compilador a los 15 años (Pascal para TRS Z-80). Es más conocido por su trabajo en C2 en Java (el IR Sea of Nodes). Este compilador mostró al mundo que JIT puede generar código de calidad, lo que fue uno de los factores que cimentó a Java como una de las principales plataformas de software moderno. Luego, Cliff ayudó a Azul Systems a construir un mainframe de 864 núcleos con software en Java puro, que soportaba pausas de GC en un montón de 500 gigabytes en alrededor de 10 milisegundos. En general, Cliff ha trabajado en todos los aspectos de la JVM.

 
Esta publicación en Habr es una gran entrevista con Cliff. Hablaremos sobre los siguientes temas:

  • Transición a optimizaciones de bajo nivel
  • Cómo realizar una gran refactorización
  • Modelo de costos
  • Aprendizaje de optimizaciones de bajo nivel
  • Ejemplos prácticos de mejora del rendimiento
  • Por qué crear su propio lenguaje de programación
  • Carrera como ingeniero de rendimiento
  • Desafíos técnicos
  • Un poco sobre la asignación de registros y la multiprocesadoridad
  • El mayor desafío en la vida

La entrevista es conducida por:

  • Andrei Satariin de Amazon Web Services. A lo largo de su carrera ha trabajado en proyectos muy diversos: probó una base de datos distribuida NewSQL en Yandex, un sistema de detección en la nube en el Laboratorio Kaspersky, un juego multijugador en Mail.ru y un servicio de cálculo de precios de divisas en Deutsche Bank. Le interesa la prueba de sistemas back-end a gran escala y distribuidos.
  • Vladimir Sitnikov de Netcracker. Ha trabajado durante diez años en el rendimiento y escalabilidad de NetCracker OS, un software utilizado por operadores de telecomunicaciones para automatizar procesos de gestión de red y equipo de red. Se interesa por el rendimiento de Java y Oracle Database. Es autor de más de una docena de mejoras en el rendimiento del controlador JDBC oficial de PostgreSQL.

Transición a optimizaciones de bajo nivel

Andrei: Usted es una persona conocida en el mundo de la compilación JIT, en Java y en el trabajo de rendimiento en general, ¿verdad? 

Cliff: ¡Así es!

Andrei: Comencemos con preguntas generales sobre el trabajo en el rendimiento. ¿Qué piensas sobre la elección entre optimizaciones de alto nivel y de bajo nivel, como trabajar al nivel de la CPU?

Cliff: Aquí es bastante simple. El código más rápido es aquel que nunca se ejecuta. Por eso, siempre se debe comenzar desde un alto nivel, trabajando en los algoritmos. Una mejor notación O superará a una peor notación O, a menos que intervengan constantes suficientemente grandes. Las cosas de bajo nivel suelen llegar al final. Normalmente, si has optimizado bien el resto de la pila y aún queda algo interesante, eso es lo que se considera bajo nivel. Pero, ¿cómo comenzar desde un alto nivel? ¿Cómo saber si se ha hecho suficiente trabajo en un alto nivel? Bueno… no hay fórmulas listas. Necesitas entender el problema, decidir qué planeas hacer (para no dar pasos innecesarios) y entonces puedes sacar un perfilador, que pueda decir algo útil. En algún momento, te das cuenta de que te has deshecho de las cosas innecesarias y es hora de hacer un ajuste fino a nivel bajo. Esto es, sin duda, un tipo especial de arte. Muchas personas hacen cosas innecesarias, pero se mueven tan rápido que no tienen tiempo para preocuparse por el rendimiento. Pero esto sigue así hasta que la cuestión se vuelve apremiante. Normalmente, el 99% del tiempo a nadie le interesa lo que estoy haciendo, hasta el momento en que en una ruta crítica surge algo importante que a alguien le importa. Y en ese momento, todos comienzan a cuestionarte sobre por qué no funcionó perfectamente desde el principio. En resumen, siempre hay algo que mejorar en el rendimiento. Pero el 99% del tiempo no tienes pistas. Simplemente intentas que algo funcione y en el proceso entiendes lo que es importante. Nunca se puede saber de antemano que este pedazo debe hacerse perfecto, por lo que, en esencia, tienes que ser perfecto en todo. Y eso no es posible y no lo haces. Siempre hay un montón de cosas que arreglar, y eso es completamente normal.

Cómo realizar una gran refactorización

Andrei: ¿Cómo trabajas en el rendimiento? Es un problema transversal. Por ejemplo, ¿has tenido que trabajar en problemas causados por la interacción de una gran cantidad de funcionalidades existentes?

Cliff: Intento evitar esto. Si sé que el rendimiento será un problema, lo pienso antes de comenzar a programar, especialmente sobre las estructuras de datos. Pero a menudo descubres todo esto mucho más tarde. Y entonces tienes que recurrir a medidas extremas y hacer lo que yo llamo «reescribir y dominar»: debes abarcar una parte lo suficientemente grande. Parte del código aún tendrá que ser reescrito por razones de rendimiento o por cualquier otra cosa. Cualquiera que sea la razón para reescribir el código, casi siempre es mejor reescribir un bloque mayor que uno menor. En ese momento, todos comienzan a temblar de miedo: “¡Oh Dios, no se puede tocar tanto código!”. Pero, de hecho, este enfoque casi siempre funciona mucho mejor. Debes enfrentar un gran problema de inmediato, dibujar un gran círculo alrededor de él y decir: todo lo que esté dentro del círculo, lo reescribiré. La frontera es mucho más pequeña que el contenido que está dentro de ella, que debe ser reemplazado. Y si definir esos límites permite que el trabajo interno sea perfecto, tienes las manos libres, haz lo que quieras. Una vez que entiendes el problema, el proceso de reescritura es mucho más fácil, así que ¡muérdete un gran trozo!
Al mismo tiempo, cuando haces una reescritura de gran tamaño y entiendes que el rendimiento será un problema, puedes empezar a preocuparte por ello de inmediato. Generalmente, esto se convierte en cosas simples como «no copies datos, gestiona los datos de la forma más simple posible, haz más pequeños». En grandes reescrituras, hay formas estándar de mejorar el rendimiento. Y casi siempre giran en torno a los datos.

Modelo de costos

Andrei: En uno de los podcasts mencionaste los modelos de costo en el contexto del rendimiento. ¿Puedes explicar qué querías decir con eso?

Cliff: Claro. Nací en una época en la que el rendimiento del procesador era extremadamente importante. Y esa era vuelve a aparecer: el destino no está exento de ironía. Comencé a vivir en tiempos de máquinas de ocho bits, mi primer computadora funcionaba con 256 bytes. Exactamente con bytes. Todo era muy pequeño. Tenías que contar instrucciones y, a medida que comenzamos a avanzar en el ámbito de los lenguajes de programación, estos se encargaban de más y más. Primero fue Ensamblador, luego Basic, luego C, y C asumió la responsabilidad de muchos detalles, como la asignación de registros y la selección de instrucciones. Pero allí todo era bastante claro y si hacía un puntero a una instancia de una variable, obtendría un load, y el costo de esa instrucción era conocido. El hardware entrega un número conocido de ciclos de máquina, así que la velocidad de ejecución de diferentes cosas se podía calcular simplemente sumando todas las instrucciones que planeabas ejecutar. Cada compare/test/branch/call/load/store se podía sumar y decir: aquí tienes el tiempo de ejecución. Al trabajar en la mejora del rendimiento, definitivamente notarás qué números corresponden a pequeños ciclos calientes. 
Pero tan pronto como cambias a Java, Python y cosas similares, rápidamente te alejas del hardware de bajo nivel. ¿Cuál es el costo de llamar a un getter en Java? Si el JIT en HotSpot lo hizo correctamente, lo habrá inlined, eso será un load, pero si no lo hizo, será una llamada a función. Dado que la llamada está en un ciclo caliente, cancelará todas las demás optimizaciones en ese ciclo. Por lo tanto, el costo real será mucho mayor. Y de inmediato pierdes la capacidad de mirar un fragmento de código y entender qué deberíamos ejecutar en términos de la frecuencia del procesador, memoria utilizada y caché. Todo esto se vuelve interesante solo si realmente te sumerges en el rendimiento.
Ahora nos encontramos en una situación en la que la velocidad de los procesadores no ha crecido casi nada en una década. ¡Los viejos tiempos han vuelto! Ya no puedes contar con un buen rendimiento de un solo hilo. Pero si de repente decides dedicarte a los cálculos paralelos, eso es increíblemente complicado; todos te miran como si fueras James Bond. Aceleraciones de diez veces suelen ocurrir en aquellos lugares donde alguien ha pasado por alto algo. La paralelización requiere mucho trabajo. Para obtener esa aceleración diez veces mayor, es necesario entender el modelo de costos. Qué cuesta y cuánto. Y para eso, necesitas comprender cómo el lenguaje se ajusta al hardware subyacente.
Martin Thompson encontró una excelente palabra para su blog Simpatía Mecánica! Es fundamental entender qué se propone hacer el hardware, cómo lo va a hacer y por qué hace lo que hace. Usando esto, es bastante simple empezar a contar instrucciones y averiguar dónde se pierde el tiempo de ejecución. Si no tienes la preparación adecuada, simplemente estás buscando un gato negro en una habitación oscura. Constantemente veo a personas optimizando el rendimiento que no tienen la menor idea de lo que están haciendo. Ellos sufren mucho y no avanzan realmente. Y cuando yo tomo ese mismo trozo de código, introduzco un par de pequeños hacks y obtengo una aceleración de cinco o diez veces, ellos dicen: bueno, eso no es justo, ya sabíamos que tú eras mejor. Asombroso. ¿De qué estaba hablando... el modelo de costos es sobre qué código estás escribiendo y cuán rápido funciona en promedio dentro del panorama general.

Andrei: ¿Y cómo se puede mantener tanto en la cabeza? ¿Se logra con mucha experiencia, o? ¿Dónde se adquiere esa experiencia?

Cliff: Bueno, adquirí mi experiencia no de la manera más sencilla. Programé en ensamblador en aquellos tiempos en que era posible comprender cada instrucción individual. Suena tonto, pero desde entonces tengo en mi cabeza, en mi memoria, un conjunto de instrucciones Z80 que perduran. No recuerdo los nombres de las personas un minuto después de conversar, pero recuerdo el código que escribí hace 40 años. Curiosamente, se parece al síndrome de "genio loco».

Aprendizaje de optimizaciones de bajo nivel

Andrei": ¿Hay alguna manera más sencilla de entrar en esto?

Cliff: Sí y no. El hardware que todos utilizamos no ha cambiado tanto en este tiempo. Todos utilizan x86, excepto los teléfonos inteligentes que usan Arm. Si no te dedicas a algún embebido hardcore, tienes lo mismo. Bien, continuemos. Las instrucciones tampoco han cambiado en siglos. Necesitas ir y escribir algo en ensamblador. Un poco, pero suficiente para empezar a entender. Ustedes se ríen, pero hablo muy en serio. Es necesario entender la relación entre el lenguaje y el hardware. Después de eso, debes ir y escribir un poco para crear un pequeño compilador para un lenguaje juguete. 'Juguete' significa que hay que hacerlo en un tiempo razonable. Puede ser súper simple, pero debe generar instrucciones. El acto de generación de instrucciones permitirá entender el modelo de costo para el puente entre el código de alto nivel, que todos escriben, y el código máquina, que se ejecuta en el hardware. Esta relación se grabará en la mente en el momento de escribir el compilador. Incluso un compilador muy simple. Después de eso, puedes empezar a ver Java y cómo su brecha semántica es mucho más profunda, haciendo que construir puentes sobre ella sea mucho más complicado. En Java es mucho más difícil entender si nuestro puente es bueno o malo, qué lo hará colapsar y qué no. Pero necesitas algún punto de partida cuando miras el código y entiendes: 'claro, este getter debe inlinearse cada vez'. Y luego resulta que a veces sí lo es, excepto cuando el método se vuelve demasiado grande, y el JIT comienza a inlinear todo. El rendimiento en esos casos se puede predecir de inmediato. Normalmente, los getters funcionan bien, pero después miras grandes bucles calientes y te das cuenta de que hay llamadas a funciones que no entiendes qué hacen. Esa es la problemática del uso generalizado de getters, la razón por la cual no se inlinean: no está claro si es un getter. Si tienes una base de código súper pequeña, simplemente puedes memorizarla y luego decir: este es un getter y este es un setter. En una gran base de código, cada función vive su propia historia, que en general no es conocida por nadie. El profiler dice que hemos perdido el 24% del tiempo en un ciclo y para entender qué hace ese ciclo, necesitas mirar cada función dentro. No es posible comprenderlo sin estudiar la función, lo cual ralentiza seriamente el proceso de entendimiento. Por eso no utilizo getters y setters, ¡he alcanzado un nuevo nivel!
¿De dónde obtener un modelo de costos? Bueno, se puede leer algo, claro... Pero creo que la mejor manera es actuar. Crear un pequeño compilador es la mejor forma de comprender el modelo de costos y asimilarlo en tu mente. Un pequeño compilador que serviría para programar un microondas es una tarea para principiantes. Quiero decir, si ya tienes habilidades de programación, deberías ser capaz de hacerlo. Todas esas cosas como analizar una cadena que será alguna expresión algebraica, extraer de allí las instrucciones de operaciones matemáticas en el orden correcto, obtener los valores correctos de los registros, todo eso se puede hacer sin problemas. Y mientras lo haces, se grabará en tu mente. Creo que todos saben lo que hace un compilador. Y esto ayudará a entender el modelo de costos.

Ejemplos prácticos de mejora del rendimiento

Andrei: ¿En qué más debes prestar atención al trabajar en el rendimiento?

Cliff: Estructuras de datos. Por cierto, hace tiempo que no daba esas clases... Rocket School. Fue divertido, pero requería dedicar tanto esfuerzo, ¡y también tengo vida! Bien. En una de las clases grandes e interesantes, "¿A dónde va tu rendimiento?", di a los estudiantes un ejemplo: se leían dos terabytes y medio de datos fintech desde un archivo CSV y luego había que contar la cantidad de productos vendidos. Datos de mercado de tick habituales. Paquetes UDP convertidos a formato de texto desde los años 70. Chicago Mercantile Exchange, cosas como petróleo, maíz, soja y similares. Tenías que contar estos productos, la cantidad de transacciones, el volumen promedio de movimiento de fondos y mercancías, etc. Es matemáticas comerciales bastante simples: encontrar el código del producto (que son 1-2 caracteres en una tabla hash), obtener la suma, agregarla a uno de los conjuntos de transacciones, añadir el volumen, añadir el costo, y un par de cosas más. Matemáticas muy simples. La implementación a nivel juguete fue muy directa: todo está en un archivo, leo el archivo y avanzo en él, separando los registros individuales en cadenas de Java, buscando las cosas necesarias y sumando según la matemática descrita anteriormente. Y esto funciona a una velocidad pequeña.

Con este enfoque, es obvio qué está sucediendo, y los cálculos paralelos no ayudarán en esto, ¿verdad? Resulta que se puede lograr un aumento de rendimiento cinco veces solo eligiendo las estructuras de datos adecuadas. ¡Esto sorprende incluso a los programadores experimentados! En mi caso específico, el enfoque estaba en no hacer asignaciones de memoria en el ciclo caliente. Bueno, esa no es toda la verdad, pero en general, no deberías asignar "una vez cada X", cuando X es lo suficientemente grande. Cuando X son dos mil quinientos megabytes, no deberías asignar nada "una vez por letra", o "una vez por línea", o "una vez por campo", nada por el estilo. Eso es lo que realmente toma tiempo. ¿Cómo funciona esto? Imagina que estoy haciendo una llamada String.split() o BufferedReader.readLine(). Readline que convierte un conjunto de bytes que llegan por la red en una cadena, una vez por cada línea, para cada una de las cientos de millones de líneas. Tomo esta línea, la analizo y la descarto. ¿Por qué la descarto? Bueno, ya la he procesado, eso es todo. Entonces, para cada byte leído de estos 2.7G, se escriben dos caracteres en la cadena, es decir, ya son 5.4G, y no los necesito más, así que se descartan. Si miras el ancho de banda de la memoria, cargamos 2.7G que pasan a través de la memoria y el bus de memoria en el procesador, y después se envían el doble en la cadena que está en la memoria, y todo esto se desgasta al crear cada nueva cadena. Pero necesito leerla, el hardware la lee, incluso si luego todo será desgastado. Y debo escribirla porque he creado una cadena y las cachés se han saturado: la caché no puede contener 2.7G. Por lo tanto, para cada byte leído, leo otros dos bytes adicionales y escribo otros dos bytes adicionales, y al final tienen una relación de 4:1: así de derrochadamente estamos utilizando el ancho de banda de la memoria. Y luego resulta que si hago String.split() – lo hago muy lejos de ser la última vez, porque puede haber otros 6-7 campos dentro. Por lo tanto, el código clásico para leer CSV con el posterior análisis de líneas conduce a pérdidas de ancho de banda de memoria en un rango de 14:1 en comparación con lo que realmente desearías tener. Si se eliminan estas asignaciones, se puede lograr una aceleración de cinco veces.

Y no es que sea muy complicado. Si miras el código desde el ángulo correcto, todo se vuelve bastante simple, en cuanto comprendes la esencia del problema. No deberías dejar de asignar memoria en absoluto: el problema es que asignas algo y se agota de inmediato, y por 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 del rendimiento. En x86, normalmente necesitas quemar ciclos de CPU activamente, y aquí has quemado toda la memoria mucho antes. La solución es reducir la cantidad de asignaciones. 
Otra parte del problema es que, si ejecutas el perfilador cuando se ha agotado el ancho de banda de la memoria, justo en el momento en que esto ocurre, generalmente esperas que el caché regrese, porque está lleno de basura que acabas de generar, con todas esas cadenas. Por lo tanto, cada operación de carga o almacenamiento se vuelve lenta, ya que conducen a fallos en el caché: todo el caché se ha vuelto lento, esperando a que se libere la basura. Entonces, el perfilador mostrará únicamente un ruido aleatorio tibio, disperso a lo largo de todo el ciclo; no habrá ninguna instrucción o lugar caliente en el código. Solo ruido. Y si miras los ciclos de GC, todos estarán en Young Generation y serán súper rápidos: microsegundos o milisegundos como máximo. Porque toda esta memoria muere instantáneamente. Asignas miles de millones de gigabytes, y él los corta, y corta, y vuelve a cortar. Todo esto sucede muy rápido. Resulta que hay ciclos de GC baratos, ruido tibio a lo largo de todo el ciclo, pero queremos obtener una aceleración de 5 veces. En este momento, algo debería 

El problema radica en la estructura de los datos: una estructura desnuda que subyace a todo lo que ocurre, es demasiado grande, son 2.7G en el disco, por lo que hacer una copia de esta cosa es muy indeseable; se desea cargarla directamente desde el búfer de bytes de la red a los registros, para no leer y escribir en la cadena de ida y vuelta cinco veces. Desafortunadamente, Java por defecto no ofrece una biblioteca así en el JDK. Pero, en realidad, es trivial, ¿verdad? En esencia, son de 5 a 10 líneas de código que servirán para implementar tu propio cargador de cadenas con búfer, que replica el comportamiento de la clase String, siendo a la vez un envoltorio alrededor del búfer de bytes subyacente. Como resultado, parece que trabajas casi como si estuvieras manipulando cadenas, pero en realidad se están moviendo punteros en el búfer, y los bytes en bruto no se copian a ningún lado, reutilizándose así los mismos búferes una y otra vez, mientras que el sistema operativo está feliz de encargarse de las cosas para las que está diseñado, como la doble amortiguación oculta de esos búferes de bytes, y tú ya no procesas un flujo interminable de datos innecesarios. Por cierto, ¿entienden 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, nada de esto puede estar en la caché, y luego sucede un fallo garantizado al 100%. Cuando trabajas con un puntero, en x86 leer un registro de la memoria toma de 1 a 2 ciclos, y tan pronto como esto sucede, pagas, pagas, pagas, porque toda la memoria está en NINE caches – y esto es el costo de la asignación de memoria. El verdadero costo.

En otras palabras, las estructuras de datos son lo más difícil de cambiar. Y una vez que te das cuenta de que elegiste la estructura de datos incorrecta, la cual afectará el rendimiento más adelante, generalmente se requiere un trabajo considerable para corregirlo. Si no lo haces, las cosas empeorarán. Primero que todo, debes pensar en las estructuras de datos, es importante. El costo principal recae en las estructuras de datos pesadas, que comienzan a usarse con la mentalidad de “copié la estructura de datos X en la estructura de datos Y, porque Y me gusta más visualmente”. Pero la operación de copiar (que parece barata) en realidad consume memoria y ahí es donde se pierde todo el tiempo de ejecución. Si tengo una cadena gigantesca con JSON y quiero convertirla en un árbol DOM estructurado de POJO o algo similar, la operación de análisis de esa cadena y la construcción del POJO, y luego la nueva referencia al POJO en el futuro, tendrán un costo adicional – no es barato. A menos que estés accediendo a POJO con mucha más frecuencia que a la cadena. A simple vista, podrías intentar decodificar la cadena y extraer solo lo necesario, sin convertirlo en POJO. Si todo esto ocurre en una ruta que requiere el máximo rendimiento, nada de POJO, se necesita profundizar directamente en la cadena.

Por qué crear su propio lenguaje de programación

Andrei: Dijiste que para entender el modelo de costos, necesitas escribir tu propio pequeño lenguaje...

Cliff: No un lenguaje, sino un compilador. Un lenguaje y un compilador son cosas diferentes. La principal diferencia está en tu mente. 

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

Cliff: ¡Porque puedo! Estoy semi-jubilado, así que esto es mi hobby. He pasado toda mi vida realizando los lenguajes de otras personas. Además, he trabajado mucho en el estilo de codificación. También porque veo problemas en otros lenguajes. Veo que hay mejores maneras de hacer las cosas habituales. Y me gustaría aprovecharlas. Estoy cansado de ver problemas en mí mismo, en Java, en Python, en cualquier otro lenguaje. Ahora estoy escribiendo en React Native, JavaScript y Elm como un hobby que no se trata de la jubilación, sino de un trabajo activo. También escribo en Python y, muy probablemente, seguiré trabajando en el aprendizaje automático para los backends en Java. Hay muchos lenguajes populares y todos tienen características interesantes. Cada uno es bueno en algo y se pueden intentar combinar todas esas características. Así que me estoy dedicando a estudiar cosas que me interesan, el comportamiento del lenguaje, intentando idear una semántica razonable. ¡Y hasta ahora me va bien! 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 fuerte y semántica de memoria para cargas y almacenes. Al mismo tiempo, tener inferencia de tipos automática como en Haskell. Aquí estoy intentando mezclar la inferencia de tipos al estilo de Haskell con una memoria que funcione como en C y Java. En eso he estado trabajando los últimos 2-3 meses, por ejemplo.

Andrei: Si estás construyendo un lenguaje que toma lo mejor de otros lenguajes, ¿has considerado que alguien podría hacer lo contrario: tomar tus ideas y usarlas en su propio lenguaje?

Cliff: Así es como surgen los nuevos lenguajes. ¿Por qué Java se parece a C? Porque C tenía una buena sintaxis que todos entendían y Java se inspiró en esa sintaxis, añadiendo seguridad de tipos, verificaciones de límites de arreglos, GC, y además mejoraron algunas cosas de C. Agregaron las suyas. Pero se inspiraron bastante fuerte, ¿verdad? Todos se apoyan en los hombros de gigantes que estuvieron antes que tú: así es como se hace el progreso.

Andrei: Según entiendo, tu lenguaje será seguro en cuanto al uso de memoria. ¿Has considerado implementar algo como el verificador de préstamos de Rust? ¿Lo has visto, qué opinas de él?

Cliff: Bueno, he estado programando en C durante una eternidad, lidiando con todos esos malloc y free, y gestionando manualmente el ciclo de vida. Saben, el 90-95% del tiempo de vida gestionado manualmente tiene una estructura similar. Y es muy, muy doloroso manejarlo de esta manera. Me gustaría que el compilador simplemente me dijera qué está sucediendo y qué logré con mis acciones. Para algunas cosas, el verificador de préstamos lo hace automáticamente. Además, debería proporcionar automáticamente información, entender todo y no sobrecargarme con la necesidad de expresar ese entendimiento. Al menos debería realizar un análisis de escape local, y solo si no puede lograrlo, entonces debería agregar anotaciones de tipo que describan el ciclo de vida; este esquema es mucho más complicado que el verificador de préstamos, o cualquier verificador de memoria existente. La elección entre 'todo está bien' y 'no entendí nada' — no, debería haber algo mejor. 
Así que, como alguien que ha escrito mucho código en C, considero que tener soporte para la gestión automática del ciclo de vida es fundamental. Además, me ha frustrado mucho lo que Java usa de memoria y la principal queja es el GC. Al asignar memoria en Java, no recuperas la memoria que fue local en el último ciclo de GC. En lenguajes con gestión de memoria más precisa, esto no sucede. Si llamas a malloc, obtienes la memoria que generalmente acaba de ser utilizada. Normalmente, haces algunas cosas temporales con la memoria y la devuelves de inmediato. Y se devuelve de inmediato al grupo de malloc, y el siguiente ciclo de malloc la vuelve a extraer. Por lo tanto, el uso real de la memoria se reduce al conjunto de objetos vivos en un momento específico, más las fugas. Y si no se filtra de manera completamente inapropiada, la mayor parte de la memoria se asienta en cachés y en el procesador, y esto funciona rápido. Pero requiere mucha gestión manual de la memoria con malloc y free, llamados en el orden y lugar correctos. Rust puede manejar esto correctamente por sí mismo y en muchos casos puede incluso ofrecer un rendimiento mayor, ya que el consumo de memoria se reduce solo a los cálculos actuales, en lugar de esperar el siguiente ciclo de GC que liberará la memoria. Al final, tenemos una forma muy interesante de mejorar el rendimiento. Y bastante poderosa; de hecho, he estado trabajando en cosas así en el procesamiento de datos para fintech, y esto ha permitido obtener una aceleración de aproximadamente cinco veces. Esto es una aceleración considerable, especialmente en un mundo donde los procesadores no se vuelven más rápidos, pero seguimos esperando mejoras.

Carrera como ingeniero de rendimiento

Andrei: También me gustaría hacer algunas preguntas sobre la carrera en general. Te hiciste famoso gracias a tu trabajo en JIT en HotSpot, y luego te trasladaste a Azul, que también es una empresa de JVM. Pero te ocupaste más de hardware que de software. Y luego, de repente, te cambiaste a Big Data y Machine Learning, y luego a la detección de fraudes. ¿Cómo sucedió esto? Son áreas de desarrollo muy diferentes.

Cliff: He estado programando durante bastante tiempo y he tenido experiencias muy diversas. Y cuando la gente dice: «Oh, ¡tú eres el que hizo JIT para Java!», siempre me parece gracioso. Antes de eso, trabajé en un clon de PostScript, el lenguaje que Apple usó alguna vez para sus impresoras láser. Antes de eso, implementé el lenguaje Forth. Creo que el tema común para mí es el desarrollo de herramientas. He estado creando herramientas toda mi vida, las cuales permiten a otros escribir sus programas geniales. Pero también he trabajado en el desarrollo de sistemas operativos, controladores, depuradores a nivel de kernel, lenguajes para desarrollar sistemas operativos que comenzaban de manera trivial, pero que con el tiempo se volvían cada vez más complejos. Sin embargo, el tema principal sigue siendo el desarrollo de herramientas. Gran parte de mi vida transcurrió entre Azul y Sun, y estuvo relacionada con Java. Pero cuando empecé con Big Data y Machine Learning, volví a ponerme mi sombrero de gala y dije: «Oh, ahora tenemos un problema no trivial, y aquí está ocurriendo una gran cantidad de cosas interesantes y personas haciendo algo». Es un camino excelente para el desarrollo que vale la pena recorrer.

Sí, me encantan los cálculos distribuidos. Mi primer trabajo fue durante la universidad en C, en un proyecto publicitario. Era un trabajo de cálculos distribuidos en chips Zilog Z80, que recolectaban datos para el reconocimiento óptico analógico de textos, realizado por un verdadero analizador analógico. Era un tema increíble y completamente extraño. Pero había problemas; algunas partes no se reconocían correctamente, así que había que obtener la imagen y mostrarla a una persona que la hubiera leído y que informara sobre lo que decía, y así había trabajos con datos, y esos trabajos tenían su propio lenguaje. Había un backend que procesaba todo esto: Z80 trabajando en paralelo con terminales vt100 en uso, uno por persona, y había un modelo de programación paralela en Z80. Un cierto bloque de memoria común que compartían todos los Z80 dentro de una configuración en estrella; se compartía tanto el backplane como la mitad de la RAM dentro de la red, mientras que la otra mitad era privada o se usaba para otra cosa. Un sistema paralelo distribuido complejo de manera significativa con memoria compartida... semi-compartida. ¿Cuándo fue esto? Ya no lo recuerdo, por ahí en la mitad de los años 80. Hace bastante tiempo. 
Sí, digamos que 30 años es bastante tiempo. Las tareas relacionadas con los cálculos distribuidos existen desde hace bastante tiempo, la gente ha estado luchando con Beowulf-clústeres. Estos clústeres se ven como… Por ejemplo: hay Ethernet y tu rápido x86 está conectado a este Ethernet, y ahora quieres obtener memoria compartida simulada, porque en ese momento nadie podía dedicarse a la codificación de computación distribuida, era demasiado complicado y por eso había memoria compartida simulada con protección de páginas de memoria en x86, y si escribías en esa página, le decíamos a los otros procesadores que si accedían a la misma memoria compartida, debían cargarla de ti, y así surgió algo similar a un protocolo de soporte de coherencia de cachés y software para ello. Una idea interesante. El verdadero problema, por supuesto, estaba en otro lugar. Todo esto funcionaba, pero rápidamente te encontrabas con problemas de rendimiento, ya que nadie entendía el modelo de rendimiento a un nivel suficientemente bueno – cuáles eran los patrones de acceso a la memoria, cómo hacer para que los nodos no se pingen eternamente entre sí, y así sucesivamente.

En H2O, se me ocurrió lo siguiente: los propios desarrolladores son responsables de determinar dónde se encuentra el paralelismo y dónde no. He ideado un modelo de codificación que hace que escribir código de alto rendimiento sea fácil y simple. Sin embargo, escribir código que funcione lentamente es complicado; se verá mal. Se necesita un gran esfuerzo para escribir código lento, tendrá que usarse métodos no estándar. El código que causa retrasos se ve de inmediato. Como resultado, generalmente se escribe código que funciona rápido, pero luego hay que lidiar con lo que hacer en caso de memoria compartida. Todo esto está ligado a grandes arreglos y el comportamiento allí es similar al de los grandes arreglos no volátiles en Java paralelo. En otras palabras, imagina que dos hilos escriben en un arreglo paralelo, uno de ellos gana y el otro, por consiguiente, pierde, y no sabes quién es quién. Si no son volátiles, el orden puede ser cualquier cosa, y realmente funciona bien. A la gente realmente le importa el orden de las operaciones; colocan correctamente los volátiles y en los lugares adecuados anticipan problemas de rendimiento relacionados con la memoria. De lo contrario, simplemente escribirían el código en forma de bucles del 1 al N, donde N son algunos billones, con la esperanza de que todos los casos complejos se vuelvan automáticamente paralelos, y eso no funciona. Pero en H2O, no es Java ni Scala; se podría considerar como «Java menos menos», si se quiere. Es un estilo de programación muy comprensible y se asemeja a escribir código simple en C o Java con bucles y arreglos. Pero al mismo tiempo, se puede manejar memoria en terabytes. Todavía utilizo H2O. De vez en cuando lo empleo en diferentes proyectos, y sigue siendo la cosa más rápida, superando a sus competidores por decenas de veces. Si estás trabajando con Big Data con datos en columnas, es muy difícil superar a H2O.

Desafíos técnicos

Andrei: ¿Cuál ha sido el mayor desafío en toda su carrera?

Cliff: ¿Estamos discutiendo la parte técnica o la no técnica de la cuestión? Yo diría que los mayores desafíos son los no técnicos. 
En cuanto a los desafíos técnicos, simplemente los superé. Ni siquiera sé cuál fue el más grande, pero hubo varios bastante interesantes que tomaron mucho tiempo y lucha mental. Cuando ingresé a Sun, estaba seguro de que haría un compilador rápido, mientras que un montón de personas mayores me decían que nunca lo lograría. Pero seguí por ese camino, escribí un compilador hasta el asignador de registros, y fue bastante rápido. Era tan rápido como el moderno C1, pero en ese entonces el asignador era mucho más lento, y mirando hacia atrás, esa fue una gran problema de la estructura de datos. La necesitaba para escribir un asignador gráfico de registros y no entendía la 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 a menudo superaba el tamaño de la caché en los x86 de ese tiempo, así que, si inicialmente supuse que el asignador de registros ocuparía el 5-10% de todo el tiempo de compilación JIT, en realidad resultó ser el 50%.

Con el tiempo, el compilador se volvió cada vez más claro y eficiente, dejó de generar código horrible en más casos, y el rendimiento se asemejó más a lo que produce un compilador de C. A menos que, por supuesto, estés escribiendo algo tan malo que ni siquiera C puede acelerar. Si escribes código como en C, obtendrás un rendimiento similar al de C en más casos. Y a medida que pasaba el tiempo, más frecuente es que el código coincidiera asintóticamente con el nivel de C, el asignador de registros comenzó a parecerse a algo terminado... independientemente de si tu código se ejecuta rápido o lento. Continué trabajando en el asignador para que hiciera mejores asignaciones. Se volvía más y más lento, pero proporcionaba un rendimiento cada vez mejor en aquellos casos donde nadie más podía. Podía sumergirme en el asignador de registros, dedicar un mes de trabajo, y de repente todo el código comenzaba a ejecutarse un 5% más rápido. Esto sucedía una y otra vez y el asignador de registros se convirtió en una especie de obra de arte: todos lo amaban o lo odiaban, y las personas de la academia hacían preguntas sobre por qué todo se hacía de esa manera, por qué no búsqueda lineal, y cuál es la diferencia. La respuesta sigue siendo la misma: un asignador basado en el grafo de coloración más un manejo muy cuidadoso del código de búfer equivale a un arma de victoria, la mejor combinación que nadie puede vencer. Y esto es algo bastante poco obvio. Todo lo demás que hace el compilador son cosas bastante estudiadas, aunque también han sido llevadas a un nivel artístico. Siempre he hecho cosas que deberían convertir el compilador en una obra de arte. Pero nada de esto fue algo extraordinario, excepto el asignador de registros. La clave es que hay que manejarlo con cuidado. recortar bajo carga y, si esto ocurre (puedo explicar con más detalle si es interesante), significa que se puede hacer inlining de manera más agresiva, sin el riesgo de superar el quiebre en el gráfico de rendimiento. En aquellos tiempos había un montón de compiladores completos, adornados con chucherías y bocinas, que tenían asignadores de registros, pero nadie pudo hacerlo así nuevamente.

El problema es que, si agregas métodos que deben ser in-lined, aumentando cada vez más el área de inlining, el conjunto de valores utilizados supera instantáneamente la cantidad de registros, y es necesario hacer un spill. El nivel crítico generalmente ocurre cuando el allocador se rinde, y un buen candidato para el spill sustituye a otro, lo que lleva a unos spills bastante extraños. El valor del inlining aquí es que pierdes parte de la sobrecarga, la sobrecarga de la llamada y el almacenamiento; puedes ver los valores internamente y puedes seguir optimizándolos. El costo del inlining radica en que se genera una gran cantidad de valores vivos, y si tu allocador de registros hace spill de más de lo necesario, pierdes inmediatamente. Por eso, la mayoría de los allocadores tienen el problema de que, cuando el inlining sobrepasa un cierto límite, todo empieza a hacer spill y el rendimiento puede irse por el desagüe. Aquellos que implementan compiladores añaden algunas heurísticas: por ejemplo, para detener el inlining cuando llega a un tamaño suficientemente grande, ya que las asignaciones arruinarían todo. Así se forma una quiebra en el gráfico de rendimiento: inlines, inlines, el rendimiento incrementa lentamente, y luego ¡bam! – cae en picada debido a que has inlined demasiado. Así funcionó todo hasta la llegada de Java. Java requiere mucho más inlining, por lo que tuve que hacer que mi allocador fuera mucho más agresivo, para que se estabilizara y no cayera, y si inlines demasiado, comienza a hacer spill, pero luego llega el momento de "no más spills". Esta es una observación interesante y me llegó de la nada, no era obvio, pero valió la pena. Me embarqué en un inlining agresivo y eso me llevó a lugares donde el rendimiento de Java y C van de la mano. Realmente son cercanos: puedo escribir código en Java que es significativamente más rápido que el código en C y cosas por el estilo, pero en términos generales, en el gran esquema de las cosas, son comparables. Creo que parte de este crédito se debe al allocador de registros, que me permite inlining de manera muy tonta. Simplemente inlineo todo lo que veo. La cuestión aquí es si el allocador funciona bien, si como resultado se obtiene un código razonablemente eficiente. Ese fue un gran desafío: entender todo esto y hacer que funcionara.

Un poco sobre la asignación de registros y la multiprocesadoridad

Vladimir: Problemas como la asignación de registros parecen ser un tema eterno e interminable. Es interesante, ¿hubo alguna vez una idea que parecía prometedora y luego fracasó en la práctica?

Cliff: ¡Por supuesto! La asignación de registros es un área en la que, para resolver un problema NP-completo, intentas encontrar algunas heurísticas. Y nunca podrás lograr una solución perfecta, ¿verdad? Simplemente es imposible. Mira, la compilación Ahead of Time también funciona mal. Aquí se habla de algunos casos promedio. De un rendimiento típico, así que puedes ir y medir algo que consideras un buen rendimiento típico; al final, ¡estás trabajando en mejorar eso! La asignación de registros es un tema completamente dedicado al rendimiento. Una vez que tienes el primer prototipo, que funciona y hace lo que necesita, comienza el trabajo sobre el rendimiento. Hay que aprender a medir bien. ¿Por qué es importante? Si tienes datos claros, puedes ver diferentes partes y notar: ah, esto ayudó aquí, ¡pero allí todo falló! Surgen buenas ideas, agregas una nueva heurística y de repente todo empieza a funcionar un poco mejor en promedio. O no lo hace. Tuve un montón de casos en los que luchamos por cinco por ciento de rendimiento que diferenciaban nuestro desarrollo del asignador anterior. Y cada vez se ve así: en algún lugar gané, en algún lugar perdí. Si tienes buenas herramientas de análisis de rendimiento, puedes encontrar ideas perdedoras y entender por qué están fallando. Quizás debas dejar todo como está, o tal vez tomarte más en serio la afinación, o ir y arreglar algo más. ¡Es toda una serie de cosas! Hice este genial truco, pero necesito también esto, y esto, y esto; y la combinación total da algunas mejoras. A veces, las soluciones aisladas pueden fracasar. Esa es la naturaleza de trabajar en problemas de rendimiento NP-completos.

Vladimir: Da la impresión de que cosas como la pintura en los asignadores es una tarea ya resuelta. Bueno, para ustedes parece resuelta, según lo que cuentan, entonces, ¿vale la pena…?

Cliff: No está resuelta como tal. Eres tú quien debe convertirla en 'resuelta'. Hay tareas difíciles y hay que resolverlas. Una vez hecho esto, llega el momento de trabajar en el rendimiento. Hay que tratar este trabajo de manera apropiada: hacer benchmarks, recopilar métricas, explicar situaciones en las que al regresar a una versión anterior tu viejo truco volvió a funcionar (o viceversa, dejó de funcionar). Y no retroceder hasta que consigas algo. Como ya he mencionado, si hay grandes ideas que no funcionaron, en el ámbito de la asignación de registros las ideas son prácticamente infinitas. Por ejemplo, se pueden leer publicaciones científicas. Aunque ahora este campo se ha movido mucho más lento y es más claro que en su juventud. Sin embargo, en este campo trabajan una infinidad de personas y vale la pena probar todas sus ideas; todas están esperando su momento. Y no puedes decir cuán buenas son si no las pruebas. Cuán bien se integran con todo lo demás en tu asignador, ya que el asignador realiza múltiples tareas y algunas ideas en tu asignador específico pueden no funcionar, mientras que en otro asignador sí lo harían. La principal forma de triunfar para un asignador es sacar la parte lenta fuera del camino principal y forzar la división en los límites de los caminos lentos. Por lo tanto, si deseas iniciar GC, ir por el camino lento, desoptimizar, lanzar una excepción, todo en ese sentido, sabes que esas cosas son relativamente raras. Y realmente son raras, lo he comprobado. Haces trabajo adicional y gracias a esto desaparecen muchas restricciones en esos caminos lentos, pero no es muy importante porque son lentos y rara vez se utilizan. Por ejemplo, un puntero nulo: nunca ocurre, ¿verdad? Necesitas tener varios caminos para diferentes cosas, pero no deben interferir en el principal. 

Vladimir: ¿Qué opinas sobre la multicore, cuando hay miles de núcleos? ¿Es algo útil?

Cliff: ¡El éxito de las GPU demuestra que sí, es bastante útil!

Vladimir: Son bastante especializadas. ¿Y qué hay de los procesadores de propósito general?

Cliff: Bueno, este era el modelo de negocio de Azul. La respuesta llegó en una era en la que a la gente le encantaba la previsibilidad en el rendimiento. En aquel entonces, era un poco complicado escribir código paralelo. El modelo de codificación H2O se escala bien, pero no es un modelo de uso general. Es solo un poco más general que el uso de GPU. ¿Estamos hablando de la complejidad de desarrollar algo así o de la complejidad de su uso? Por ejemplo, una lección interesante que me enseñó Azul, bastante no obvia: las cachés pequeñas están bien. 

El mayor desafío en la vida

Vladimir: ¿Qué hay de los desafíos no técnicos?

Cliff: El mayor desafío fue no ser… amable y bueno con la gente. Como consecuencia, me encontraba constantemente en situaciones de conflicto extremo. Situaciones en las que sabía que todo iba mal, pero no sabía cómo avanzar en la solución de esos problemas y no pude manejarlos. Muchas de las dificultades a largo plazo, que duraron décadas, surgieron de esta manera. El hecho de que en Java existen compiladores C1 y C2 es una consecuencia directa de esto. También es una consecuencia directa que en Java no hubo compilación multinivel durante diez años. Es evidente que necesitábamos un sistema así, pero no está claro por qué no existía. Tuve problemas con un ingeniero… o un grupo de ingenieros. Hace mucho tiempo, cuando comencé a trabajar en Sun, yo era… Bueno, no solo en ese momento, siempre he tenido mi propia opinión sobre todo. Y consideraba que era verdad que podía simplemente presentar mi verdad de forma directa. Sobre todo porque, sorprendentemente, tenía razón la mayor parte del tiempo. Y si a alguien no le gusta este enfoque… especialmente si está claramente equivocado y hace tonterías… En general, pocas personas podrían soportar este tipo de comunicación con tolerancia. Aunque algunas sí podrían, como yo. He basado toda mi vida en principios meritocráticos. Si me muestras que algo está mal, me daré la vuelta y diré: dijiste tonterías. Al mismo tiempo, por supuesto, me disculpo y cosas así, reconozco los méritos, si los hay, y tomo otras acciones correctas. Por otro lado, tengo razón sorprendentemente en un porcentaje sorprendentemente alto de tiempo. Y eso no funciona muy bien en las relaciones con las personas. No intento ser amable, sino que planteo las preguntas directamente. "Esto nunca funcionará, porque uno, dos y tres". Y ellos responden: "¡Oh!". Hubo otras consecuencias, que probablemente es mejor omitir: por ejemplo, las que llevaron a mi divorcio y a una década de depresión después de eso.

El desafío es luchar contra las personas y su percepción de lo que puedes o no puedes hacer, de lo que es importante y lo que no. Ha habido muchos desafíos sobre el estilo de codificación. Todavía escribo mucho código, y en esos tiempos tuve que incluso desacelerarme porque estaba haciendo demasiadas tareas en paralelo y las estaba haciendo mal, en lugar de enfocarme en una sola. Mirando hacia atrás, escribí la mitad del código del equipo de Java JIT, del equipo C2. El siguiente programador más rápido escribía la mitad de lento, el siguiente, aún la mitad más lento, y eso fue una caída exponencial. La séptima persona en esta fila era muy, muy lenta; ¡eso siempre sucede! Toqué un montón de código. Observé lo que cada uno escribía, sin excepciones, miré su código, revisé a cada uno de ellos, y aún continué escribiendo más que cualquiera de ellos. Este enfoque no funciona muy bien con las personas. A algunos no les gusta. Y cuando no pueden manejarlo, comienzan a surgir todo tipo de quejas. Por ejemplo, una vez me dijeron que dejara de escribir código porque escribo demasiado código, y eso pone en peligro al equipo; para mí, todo eso sonaba a broma: amigo, si todo el resto del equipo desaparece y yo sigo escribiendo código, solo perderás la mitad del equipo. Por otro lado, si continúo escribiendo código y tú pierdes la mitad del equipo, eso suena a una muy mala gestión. Nunca realmente pensé en eso, nunca lo mencioné, pero aún así estaba en algún lugar de mi cabeza. En el fondo de mi conciencia, la idea giraba: '¿Están todos de broma?'. Así que, el mayor problema era yo y mis relaciones con las personas. Ahora me entiendo mucho mejor; fui líder de equipo con programadores por un tiempo, y ahora les digo directamente a las personas: sabes, soy como soy, y tendrán que lidiar conmigo; ¿está bien si me quedo aquí? Y cuando empezaron a manejarlo, todo funcionó. En realidad, no soy ni bueno ni malo, no tengo malas intenciones ni aspiraciones egoístas, simplemente es mi esencia, y hay que vivir con eso.

Andrei: Recientemente, todos han comenzado a hablar sobre la autoconciencia para introvertidos y, en general, sobre las habilidades blandas. ¿Qué se puede decir al respecto?

Cliff: Sí, fue una comprensión y una lección que saqué de mi divorcio con mi esposa. Lo que aprendí de la separación fue el entendimiento de mí mismo. Así comencé a entender a los demás. Comprender cómo funciona esa interacción. Esto condujo a descubrimientos uno tras otro. Surgió la conciencia de quién soy y qué represento. Lo que hago: o estoy preocupado por la tarea, evito el conflicto, o algo más; y ese nivel de autoconciencia realmente ayuda a mantenerme en control. Después de esto, todo se vuelve mucho más fácil. Una cosa que descubrí, no solo en mí mismo, sino también en otros programadores, es la incapacidad de verbalizar pensamientos cuando estás bajo estrés emocional. Por ejemplo, estás codificando, en un estado de flujo, y de repente vienen corriendo y gritando histéricamente que algo se ha roto, y que se tomarán medidas drásticas. Y no puedes decir una palabra, porque estás en un estado de estrés emocional. Los conocimientos adquiridos permiten prepararse para ese momento, vivirlo y pasar al plan de escape, después del cual ya puedes hacer algo. Así que sí, cuando comienzas a darte cuenta de cómo funciona todo esto, es un gran acontecimiento que cambia la vida. 
No pude encontrar las palabras correctas, pero memoricé la secuencia de acciones. La esencia es que esta reacción es tan física como verbal, y necesitas espacio. Un espacio así, en el sentido zen. Eso es lo que hay que explicar, y luego apartarse enseguida, físicamente. Cuando estoy en silencio verbalmente, puedo procesar la situación en términos de emociones. A medida que la adrenalina llega al cerebro y te coloca en el modo de 'pegar o huir', ya no puedes hablar; no, ahora eres un idiota, un ingeniero para golpear, incapaz de dar una respuesta digna o al menos detener el ataque, y el atacante puede volver a atacar una y otra vez. Primero tienes que volver a ser tú mismo, recuperar el control, salir del modo de 'pegar o huir'.

Y para esto es necesario un espacio verbal. Simplemente un espacio libre. Si se va a hablar de algo, se puede afirmar exactamente eso y luego salir y realmente encontrar un «espacio»: dar un paseo por el parque, encerrarse en la ducha, no importa. Lo principal es desconectarse temporalmente de la situación. Tan pronto como te desconectas aunque sea por unos segundos, el control regresa, comienzas a pensar con claridad. «Bien, no soy algún idiota, no estoy haciendo cosas tontas, soy una persona bastante útil». Una vez que logras convencerte a ti mismo, es momento de pasar a la siguiente etapa: entender lo que ocurrió. Te atacaron, el ataque vino de donde no lo esperabas, fue una emboscada deshonesta y vil. Eso es malo. El siguiente paso es entender por qué el atacante necesitaba hacer eso. De verdad, ¿por qué? ¿Tal vez porque él mismo está furioso? ¿Por qué está furioso? Por ejemplo, porque cometió un error y no puede asumir la responsabilidad. Así es como se debe procesar cuidadosamente toda la situación. Pero para esto necesitas un espacio para maniobrar, un espacio verbal. El primer paso es romper el contacto verbal. Alejarse de la discusión verbalmente. Cancelarlo, irse lo más rápido posible. Si es una llamada telefónica, simplemente cuelga — esta es una habilidad que adquirí de mi comunicación con mi exesposa. Si la conversación no lleva a nada bueno, simplemente di «adiós» y cuelga. Del otro lado del teléfono: «bla-bla-bla», tú respondes: «ajá, ¡hasta luego!» y cuelgas. Simplemente cortas la conversación. Cinco minutos después, cuando recuperas la capacidad de pensar con claridad, te calmas un poco, es posible reflexionar sobre lo que realmente ocurrió y qué pasará después. Y comenzar a formular una respuesta reflexionada, en lugar de simplemente reaccionar emocionalmente. Para mí, un avance en la autoconciencia fue precisamente eso: en caso de estrés emocional no puedo hablar. Salir de ese estado, pensar y planificar cómo responder y compensar los problemas — esos son los pasos correctos cuando no puedes hablar. La manera más simple es escapar de la situación en la que se manifiesta el estrés emocional y simplemente dejar de participar en ese estrés. Después de esto, recuperas la capacidad de pensar, cuando puedes pensar, surge la posibilidad de hablar, y así sucesivamente.

Por cierto, en la corte, el abogado de la parte opuesta intenta hacer esto contigo, ahora ya se entiende por qué. Porque tiene la capacidad de aplastarte hasta un punto en el que no puedes ni siquiera pronunciar tu nombre, por ejemplo. En el sentido más literal, no puedes hablar. Si esto te está sucediendo y si sabes que estarás en un lugar donde estallan batallas verbales, como en la corte, puedes acudir con tu abogado. El abogado te defenderá y detendrá el ataque verbal de manera completamente legal, y te devolverá el espacio zen perdido. Por ejemplo, en un par de ocasiones necesité llamar a mi familia, el juez fue bastante amigable al respecto, pero el abogado de la parte contraria gritaba y gritaba, ni siquiera podía interrumpir. En tales casos, lo que mejor funciona para mí es utilizar un intermediario. El intermediario detiene toda esa presión que fluye sobre ti de forma continua, descubres el espacio zen necesario, y junto con él regresa la capacidad de hablar. Es todo un campo de conocimiento en el que hay mucho por aprender, mucho por descubrir dentro de uno mismo, y todo se convierte en decisiones estratégicas de alto nivel, diferentes para distintas personas. Algunos no tienen los problemas descritos anteriormente, por lo general, no los tienen las personas que se dedican profesionalmente a las ventas. Todas estas personas que se ganan la vida con palabras: cantantes conocidos, poetas, figuras religiosas y políticos, siempre tienen algo que decir. No tienen esos problemas, pero yo sí.

Andrei: Fue… inesperado. Bien, hemos hablado bastante y es hora de concluir esta entrevista. Nos encontraremos en la conferencia y podremos continuar este diálogo. ¡Nos vemos en Hydra!

Se podrá continuar la comunicación con Cliff en la conferencia Hydra 2019, que se llevará a cabo los días 11 y 12 de julio de 2019 en San Petersburgo. Vendrá con una presentación «La experiencia de memoria transaccional de hardware Azul». Las entradas se pueden adquirir en el sitio oficial.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster