3 puntos por GN⁺ 2024-03-18 | 1 comentarios | Compartir por WhatsApp
  • Fedora y Ubuntu 24.04 LTS están volviendo a compilar por defecto los componentes del sistema operativo, como libc, incluyendo frame pointers, lo que permite que el profiling en Linux y los Flame Graph vean stacks más completos
  • Sin frame pointers, el profiler puede detener el stack walking en capas de librerías como libc o seguir frames incorrectos, lo que puede romper los Flame Graph de CPU y Off-CPU
  • El cambio de gcc para i386 en 2004 fue una decisión para conseguir un registro adicional, pero terminó extendiéndose a x86-64 y dejó un costo de largo plazo para los profilers del sistema y la observabilidad basada en eBPF
  • En la experiencia de Netflix con Java y libc, el costo normalmente fue de menos de 1%; algunos reportes muestran entre 1% y 2%, y en microbenchmarks especiales o cargas anómalas llegó hasta 10%
  • Existen alternativas como LBR, DWARF, eBPF stack walker, ORC, SFrames y Shadow Stacks, pero hoy la forma más práctica de encontrar mejoras de rendimiento en producción es habilitar frame pointers por defecto

Por qué se rompen los Flame Graph

  • Un CPU Flame Graph puede verse normal a simple vista, pero en sistemas donde libc fue compilada sin frame pointers, algunas muestras pueden agruparse incorrectamente sobre [unknown]
    • En el ejemplo, el 15% de las muestras de la izquierda está en una posición incorrecta y faltan frames de la aplicación
    • El profiler atraviesa los frames del kernel, llega al syscall y al wrapper de syscall en libc, y luego falla al resolver el símbolo del siguiente frame
  • La causa es una optimización del compilador en la que el registro frame pointer se usa para guardar datos en lugar de servir como punto de referencia del stack frame
    • El profiler no puede saber si ese valor es solo un número e intenta interpretarlo como una dirección de función
    • Si el valor no apunta al siguiente frame, el stack walking se detiene
    • Si por casualidad parece un puntero válido, puede generar un junk frame incorrecto
    • Si el valor apunta a sí mismo, puede formarse una torre de junk frames hasta alcanzar el límite máximo de frames de perf
  • Los Off-CPU Flame Graph pueden romperse aún más cuando no hay frame pointers, porque suelen aparecer muchas funciones read/write y de mutex de libc
  • Si la propia aplicación también fue compilada sin frame pointers, no solo se ven afectadas las librerías, sino todo el stack trace

Qué información aportan los frame pointers

  • El ABI de x86-64 define que %rbp puede usarse como base pointer del stack frame, es decir, como frame pointer
  • Profilers y debuggers externos como Linux perf y eBPF usan esa información para recorrer el stack trace y visualizar el resultado como Flame Graph
  • El ABI de x86-64 incluye una nota al pie que dice que usar %rbp es opcional
    • Si se indexa el stack frame con %rsp, se pueden ahorrar dos instrucciones en el prologue y el epilogue
    • %rbp puede usarse como un registro de propósito general adicional

La eliminación en 2004 y 20 años de consecuencias

  • En 2004, gcc cambió para que el backend de i386 hiciera por defecto algo equivalente a -fomit-frame-pointer -ffixed-ebp
  • i386 solo tenía 4 registros de propósito general, así que liberar %ebp aumentaba de forma importante los registros disponibles
    • Entre las razones del cambio estaba también lograr mejor rendimiento que el compilador icc de Intel
    • En ese momento también se asumía que los debuggers no se romperían porque ya soportaban otras técnicas de stack walking
  • Ese cambio también se aplicó a x86-64
    • x86-64 tiene más de 12 registros, así que la ventaja de conseguir uno más no era tan grande como en i386
    • Los profilers de sistema actuales, como eBPF, no existían entonces, y este cambio puede romperlos
  • En 2004, Eric Schrock consideró que en amd64 la ventaja de obtener un 17.º registro de propósito general no compensaba la pérdida de capacidad de depuración
    • Dejó la evaluación de que una vez que se empezaba a compilar /usr/bin sin frame pointers, la situación se volvía incontrolable
    • En Linux, esa misma tendencia se expandió no solo a /usr/bin, sino también a /usr/lib y al código de las aplicaciones

