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

Cliff Click es el CTO de Cratus (sensores IoT para la 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 TRS Z-80)! Mejor conocido por trabajar en C2 en Java (Sea of ​​Nodes IR). Este compilador mostró al mundo que JIT puede producir código de alta calidad, que se ha convertido en uno de los factores para hacer de Java una de las principales plataformas de software modernas. Cliff luego ayudó a Azul Systems a construir un mainframe de 864 núcleos con un software Java puro que admitía pausas GC en un montón de 500 gigabytes durante 10 milisegundos. En general, Cliff logró trabajar en todos los aspectos de la JVM.

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


  • Transición a optimizaciones de bajo nivel
  • Cómo hacer muchas refactorizaciones
  • Modelo de costo
  • Entrenamiento de optimización de bajo nivel
  • Estudios de casos de mejora de la productividad
  • ¿Por qué crear tu propio lenguaje de programación?
  • Carrera de ingeniero de rendimiento
  • Desafíos técnicos
  • Un poco sobre la asignación de registros y multinúcleo
  • El mayor desafío de la vida.

Entrevistas realizadas por:


  • Andrey Satarin de Amazon Web Services. En su carrera, logró trabajar en proyectos completamente diferentes: probó la base de datos distribuida NewSQL en Yandex, el sistema de detección en la nube en Kaspersky Lab, el juego multiusuario en Mail.ru y el servicio de cálculo de cambio de divisas en Deutsche Bank. Está interesado en probar sistemas distribuidos y backend a gran escala.
  • Vladimir Sitnikov de Netcracker. Durante diez años, ha estado trabajando en el rendimiento y la escalabilidad del sistema operativo NetCracker, un software utilizado por los operadores de telecomunicaciones para automatizar los procesos de gestión de redes y equipos de red. Está interesado en los problemas de rendimiento de Java y Oracle Database. El 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


Andrei : Eres una persona famosa en el mundo de la compilación JIT, en Java y trabajas en el rendimiento en general, ¿verdad?


Cliff : ¡Eso es!


Andrew : Comencemos con preguntas generales sobre cómo trabajar en el rendimiento. ¿Qué opina de la elección entre optimizaciones de alto y bajo nivel como el trabajo a nivel de CPU?


Acantilado : es fácil. El código más rápido es uno que nunca se ejecuta. Por lo tanto, siempre debe comenzar desde un alto nivel, trabajar en algoritmos. Una mejor notación O superará a una notación O peor, a menos que intervengan constantes bastante grandes. Las cosas de bajo nivel son lo último. Por lo general, si optimizó el resto de la pila lo suficientemente bien, y todavía queda algo interesante: este es el nivel bajo. Pero, ¿cómo comenzar desde un nivel alto? ¿Cómo descubrir que se ha realizado suficiente trabajo a alto nivel? Bueno ... de ninguna manera. No hay recetas preparadas. Debe comprender el problema, decidir qué va a hacer (para no hacer pasos innecesarios en el futuro) y luego puede descubrir un generador de perfiles que pueda decir algo útil. En algún momento, usted mismo comprende que se deshizo de cosas innecesarias y que es hora de ajustar el nivel bajo. Este es definitivamente un tipo especial de arte. Mucha gente hace cosas innecesarias, pero se mueven tan rápido que no tienen tiempo para preocuparse por el rendimiento. Pero esto es mientras la pregunta no se mantenga en pie. Por lo general, el 99% de las veces a nadie le importa lo que hago, hasta el momento en que algo importante que le importa a alguien no entra en el camino crítico. Y aquí todo el mundo comienza a molestarte sobre el tema "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 pistas! Solo está tratando de hacer que algo funcione y en el proceso comprende lo que es importante. Nunca se puede saber de antemano que esta pieza debe perfeccionarse, por lo tanto, en esencia, debe ser perfecto en todo. Y esto es imposible, y tú no haces eso. Siempre hay un montón de cosas que arreglar, y eso es perfectamente normal.


Cómo hacer muchas refactorizaciones


Andrew : ¿Cómo trabajas en el rendimiento? Este es un tema transversal. Por ejemplo, ¿ha tenido que trabajar en problemas derivados de la intersección de una gran cantidad de funcionalidad existente?


Cliff : trato de evitar esto. Si sé que el rendimiento se convertirá en un problema, lo pienso antes de comenzar a codificar, especialmente en estructuras de datos. Pero a menudo descubres todo esto mucho más tarde. Y luego tienes que tomar medidas extremas y hacer lo que yo llamo "reescribir y conquistar": necesitas agarrar una pieza bastante grande. Parte del código aún tendrá que reescribirse debido a problemas de rendimiento u otra cosa. Cualquiera sea la razón para reescribir el código, casi siempre es mejor reescribir un fragmento más grande que un fragmento más pequeño. En este momento, todos comienzan 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 el gran problema de inmediato, dibujar un gran círculo alrededor de él y decir: reescribiré todo dentro del círculo. El borde es mucho más pequeño que el contenido en su interior que debe reemplazarse. Y si esa delimitación de bordes le permite hacer el trabajo en el interior perfectamente: tiene las manos desatadas, haga lo que quiera. Una vez que comprenda el problema, el proceso de reescritura es mucho más fácil, ¡así que muerda una gran parte!
Al mismo tiempo, cuando reescribe en fragmentos grandes y comprende que el rendimiento se convertirá en un problema, puede comenzar a preocuparse de inmediato. Por lo general, esto se convierte en cosas simples como "no copie datos, administre datos lo más simple posible, hágalos más pequeños". En reescrituras grandes, hay formas estándar de mejorar el rendimiento. Y casi siempre giran en torno a los datos.


Modelo de costo


Andrew : En uno de los podcasts, habló sobre modelos de costos en el contexto de la productividad. ¿Puedes explicar qué se entiende por esto?


Acantilado : por supuesto. Nací en una era en la que el rendimiento del procesador era extremadamente importante. Y esta era está volviendo de nuevo: el destino no está exento de ironía. Comencé a vivir en la época de las máquinas de ocho bits; mi primera computadora funcionaba con 256 bytes. Es bytes. Todo era muy pequeño. Tuvimos que leer las instrucciones y tan pronto como comenzamos a subir la pila de lenguajes de programación, los lenguajes adquirieron cada vez más. Hubo Assembler, luego Basic, luego C, y C se hizo cargo del trabajo con muchos detalles, como la asignación de registros y la selección de instrucciones. Pero todo estaba bastante claro allí, y si hice un puntero a una instancia de una variable, obtendré carga, y el costo es conocido por esta instrucción. Iron produce un número conocido de ciclos de máquina, por lo que la velocidad de ejecución de diferentes piezas se puede calcular simplemente agregando todas las instrucciones que estaba a punto de ejecutar. Cada comparación / prueba / sucursal / llamada / carga / tienda podría plegarse y decir: aquí tiene el tiempo de entrega. Cuando mejore el rendimiento, definitivamente prestará atención a qué tipo de números corresponden a pequeños ciclos calientes.
Pero tan pronto como cambie a Java, Python y cosas similares, se alejará rápidamente del hierro de bajo nivel. ¿Cuánto cuesta una llamada getter en Java? Si el JIT en HotSpot está correctamente en línea , se cargará, pero si no lo hizo, será una llamada de función. Dado que el desafío radica en el bucle activo, deshacerá todas las demás optimizaciones en este bucle. Por lo tanto, el valor real será mucho mayor. E inmediatamente pierde la capacidad de mirar un fragmento de código y comprende que debemos ejecutarlo en términos de la velocidad del reloj del procesador, la memoria utilizada y el caché. Todo esto se vuelve interesante solo si realmente te emborrachaste en el rendimiento.
Ahora estamos en una situación en la que las velocidades de los procesadores casi no han crecido durante una década. Los viejos tiempos han vuelto! Ya no puede contar con un buen rendimiento de subproceso único. Pero si de repente te involucras en la computación paralela, es increíblemente difícil, todos te miran como James Bond. La aceleración de diez veces aquí generalmente ocurre en aquellos lugares donde alguien golpea algo. La concurrencia requiere mucho trabajo. Para obtener la misma aceleración de diez veces, debe comprender el modelo de costos. Qué y cuánto cuesta. Y para esto necesita comprender cómo la lengua se apoya en el hierro subyacente.
¡Martin Thompson tiene una gran palabra para su blog Mechanical Sympathy ! Debe comprender qué hará el hierro, cómo lo hará exactamente y por qué generalmente hace lo que hace. Con esto, es bastante simple comenzar a leer las instrucciones y descubrir dónde fluye el tiempo de ejecución. Si no tienes el entrenamiento adecuado, solo estás buscando un gato negro en una habitación oscura. Constantemente veo personas que optimizan el rendimiento que no tienen idea de qué demonios están haciendo. Están muy atormentados y realmente no van a ningún lado. Y cuando tomo el mismo código, le doy un par de pequeños trucos allí y obtengo una aceleración de cinco o diez veces, son así: bueno, es tan deshonesto, ya sabíamos que eres mejor. Es asombroso. De lo que estoy hablando ... el modelo de costo es sobre qué código escribes y qué tan rápido funciona en promedio en la imagen general.