Experiencia de uso en Java, libc y Netflix

  • Cuando se unió a Netflix en 2014, todo el stack de la aplicación estaba roto por la falta de soporte de frame pointers en Java
  • Se creó un parche para el compilador c2 de la JVM, y Oracle lo retrabajó para agregar la opción -XX:+PreserveFramePointer en JDK8u60
  • El cambio en Java permitió encontrar varias mejoras de rendimiento en el código de aplicación, pero libc seguía rompiendo parte de las muestras de CPU y la mayoría de los Off-CPU Flame Graph
  • Después compilaron internamente una libc con frame pointers para producción y también trabajaron con Canonical en una libc precompilada para Ubuntu
    • Durante un tiempo se recomendó usar libc6-prof
    • libc6-prof era libc6 con frame pointers incluidos

Overhead de rendimiento y casos excepcionales

  • Durante la implementación en producción, el costo de agregar frame pointers a libc y Java normalmente fue de menos de 1%
  • Hubo un caso excepcional en el que una aplicación mostró 10% de overhead
    • Era una aplicación inusual que generaba stack traces de más de 1000 frames a través de Groovy
    • Linux perf tampoco podía manejarlo, así que Arnaldo Carvalho de Melo, de Red Hat, agregó el sysctl kernel.perf_event_max_stack para la carga de trabajo de Netflix
    • Ese entorno era una máquina virtual sin funciones de profiling de hardware de bajo nivel, así que no se pudo confirmar si ese 10% se debía completamente a los frame pointers
  • Otros reportes también muestran costos de alrededor de 1% o 2%
  • Los microbenchmarks pueden empeorar hasta 10%
    • Cuando se ejecutan funciones pequeñas repetidamente en un loop, las instrucciones adicionales pueden afectar la calidez de la L1 cache o los límites de las cache lines
    • En ese caso, el efecto podría producirse no por el frame pointer en sí, sino por agregar cualquier cosa a una función hot
  • El benchmark de Python scimark_sparse_mat_mult también podía llegar a 10%
    • Según el análisis de Andrii Nakryiko, fue un caso especial donde en una función grande gcc usó offsets relativos a %rbp en lugar de offsets de %rsp, requiriendo más bytes y causando el problema de rendimiento
    • Después también hubo noticias de que Python fue modificado para poder reactivar por defecto los frame pointers
  • Las mejoras de rendimiento encontradas gracias a los frame pointers fueron desde 5% hasta 500%, y se considera que un costo normalmente inferior a 1% suele estar justificado
  • Si se trata de dispositivos sin posibilidad de profiling o debugging, puede compilarse sin frame pointers, pero el objetivo principal son Linux empresariales y servidores backend

Activación por defecto en Fedora y Ubuntu

  • Grandes empresas como Meta, Google y Netflix ya usaban internamente una libc con frame pointers para asegurar capacidades de profiling
  • El primer intento de upstream de habilitarlo por defecto en Fedora derivó en una discusión larga
    • La discusión en Fedora se convirtió en un hilo con 116 mensajes
    • Un participante exigió que Meta o Netflix proporcionaran infraestructura de side repository para pruebas, benchmarks y mediciones del tamaño del código
    • Jonathan Corbet resumió esto en Fedora's tempest in a stack frame
  • Más tarde Fedora volvió a revisar la propuesta y la aceptó, convirtiéndose en la primera distribución en reactivar los frame pointers
  • Ubuntu también anunció frame pointers by default en Ubuntu 24.04 LTS
  • También se agregó la noticia de que Arch Linux está avanzando en la activación de frame pointers
  • El stack walking de las librerías del sistema mejora con este cambio, pero los runtimes de aplicaciones pueden requerir configuración aparte
    • Java ofrece la opción -XX:+PreserveFramePointer
    • Go hizo del soporte de frame pointers el valor por defecto hace algunos años