Andrew : ¿Y cómo mantener ese volumen en tu cabeza? ¿Esto se logra con más experiencia o? ¿Dónde se gana esa experiencia?


Cliff : Bueno, mi experiencia no fue la forma más fácil. Lo programé en Assembler en un momento en que era posible entender cada instrucción individual. Suena tonto, pero desde entonces en mi cabeza, en mi memoria, el conjunto de instrucciones Z80 se ha mantenido para siempre. No recuerdo los nombres de las personas un minuto después de la conversación, pero recuerdo el código escrito hace 40 años. Es curioso, parece un síndrome de " idiota aprendido ".


Entrenamiento de optimización de bajo nivel


Andrew : ¿Hay alguna forma más sencilla de entrar en el negocio?


Acantilado : Sí y no. El hierro que todos usamos no ha cambiado tanto durante este tiempo. Todos usan x86, con la excepción de los teléfonos inteligentes Arm. Si no realiza una incrustación hardcore, tiene lo mismo. Ok, siguiente Las instrucciones tampoco han cambiado en siglos. Tienes que ir y escribir algo en Assembler. Un poco, pero suficiente para comenzar a entender. Estás sonriendo, pero lo digo en serio. Es necesario comprender la correspondencia del lenguaje y el hierro. Después de eso, debes ir, orinar un poco y hacer un pequeño compilador de juguetes para un lenguaje de juguetes pequeño. "Juguete" significa que debes hacerlo en un tiempo razonable. Puede ser súper simple, pero debe generar instrucciones. El acto de generar instrucciones nos permitirá comprender el modelo de costo para el puente entre el código de alto nivel en el que todos escriben y el código de máquina que se ejecuta en el hardware. Esta correspondencia se grabará en el cerebro al momento de escribir el compilador. Incluso el compilador más simple. Después de eso, puede comenzar a mirar Java y al hecho de que tiene una brecha semántica más profunda, y construir puentes encima es mucho más difícil. En Java, es mucho más difícil entender si nuestro puente resultó ser bueno o malo, lo que hará que se desmorone y no. Pero necesitas un punto de partida cuando miras el código y entiendes: "sí, este captador debe estar en línea todo el tiempo". Y luego resulta que a veces esto sucede, con la excepción de la situación cuando el método se hace demasiado grande y el JIT comienza a alinear todo. El rendimiento de dichos lugares se puede predecir al instante. Por lo general, los captadores funcionan bien, pero luego miras los grandes bucles calientes y te das cuenta de que hay algún tipo de llamadas a funciones flotantes 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: no está claro si se trata de un captador. Si tiene una base de código súper pequeña, puede recordarla y luego decir: este es un captador, pero este es un establecedor. En una base de código grande, cada función vive su propia historia, que, en general, nadie conoce. El generador de perfiles dice que perdimos el 24% de nuestro tiempo en algún tipo de ciclo, y para comprender lo que hace este ciclo, debemos observar cada función en su interior. Es imposible entender esto sin estudiar la función, y esto ralentiza seriamente el proceso de comprensión. Es por eso que no uso getters y setters, ¡fui a un nuevo nivel!
¿Dónde obtener el modelo de costo? Bueno, puedes leer algo, por supuesto ... Pero creo que la mejor manera es actuar. Haga un pequeño compilador y esta será la mejor manera de realizar el modelo de costos y ajustarlo en su propia cabeza. Un pequeño compilador que funcionaría para la programación de microondas es una tarea para un principiante. Bueno, quiero decir, si ya tienes habilidades de programación, entonces deberían ser suficientes. Todas estas cosas son como analizar una cadena, que tendrá algún tipo de expresión algebraica, extraer las instrucciones de las operaciones matemáticas desde allí en el orden correcto, tomar los valores correctos de los registros; todo esto se hace a la vez. Y mientras lo hagas, quedará impreso en el cerebro. Creo que todos saben lo que hace el compilador. Y esto dará una comprensión del modelo de costos.


Estudios de casos de mejora de la productividad


Andrew : ¿A qué más vale la pena prestar atención al trabajar en el rendimiento?


Acantilado : Estructuras de datos. Por cierto, sí, no he enseñado estas clases durante mucho tiempo ... Rocket School . Fue divertido, pero tomó mucho esfuerzo invertir, ¡y también tengo vida! Esta bien Entonces, en una de las clases grandes e interesantes, "¿A dónde va su desempeño?", Les di un ejemplo a los estudiantes: se leyeron dos gigabytes y medio de datos fintech de un archivo CSV y luego tuvimos que calcular la cantidad de productos vendidos. Datos regulares del mercado de ticks. Paquetes UDP convertidos a formato de texto desde los años 70. El Chicago Mercantile Exchange es todo tipo de cosas como mantequilla, maíz, soja y similares. Era necesario contar estos productos, el número de transacciones, el volumen promedio de movimiento de fondos y bienes, etc. Esta es una matemática comercial bastante simple: encuentre el código del producto (estos son 1-2 caracteres en la tabla hash), obtenga la cantidad, agréguela a uno de los conjuntos de ofertas, agregue volumen, agregue valor y un par de otras cosas. Muy simple matemática. La implementación del juguete fue muy sencilla: todo está en el archivo, lo leo y me muevo alrededor, separando las entradas individuales en cadenas de Java, buscando las cosas necesarias en ellas y doblándolas de acuerdo con las matemáticas descritas anteriormente. Y funciona a baja velocidad.


Con este enfoque, todo es obvio lo que está sucediendo, y la computación paralela no ayudará aquí, ¿verdad? Resulta que un aumento de cinco veces en la productividad solo se puede lograr eligiendo las estructuras de datos correctas. ¡Y esto incluso sorprende a los programadores experimentados! En mi caso particular, el truco fue que no debería hacer asignaciones de memoria en un bucle activo. Bueno, esta no es toda la verdad, pero en general, no debe resaltar "una vez en X" cuando X es lo suficientemente grande. Cuando X es de dos gigabytes y medio, no debe asignar nada "una vez por letra", "una vez por línea", o "una vez por campo", nada de eso. Eso es exactamente lo que lleva tiempo. ¿Cómo funciona? Imagine hacer una llamada a String.split() o BufferedReader.readLine() . Readline una línea a partir de un conjunto de bytes que provienen de la red, una vez por cada línea, por cada uno de cientos de millones de líneas. Tomo esta línea, la analizo y la tiro. ¿Por qué tirarlo? 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, 5.4G ya, y ya no los necesito, por lo tanto, se descartan. Si observa 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ían el doble a la línea que se encuentra en la memoria, y todo esto se deshilacha cuando se crea cada nueva línea. Pero necesito leerlo, el hierro lo lee, incluso si entonces todo se frota. Y tengo que escribirlo, porque creé la línea y los cachés estaban llenos; el caché no puede caber 2.7G. En total, por cada byte leído, leo dos bytes más y escribo dos bytes adicionales, y como resultado tienen una relación 4: 1, en esta relación desperdiciamos ancho de banda de memoria. Y luego resulta que si hago String.split() , entonces no lo hago la última vez, puede haber otros 6-7 campos dentro. Por lo tanto, el código de lectura CSV clásico seguido del análisis de líneas conduce a una pérdida de ancho de banda de memoria en la región de 14: 1 en relación con lo que realmente le gustaría tener. Si tira estas secreciones, puede obtener una aceleración de cinco veces.