Candidatos para stack walking después de los frame pointers

  • LBR (Last Branch Record): función de hardware de Intel con un límite de 16 o 32 frames, insuficiente para la mayoría de los stacks de aplicación, aunque puede servir como último recurso para obtener parte de la información del stack
  • BTS (Branch Trace Store): función de Intel con menos limitación en profundidad del stack, pero con costos por load/store de memoria y por el manejo de interrupciones por overflow del buffer de BTS
  • AET (Architectural Event Trace): tracer basado en JTAG que puede seguir eventos de CPU, BIOS y dispositivos de bajo nivel, y parece poder usarse también para stack traces, aunque no hay experiencia directa con él
  • DWARF: formato de debuginfo binario usado desde hace mucho tiempo en debuggers
    • Se señaló que hubo trabajo de JIT-to-DWARF en runtimes JIT
    • No se espera que sea práctico para JVMs de Java en servidores de producción muy ocupados donde c2 sigue ejecutándose continuamente
    • El costo del propio stack walking con DWARF también es alto
  • eBPF stack walking: un tracer externo puede recorrer el interior de runtimes como la JVM sin soporte del runtime
    • Puede requerir muchas lecturas del user space dentro del runtime, por lo que el overhead puede ser alto
    • Como es sensible a cambios del runtime, parece más adecuado que se distribuya y mantenga junto con el code base del lenguaje
  • ORC (oops rewind capability): stack unwinder liviano del kernel de Linux que permite conservar el stack walking aunque kernels nuevos eliminen frame pointers
  • SFrames (Stack Frames): método liviano de unwinding de stack de usuario basado en ORC
  • Shadow Stacks: función de seguridad de Intel y AMD que empuja la dirección de retorno de una función a un stack de hardware separado para validarla al hacer return, y también parece poder aprovecharse para stack traces

Qué cambia desde ahora mismo

  • Las razones para omitir frame pointers en 2004 —la ventaja de rendimiento en i386, la evaluación de compatibilidad con debuggers de esa época y la competencia con icc— ya no encajan con la realidad de 2024
  • Ya en ese entonces se consideraba que eliminar frame pointers en x86-64 no ofrecía beneficios suficientes frente a la pérdida de capacidad de depuración
  • Con Fedora y Ubuntu revirtiendo este cambio, quienes usen sus lanzamientos de 2024 tendrán CPU Flame Graph más fáciles de entender y podrán aprovechar correctamente los Off-CPU Flame Graph por primera vez
  • Los profilers continuos también podrán obtener perfiles más completos sin exigir a los clientes cambios en el sistema operativo
  • En el futuro quizá sea posible volver a hacer stack tracing sin frame pointers mediante enfoques como SFrames o Shadow Stacks, pero la mejora disponible hoy es activar frame pointers por defecto