Y no es tan difícil. Si observa el código desde el ángulo correcto, todo se vuelve bastante simple, tan pronto como se dé cuenta de la esencia del problema. Ni siquiera deje de asignar memoria: el único problema es que asigna algo e inmediatamente muere y quema un recurso importante en el camino, que en este caso es el ancho de banda de la memoria. Y todo esto resulta en una caída en la productividad. En x86, generalmente necesita grabar activamente los relojes del procesador, y aquí quemó toda la memoria mucho antes. Solución: debe reducir la cantidad de descarga.
Otra parte del problema es que si inicia el generador de perfiles cuando la tira de memoria ha finalizado, justo en el momento en que esto sucede, generalmente espera a que regrese el caché, porque está lleno de basura que acaba de generar con todas estas líneas. Por lo tanto, cada operación de carga o almacenamiento se vuelve lenta, ya que conducen a errores en el caché: todo el caché se volvió lento, esperando que la basura lo abandone. Por lo tanto, el generador de perfiles solo mostrará un ruido aleatorio cálido extendido a lo largo de todo el ciclo; no habrá instrucciones o lugares calientes independientes en el código. Solo el ruido. Y si observa los ciclos de GC, todos serán Young Generation y súper rápidos, microsegundos o milisegundos como máximo. Después de todo, todo este recuerdo muere instantáneamente. Usted asigna miles de millones de gigabytes, y los corta, y los corta, y los vuelve a cortar. Todo esto sucede muy rápido. Resulta que hay ciclos de GC baratos, ruido cálido a lo largo de todo el ciclo, pero queremos obtener una aceleración 5x. En ese momento, algo debería cerrarse en mi cabeza y sonar: "¿por qué?" El desbordamiento del ancho de banda no aparece en el depurador clásico, debe ejecutar el depurador del contador de rendimiento del hardware y verlo usted mismo y directamente. Y no directamente, se puede sospechar de estos tres síntomas. El tercer síntoma es cuando miras lo que resaltas, le preguntas al generador de perfiles, y él responde: "Hiciste mil millones de líneas, pero el GC funcionó de forma gratuita". Tan pronto como esto sucedió, te das cuenta de que has generado demasiados objetos y quemado toda la tira de memoria. Hay una manera de resolver esto, pero no es obvio.


El problema está en la estructura de datos: la estructura básica detrás de todo lo que sucede, es demasiado grande, es 2.7G en el disco, por lo que hacer una copia de esto es muy indeseable: quiero cargarlo desde el búfer de bytes de red inmediatamente en los registros para no leer y escribir en la cadena ida y vuelta cinco veces. Desafortunadamente, Java por defecto no le proporciona una biblioteca como parte del JDK. Pero esto es trivial, ¿verdad? De hecho, estas son 5-10 líneas de código que se usarán para implementar su propio cargador de línea almacenado en búfer, que repite el comportamiento de la clase de línea, mientras que es un contenedor alrededor del búfer de bytes subyacente. Como resultado, resulta que trabajas casi como con cadenas, pero de hecho hay punteros que se mueven al búfer, y los bytes sin procesar no se copian en ninguna parte, y de esta manera se reutilizan los mismos búferes, una y otra vez, y el sistema operativo está feliz de asumir cosas para las que está destinado, como el doble almacenamiento en búfer oculto de estos búferes de bytes, y usted mismo ya no tritura un flujo interminable de datos innecesarios. Por cierto, ¿entiendes que cuando trabajas con el GC, se garantiza que cada asignación de memoria no será visible para el procesador después del último ciclo del GC? Por lo tanto, todo esto de ninguna manera puede estar en la memoria caché, y luego ocurre una falla 100% garantizada. Cuando se trabaja con un puntero en x86, restar un registro de la memoria toma 1-2 ciclos, y tan pronto como esto sucede, paga, paga, paga, porque la memoria está en NUEVE cachés , y este es el costo de asignar memoria. Valor presente


En otras palabras, las estructuras de datos son las más difíciles de cambiar. Y tan pronto como se dé cuenta de que ha elegido la estructura de datos incorrecta que matará la productividad en el futuro, por lo general necesita aumentar el trabajo esencial, pero si no lo hace, entonces será peor. En primer lugar, debe pensar en las estructuras de datos, esto es importante. El costo principal aquí radica en las estructuras de datos en negrita, que comienzan a usar al estilo de "Copié la estructura de datos X en la estructura de datos Y, porque me gusta más la forma". Pero la operación de copia (que parece barata) en realidad gasta una franja de memoria y aquí está enterrado todo el tiempo de ejecución perdido. Si tengo una cadena gigante con JSON y quiero convertirla en un árbol DOM estructurado de POJO o algo así, la operación de analizar esta cadena y construir un POJO, y luego una nueva llamada a POJO en el futuro resultará inútil, no es algo barato. Excepto si correrá en POJO con mucha más frecuencia que en una línea. Por el contrario, puede intentar descifrar la cadena y extraer solo lo que necesita de ella, sin convertirla en ningún POJO. Si todo esto sucede en la ruta desde la que se requiere el máximo rendimiento, no hay POJO para usted; de alguna manera, debe excavar directamente en la línea.


¿Por qué crear tu propio lenguaje de programación?


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


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


Andrei : Por cierto, que yo sepa, estás experimentando con la creación de tus propios idiomas. Por qué


Cliff : ¡Porque puedo! Estoy medio retirado, así que este es mi hobby. He estado implementando los idiomas de otra persona toda mi vida. También trabajé duro en el estilo de codificación. Y también porque veo problemas en otros idiomas. Veo que hay mejores formas de hacer las cosas habituales. Y los usaría. Me cansé de ver problemas en mí mismo, en Java, en Python, en cualquier otro idioma. Escribo en React Native, JavaScript y Elm como un pasatiempo, que no se trata de la jubilación, sino del trabajo activo. Y también escribo en Python y, muy probablemente, continuaré trabajando en aprendizaje automático para backends Java. Hay muchos idiomas populares y todos tienen características interesantes. Todos son buenos en algo propio y puedes intentar juntar todas estas fichas. Entonces, estudio las cosas que me interesan, el comportamiento del lenguaje, trato de llegar a una semántica razonable. ¡Y hasta ahora lo estoy haciendo! En este momento, estoy luchando con la semántica de la memoria, porque quiero tenerla tanto en C como en Java, y obtener un modelo de memoria fuerte y una semántica de memoria para cargas y tiendas. Al mismo tiempo, tenga una inferencia de tipo automática como en Haskell. Aquí, estoy tratando de mezclar inferencia tipo Haskell con memoria trabajando tanto en C como en Java. He estado haciendo esto durante los últimos 2-3 meses, por ejemplo.


Andrei : Si estás construyendo un lenguaje que toma mejores aspectos de otros idiomas, ¿pensaste que alguien haría lo contrario: tomar tus ideas y usarlas?


Cliff : ¡Así es como aparecen los nuevos idiomas! ¿Por qué Java es similar a C? Debido a que C tenía una buena sintaxis que todos entendían y Java se inspiró en esta sintaxis, agregando seguridad de tipos, verificando los límites de las matrices, GC y mejoraron algunas cosas de C. Agregaron la suya propia. Pero se inspiraron bastante, ¿verdad? Todos se paran sobre los hombros de los gigantes que vinieron antes que tú, así es como se progresa.


Andrew : Según tengo entendido, su idioma estará seguro con respecto al uso de la memoria. ¿Alguna vez has pensado en implementar algo como el comprobador de préstamos de Rust? Lo miraste, ¿cómo te gustaba?


Cliff : Bueno, he estado escribiendo C durante años, con todos estos malloc y gratis, y administro manualmente la vida. Ya sabes, el 90-95% de un tiempo de vida administrado manualmente tiene la misma estructura. Y es muy, muy doloroso hacer esto manualmente. Me gustaría que el compilador simplemente dijera lo que está sucediendo allí y lo que logró con sus acciones. Para algunas cosas, un corrector de préstamos lo hace fuera de la caja. Y debería mostrar información automáticamente, entender todo y ni siquiera cargarme para expresar esta comprensión. Debería hacer al menos un análisis de escape local, y solo si no tiene éxito, entonces debe agregar anotaciones de tipo que describirán el tiempo de vida, y dicho esquema es mucho más complicado que un comprobador de préstamos o cualquier comprobador de memoria existente. La elección entre "todo está en orden" y "No entendí nada" - no, debe haber algo mejor.
Entonces, como persona que escribió mucho código C, creo que tener el soporte para el control automático de por vida es lo más importante. Y me cansé de cuánto Java usa la memoria y la queja principal está en GC. Al asignar memoria en Java, no devolverá la memoria que era local en el último bucle del GC. En idiomas con administración de memoria más precisa, esto no es así. Si llamas a malloc, inmediatamente obtienes la memoria que generalmente se usaba. Por lo general, hace algunas cosas temporales con su memoria e inmediatamente la recupera. E inmediatamente regresa a la piscina de malloc, y el siguiente ciclo de malloc la saca de nuevo. Por lo tanto, el uso de memoria real se reduce a un conjunto de objetos vivos en un punto particular en el tiempo, además de fugas. Y si todo no fluye de manera indecente, la mayor parte de la memoria se deposita en cachés y en el procesador, y funciona rápidamente. Pero requiere una gran cantidad de administración de memoria manual con malloc y gratis, llamado en el orden correcto, en el lugar correcto. El óxido en sí mismo puede manejar esto correctamente y, en muchos casos, ofrece un rendimiento aún mayor, ya que el consumo de memoria se reduce solo a los cálculos actuales, en lugar de esperar al próximo ciclo de GC para liberar memoria. Como resultado, obtuvimos una forma muy interesante de mejorar el rendimiento. Y bastante poderoso, en el sentido, hice esas cosas al procesar datos para el fintech, y esto me permitió acelerar cinco veces. Esta es una aceleración bastante grande, especialmente en un mundo donde los procesadores no son cada vez más rápidos, y todos seguimos esperando mejoras.