1 comentarios

 
GN⁺ 2024-03-18
Opiniones de Hacker News
  • Recuerdo cuando, a principios de los 2000, empezó a difundirse la omisión del puntero de marco de pila.
    En ese entonces estudiaba ciencias de la computación en una universidad de un país pobre del tercer mundo, y por las computadoras viejas y lentas, en la mayoría de las tareas usábamos compiladores en vez de intérpretes.
    Había muchas clases interesantes en las que implementábamos desde estructuras de datos de bajo nivel, compiladores y rutinas numéricas en ensamblador hasta drivers de dispositivo para Minix; cuando un programa se comportaba raro, me conectaba con gdb y lo depuraba a nivel ensamblador siguiendo la pila manualmente.
    Pero de pronto -fomit-frame-pointer se puso de moda, los rastros de pila empezaron a dejar de tener sentido y depurar segfaults o instrucciones ilegales se volvió mucho más difícil.
    Al final, para evitar sesiones de depuración rotas, terminé usando Python para casi todo; perdí uno o dos órdenes de magnitud en rendimiento, pero aprender Python me sirvió después.

    • Me pregunto si no conocía -fno-omit-frame-pointer.
  • Me alegra que se mencione Fedora. Mantener los punteros de marco activados en toda la distribución fue una batalla bastante agotadora.
    Ej.: https://pagure.io/fesco/issue/3084
    Persiste el mito de que el overhead de los punteros de marco es grande, y viene de un caso de Python donde había una ralentización de +10%, que ya fue corregido.
    El overhead medido en la práctica es menor al 1%, y las ventajas obtenidas en ciertas aplicaciones son mucho mayores.

    • La afirmación de que “el overhead medido en la práctica es menor al 1%” me parece exagerada. El overhead depende mucho de la carga de trabajo que se mida, así que es difícil aplicarlo de forma universal.
      Tampoco encaja muy bien con las mediciones del kernel de Linux, donde vi cifras en el rango de 5–10%: https://lore.kernel.org/lkml/20170602104048.jkkzssljsompjdwy...
      En cargas de trabajo variadas como netperf, microbenchmarks del asignador de páginas, pgbench y sqlite, activar los punteros de marco produjo un overhead de 5–10%, y es importante que se haya observado impacto en PostgreSQL y SQLite.
      Un DBMS es una de las buenas formas de ejercer mucha presión sobre un sistema.
    • Para usar bien los flame graphs en OCaml 5, hay que usar punteros de marco, o LBR, que tiene profundidad limitada y puede no funcionar según la CPU, o cambiar bastante perf/eBPF para manejar las dos pilas de OCaml.
      OCaml 5 usa pilas separadas para el código OCaml y el código C, y GDB puede conectarlas con información DWARF, pero los grafos de llamadas DWARF de perf no pueden hacerlo: https://github.com/ocaml/ocaml/issues/12563#issuecomment-193...
      Si se necesita una razón para mantener los punteros de marco en lanzamientos futuros, OCaml 5 puede servir como ejemplo.
      Recién ahora me entero de que Fedora 39 ya activó los punteros de marco por defecto; mi profiling habitual todavía lo hacía sobre todo en sistemas parecidos a CentOS 7, usando perf record --call-graph dwarf -F 47 -a.
    • En 32 bits, los punteros de marco siguen siendo problemáticos, así que no encajan en áreas actuales como IoT.
      La razón para eliminar los punteros de marco no fue un mito, sino una realidad de la época anterior a los 64 bits, y tampoco fue hace tanto.
      Incluso hoy, si se quiere darle nueva vida a sistemas antiguos de 64 bits, este tipo de optimización tiene sentido.
      Idealmente también debería ser el valor predeterminado en sistemas donde la seguridad es importante, y no todo tiene que optimizarse para la “observabilidad”.
    • Creo que esos overheads “menores al 1%” se van acumulando, y por eso la experiencia de usar una computadora se volvió mediblemente más lenta que hace 30 años. Esos pequeños overheads terminan sumándose.
  • Hay algo que Apple hizo bien en ARM: hizo que el puntero de marco x29 siempre apunte a un registro de marco válido.
    Algunas funciones, como las leaf functions o las tail calls, pueden no crear una entrada en la lista, pero el resultado es que, incluso sin información de depuración, el stack trace siempre sigue siendo significativo.
    https://developer.apple.com/documentation/xcode/writing-arm6...

    • En las plataformas de Apple suelen aparecer otros tipos de problemas de interpretabilidad. Las apps de Objective-C / Swift tienen muchos bloques y closures profundamente anidados, por lo que es común que el backtrace quede disperso entre varios threads, y todavía no conozco una buena solución.
  • En 2005 yo estaba del lado opuesto en Google, y mi razonamiento de entonces era simple.
    Aunque $BIG_COMPANY decidiera compilar todo con punteros de marco, el resto de la comunidad no lo haría, así que terminaríamos en una discusión imposible de ganar contra una comunidad mucho más grande.
    Al final, esa discusión se convirtió en una discusión de casi 20 años; después escribí parches para que libunwind funcionara en gperftools y terminé manteniendo libunwind durante varios años.
    Ahora me pasé a otra área de la computación y soy un observador pasivo, pero es interesante leer la historia desde la perspectiva del lado contrario.

    • Me pregunto de qué manera agregar punteros de marco puede terminar siendo una traba. No sé qué problema funcional provoca.
  • Si vas a pasar RBP como puntero de marco, también podrías tener dos pilas
    Una apuntada por RBP, que guarde los marcos de activación, y otra apuntada por RSP, que guarde solo las direcciones de retorno
    Entonces la pila de llamadas sería literalmente un arreglo plano de direcciones de retorno, así que ni siquiera haría falta “recorrer la pila”
    Para empezar, no entiendo por qué se guardan las direcciones de retorno cerca de las variables locales, y parece tener demasiadas desventajas

    • Porque simplifica la gestión del almacenamiento. Un marco de pila es un simple bump pointer que siempre está en caché, y para el overflow solo necesita una guard page
      El esquema que propones necesitaría dos guard pages, duplicaría la manipulación de la pila y también duplicaría la posibilidad de cache misses
    • La shadow stack, una función de CPU mencionada brevemente en el artículo, es bastante parecida a esto. Aunque su objetivo principal es la seguridad
    • También me pregunto por qué la pila crece en la “dirección equivocada” y hace que los programas que se comportan mal generen problemas de seguridad
      Sé la razón, pero como muchas cosas, solo tuvo sentido por última vez hace unos 30 años, y sus efectos fueron interesantes
    • Parece que ya estás listo para que te guste Forth ;-)
      Curiosamente, el artículo de Wikipedia no parece destacar bien como una gran característica del modelo que Forth pueda acceder tanto a la pila de parámetros como a la pila de retorno
      https://en.wikipedia.org/wiki/Forth_(programming_language)
    • La ventaja de guardar la dirección de retorno en otro lugar no es clara salvo que el hardware soporte algo como una shadow stack
      Habría que demostrar que el costo de moverla a una página separada y gestionar dos punteros es realmente menor que el de los stack cookies/protectors, que ya brindan protección solo donde hace falta
      Tampoco hay una ventaja de seguridad real frente a los protectores de pila actuales. Si se puede leer/escribir arbitrariamente, al final eso puede llevar a saltarse la integridad del flujo de control
  • Virgil no usa punteros de marco. Si no hay asignación dinámica en la pila, el tamaño del marco de una función específica es fijo y se puede encontrar con una simple búsqueda en una tabla binaria
    La técnica de Virgil usa además rangos basados en índices de página para acotar la búsqueda con solo unas pocas comparaciones en promedio, combina la información de unwinding con el stackmap para el GC y ocupa muy poco espacio
    El código principal está en https://github.com/titzer/virgil/blob/master/rt/native/Nativ... y el resto del código en el mismo directorio implementa la decodificación de metadatos
    Creo que los punteros de marco solo tienen sentido cuando el tamaño del marco es dinámico, es decir, cuando se asignan datos en la pila
    Es raro usar un mecanismo dinámico cuando un mecanismo estático sería suficiente, y parece que se debe sobre todo a que no hay acuerdo sobre una ABI para codificar metadatos o sobre las rutinas de unwinding
    La medición de 1–2% es creíble y está a un nivel similar al costo de las comprobaciones de límites de arreglos
    Darle un trato especial al costo del 1% para depuración y profiling, pero mostrarse reacio a agregar capas de seguridad, indica unas prioridades muy extrañas

    • En C también se pueden agregar comprobaciones de límites, pero cuestan mucho más que 1–2%
      std::vector de C++ desactiva por defecto las comprobaciones de límites, y creo que eso es porque C++ fue diseñado por gente completamente loca y para gente igual
      Fuera de eso, no se me ocurre de inmediato ningún lenguaje que no tenga comprobaciones de límites
  • Buen artículo. Me dio pena cuando desaparecieron los punteros de marco
    En Linux, igual que en otros sistemas, mucha gente sufrió durante mucho tiempo por la ausencia de punteros de marco e intentó conservarlos en tantos entornos como fuera posible
    Ver que el Linux mainstream los trae de vuelta se siente como un reconocimiento, pero al mismo tiempo también es un poco frustrante

    • Pregunta sincera: sé que hacer unwinding de la pila con DWARF es molesto, pero no entiendo por qué es tan malo como para justificar volver más lento todo el código del sistema
      En derivados de Debian es lento porque, por razones de licencia, perf solo empaqueta la ruta lenta de unwinding, pero cuando las herramientas son buenas casi no se nota la diferencia
      Me pregunto qué me estoy perdiendo
  • En general estoy a favor de los frame pointers, pero después de trabajar varios años en esta área tengo algunas observaciones.
    Muchos desenrollados de pila basados en frame pointers no consideran problemas que sí cubre la información de desenrollado DWARF. La configuración del frame no es atómica: se compone de dos instrucciones, push $rbp y mov $rsp $rbp, y si el snapshot se toma durante el push, se puede perder el frame padre.
    Quizá podría mitigarse inspeccionando el código, pero también puede haber push %rbp sin relación con un stack frame, así que parece más bien una heurística.
    También desarrollé la solución rápida de desenrollado dentro del kernel basada en BPF que mencionó Brendan: https://web.archive.org/web/20231222054207/https://www.polar...
    Este enfoque no usa DWARF CFI tal cual, sino que lo convierte a un formato de acceso aleatorio usable desde BPF.
    Actualmente solo soporta secciones JIT con frame pointers, pero creo que sería posible implementar el desenrollado del intérprete de la JVM para que encaje con el desenrollado nativo.
    Idealmente, la activación de frame pointers debería hacerse caso por caso, y el benchmarking es clave.
    Según la industria y la naturaleza del software, los compromisos entre rendimiento, observabilidad e indicadores de negocio pueden variar mucho.
    El equipo de Fedora hizo un trabajo muy bueno y riguroso en este tema.
    Además, un sistema de compilación que permita cambiar esta configuración en todo el sistema, incluidas las bibliotecas dependientes, es muy útil no solo para pruebas sino también para llevarlo a producción.
    Por último, tengo expectativas sobre SFrame, en el que está trabajando Indu. Parece que resolverá muchos de los problemas actuales, dejando que los usuarios elijan si usar frame pointers o no, aunque puede tomar varios años hasta que la infraestructura esté lista y todos actualicen.

    • Para obtener buenos flame graphs, hay que activar los frame pointers en toda la distribución Linux.
      Para entender qué ocurre realmente se necesita análisis de todo el sistema, y con la estructura actual de distribuciones Linux binarias como Fedora o Debian, prácticamente no hay otra alternativa.
    • También es posible con una sola instrucción: ENTER N,0 reserva N de espacio en la pila para variables locales y equivale aproximadamente a PUSH EBP, MOV ESP,ESP, SUB SP,N.
      Aunque no recuerdo si ENTER existe en x86-64.
      Aun así, entre CALL y la configuración del frame no es atómico, así que si el snapshot se toma después de CALL y antes de ENTER, no se obtiene la configuración del frame.
      La razón por la que ENTER casi no se usa es que se consideró demasiado lenta.
      LEAVE se usa porque es igual de rápida o más rápida que la secuencia de instrucciones que reemplaza, pero ENTER tiene un segundo operando que arruina el rendimiento.
      Ese operando sirve para que funciones anidadas accedan a stack frames superiores, y su costo de uso es muy alto.
    • La configuración no atómica del frame es sin duda un problema para los perfiladores de CPU, pero no lo es para el perfilado de asignaciones, el perfilado Off-CPU ni otros tipos de perfilado que no se basan en interrupciones.
    • También hay buenos avances en el desenrollado para la JVM.
  • Me parece interesante entender por fin de dónde venían esas montañas de [unknown] que aparecían en los perfiles.
    Aun así, no es fácil justificarlo. Una diferencia de rendimiento del 2% en realidad es bastante grande.
    Sería bueno poder controlar con más granularidad si se incluyen frame pointers o no.
    Con un perfilado detallado, se podría decidir si una función o unidad de compilación específica necesita frame pointers.
    No me sorprendería que el resultado fuera que solo unas pocas tareas se vuelven dramáticamente más lentas al incluir frame pointers y que el resto casi no se ve afectado.

    • Una diferencia de rendimiento del 2% no es tan grande, especialmente si el perfilado permite encontrar cuellos de botella y lograr mejoras de más del 10%.
    • En GCC se puede activar o desactivar por función agregando atributos a la declaración de la función. Sin embargo, en LLVM no funciona.
      __attribute__((optimize("no-omit-frame-pointer")))
      __attribute__((optimize("omit-frame-pointer")))
    • En aplicaciones reales, el costo de rendimiento puede ser mucho menor que 2%.
      Este tipo de benchmarks son algo artificiales y no hay que creerlos por completo; las aplicaciones reales suelen arrojar resultados muy distintos.
      El perfilado es importante, y he llegado a acelerar varias secciones hasta un 20% perfilando el código con cuidado.
      Si una aplicación es muy sensible a la pérdida de rendimiento, se pueden activar los frame pointers y perfilarla en el laboratorio, y luego omitirlos en la versión que se distribuye a los clientes.
    • El overhead medido está un poco por debajo del 1%. En el pasado hubo casos raros en los que los frame pointers perjudicaron mucho el rendimiento, pero ya fueron corregidos.
    • Normalmente es mucho menor que 2%.
  • Lamentablemente, el soporte para código JIT no es bueno, pero LLVM tiene excelentes hooks para registrar cada método generado y su dirección.
    Por eso es relativamente fácil crear un desenrollado de pila en modo mixto simple, aunque principalmente dentro del propio proceso.
    La familia DNN de Intel parece volcar la información a un archivo público que perf puede leer, pero los kernels de oneDNN reutilizan constantemente RBP, así que en la práctica no sirve.
    También me sorprende la afirmación del artículo de que “los runtimes JIT como la JVM de Java no tienen información DWARF”.
    Me pregunto si está desactivada por defecto o si literalmente no está disponible.
    Al buscar, casi siempre se llega a temas sobre querer incluir la parte JNI/C en las pilas de la JVM: https://github.com/async-profiler/async-profiler/issues/215