Carrera de ingeniero de rendimiento


Andrew : También me gustaría preguntar sobre la carrera en general. Te hiciste famoso por trabajar en JIT en HotSpot y luego mudarte a Azul, y esta también es una empresa JVM. Pero ya estaban involucrados en más hierro que software. Y de repente se cambió a Big Data y Machine Learning, y luego a detección de fraude. Como sucedio Estas son áreas muy diferentes de desarrollo.


Cliff : He estado programando durante bastante tiempo y logré registrarme en clases muy diferentes. Y cuando la gente dice: "¡Oh, tú eres el que hizo JIT para Java!", Siempre es divertido. Pero antes de eso, participé en el clon PostScript, el lenguaje que Apple alguna vez usó para sus impresoras láser. Y antes de eso hizo la implementación del lenguaje Forth. Creo que el tema común para mí es el desarrollo de herramientas. Toda mi vida he estado haciendo herramientas con las que otras personas escriben sus programas geniales. Pero también participé en el desarrollo de sistemas operativos, controladores, depuradores a nivel de núcleo, lenguajes para desarrollar el sistema operativo, que comenzó de manera trivial, pero con el tiempo todo se volvió complicado y complicado. Pero el tema principal, sin embargo, es el desarrollo de herramientas. Una gran parte de la vida pasó entre Azul y Sun, y se trataba de Java. Pero cuando comencé Big Data y Machine Learning, me puse el sombrero de nuevo y dije: "Ah, y ahora tenemos un problema no trivial, y aquí suceden muchas cosas interesantes y personas que hacen algo". Este es un gran camino de desarrollo que vale la pena tomar.


Sí, realmente me gusta la informática distribuida. Mi primer trabajo fue como estudiante en C, en un proyecto publicitario. Estos fueron distribuidos computación en chips Zilog Z80, que recopilaron datos para el reconocimiento de texto óptico analógico producido por un analizador analógico real. Fue un tema genial y totalmente anormal. Pero hubo problemas, una parte no se reconoció correctamente, por lo que era necesario obtener una imagen y mostrársela a una persona que ya leía con los ojos e informaba lo que se decía allí, y por lo tanto, había falsificadores de datos, y este trabajo tenía su propio idioma. . Hubo un back-end que manejó todo esto, funcionando en paralelo al Z80 con terminales vt100 en ejecución, uno por persona, y había un modelo de programación paralela en el Z80. Cierta pieza de memoria común compartida por todos los Z80 dentro de una configuración en estrella; se compartió el plano posterior, y la mitad de la RAM se compartió dentro de la red, y otra mitad era privada o se gastaba en otra cosa. Un sistema distribuido paralelo significativamente complejo con memoria compartida ... semi-compartida. Cuando era ... Ya no lo recuerdo, en algún lugar a mediados de los 80. Hace bastante tiempo
Sí, asumiremos que 30 años es bastante tiempo. Las tareas asociadas con la informática distribuida han existido durante mucho tiempo, la gente ha luchado durante mucho tiempo con los clústeres de Beowulf . Tales clústeres se parecen a ... Por ejemplo: hay Ethernet y su x86 rápido está conectado a esta Ethernet, y ahora desea obtener una memoria compartida falsa, porque nadie podía codificar la informática distribuida, era demasiado complicado y, por lo tanto, era una memoria compartida falsa con protección páginas de memoria x86, y si escribiste en esta página, les dijimos a los otros procesadores que si tenían acceso a la misma memoria compartida, tendrían que descargarse de ti, y por lo tanto apareció algo así como un protocolo de soporte de coherencia de caché y software para esto. Concepto interesante El verdadero problema, por supuesto, era diferente. Todo esto funcionó, pero rápidamente tuvo problemas de rendimiento, porque nadie entendía los modelos de rendimiento a un nivel suficientemente bueno: qué patrones de acceso a la memoria existen, cómo asegurarse de que los nodos no se hagan ping interminablemente, y así sucesivamente.


En H2O, se me ocurrió esto: los propios desarrolladores son responsables de determinar dónde está oculto el paralelismo y dónde no. Se me ocurrió un modelo de codificación tal que escribir código de alto rendimiento fue fácil y simple. Pero escribir código de ejecución lenta es difícil, se verá mal. Debe intentar seriamente escribir código lento, debe utilizar métodos no estándar. El código de frenado es visible de un vistazo. Como resultado, generalmente se escribe un código que funciona rápidamente, pero debe averiguar qué hacer en el caso de la memoria compartida. Todo esto está vinculado a grandes matrices y el comportamiento allí es similar a las grandes matrices no volátiles en Java paralelo. Quiero decir, imagina que dos hilos escriben en una matriz paralela, uno de ellos gana, y el otro, respectivamente, pierde, y no sabes cuál de ellos es quién. Si no son volátiles, entonces el orden puede ser cualquier cosa, y realmente funciona bien. Las personas realmente se preocupan por el orden de las operaciones, establecen volátiles correctamente y esperan problemas de memoria en los lugares correctos. De lo contrario, simplemente escribirían el código en forma de ciclos de 1 a N, donde N son algunos billones, con la esperanza de que todos los casos complejos se vuelvan paralelos automáticamente, y esto no funciona allí. Pero en H2O esto no es Java ni Scala, puede considerarlo "Java menos menos" si lo desea. Este es un estilo de programación muy comprensible y es similar a escribir código C o Java simple con bucles y matrices. Pero al mismo tiempo, la memoria puede procesarse con terabytes. Todavía uso H2O. – , . Big Data , H2O.



: ?


: ? , – .
. . , , , , . Sun, , , , . , , . , C1, , – . , . , x86- , , 5-10 , 50 .


, , , , C. , , - , C . C, C . , , C, - … , . , . , , . , , 5% . - – , « », , . : , , . . , – , . , . - – . , , ( , ), , , . , , , .


, , , , , , . , , , - . , , , . , , , , . , : , . , , - : , , - , . – , , – ! – , . Java. Java , , , , – , « ». , , . , Java C . – Java, C , , , . , – , . , . , , . : .



: - . , , - , ?


: ! – , NP- - . , ? . , Ahead of Time – . - . , , – , ! – , . , , . . ? , : , , - ! - , . . , , . : - , - . , , . , , , , - . ! , , , – . . NP- .


: , – . , , , , …


: . «». . , . – , , , ( , ). , - . , , , . , , . , . , , . , , . , , - , – . – . , GC, , , , – , . , . , , . , – , ? , .


: , ? ?


: GPU , !


: . ?


: , - Azul. , . . H2O , . , GPU. ? , Azul, : – .



: ?


: , … . , . , , , , . , , . , Java C1 C2 – . , Java – . , , – . … . - , Sun, … , , . , . , . … … , . , , . . - , : . , , , , , , . , . . , . « , , ». : «!». , , , : , .


– , , , . . , , , , . , Java JIT, C2. , – . , – ! . , , , , , , . . . , . , , , , : , , . , – . , , - . : « ?». , . , , : , , – ? , . , , , , , , - .


: , -. ?


: , , . – . . , . . . : , , - – . . , , – , . , , , , - , . , . , , - . , , – , .
, . , – , , . , . , – . , . , , « », , – , , , , . , , « ».


. . - , , «»: , – . – . , , . «, -, , ». , : , . , , . . – , . , ? , ? ? , ? . , . – . . , . – – , . , « » . : «--», : «, !» . . , , , , . , . , . , – , . – , . , , , .


, – , . , , . , . , , , , . , , . , , , , . . , , , . , , , , . , , , . , – , , , . , .


: … . , . . Hydra!


Hydra 2019, 11-12 2019 -. «The Azul Hardware Transactional Memory experience» . .

Source: https://habr.com/ru/post/458718/


All Articles