3 pontos por GN⁺ 2024-03-18 | 1 comentários | Compartilhar no WhatsApp
  • Fedora e Ubuntu 24.04 LTS voltaram a compilar componentes do sistema operacional, como a libc, com frame pointers incluídos por padrão, permitindo que o profiling no Linux e os Flame Graphs enxerguem stacks mais completos
  • Sem frame pointers, o profiler pode parar o stack walking em camadas de biblioteca como a libc ou seguir frames incorretos, quebrando Flame Graphs de CPU e Off-CPU
  • A mudança do gcc para i386 em 2004 foi uma escolha para obter mais um registrador, mas acabou se espalhando para o x86-64 e deixando um custo de longo prazo para profilers de sistema e observabilidade baseada em eBPF
  • Na experiência da Netflix com Java e libc, o custo normalmente foi inferior a 1%; alguns relatos ficaram entre 1% e 2%, e microbenchmarks especiais ou workloads anormais chegaram a 10%
  • Há alternativas como LBR, DWARF, stack walker em eBPF, ORC, SFrames e Shadow Stacks, mas a forma mais prática hoje para encontrar melhorias de desempenho em produção é ativar frame pointers por padrão

Por que os Flame Graphs quebram

  • Um CPU Flame Graph pode parecer normal à primeira vista, mas em sistemas onde a libc foi compilada sem frame pointers, algumas amostras podem acabar agrupadas incorretamente acima de [unknown]
    • No exemplo, 15% das amostras à esquerda estão no lugar errado e faltam frames da aplicação
    • O profiler passa pelos frames do kernel, alcança a syscall e o wrapper de syscall da libc, e então falha ao resolver o símbolo do frame seguinte
  • A causa é a otimização do compilador, em que o registrador de frame pointer é usado para armazenar dados em vez de servir como ponto de referência do stack frame
    • O profiler não tem como saber se aquele valor é apenas um número e tenta interpretá-lo como endereço de função
    • Se o valor não apontar para o frame seguinte, o stack walking é interrompido
    • Se por acaso parecer um ponteiro válido, podem surgir junk frames incorretos
    • Se o valor apontar para si mesmo, pode surgir uma pilha de junk frames até atingir o limite máximo de frames do perf
  • Off-CPU Flame Graphs podem quebrar ainda mais sem frame pointers, porque funções libc de read/write e mutex aparecem com frequência
  • Se a própria aplicação também for compilada sem frame pointers, não são só as bibliotecas: todo o rastreamento de stack fica comprometido

Que informação os frame pointers fornecem

  • A ABI x86-64 define que %rbp pode ser usado como base pointer do stack frame, isto é, como frame pointer
  • Profilers e depuradores externos, como Linux perf e eBPF, usam essa informação para percorrer o stack trace e visualizar o resultado em Flame Graphs
  • Há uma nota de rodapé na ABI x86-64 dizendo que o uso de %rbp é opcional
    • Indexar o stack frame com %rsp pode economizar duas instruções no prologue e no epilogue
    • %rbp pode ser usado como um registrador adicional de propósito geral

A remoção em 2004 e 20 anos de efeitos

  • Em 2004, o gcc passou a ter no backend i386 um comportamento equivalente a -fomit-frame-pointer -ffixed-ebp por padrão
  • O i386 tinha apenas 4 registradores de propósito geral, então liberar %ebp aumentava bastante o número de registradores disponíveis
    • Entre as razões da mudança estava também o desejo de ter desempenho melhor do que o compilador icc da Intel
    • Na época, considerou-se que os debuggers não quebrariam, porque já suportavam outras técnicas de stack walking
  • Essa mudança também foi aplicada ao x86-64
    • O x86-64 tem mais de 12 registradores, então o benefício de ganhar mais um não é tão grande quanto no i386
    • Profilers de sistema atuais, como os baseados em eBPF, não existiam na época e podem quebrar por causa dessa decisão
  • Em 2004, Eric Schrock avaliou que o benefício de obter o 17º registrador de propósito geral no amd64 não compensava a perda de depurabilidade
    • Ele comentou que, uma vez que se começa a compilar /usr/bin sem frame pointers, a situação sai do controle
    • No Linux, isso se espalhou não só por /usr/bin, mas também por /usr/lib e pelo código das aplicações

Java, libc e a experiência de uso na Netflix

  • Quando Brendan Gregg entrou na Netflix em 2014, toda a stack da aplicação estava quebrada por falta de suporte a frame pointers no Java
  • Foi criado um patch para o compilador c2 da JVM, depois retrabalhado pela Oracle e incorporado como a opção -XX:+PreserveFramePointer no JDK8u60
  • A mudança no Java permitiu encontrar várias melhorias de desempenho no código da aplicação, mas a libc ainda quebrava parte das amostras de CPU e a maioria dos Off-CPU Flame Graphs
  • Depois disso, a empresa passou a compilar internamente uma libc com frame pointers para produção e também trabalhou com a Canonical em uma libc pré-compilada para Ubuntu
    • Por algum tempo, recomendou-se o uso de libc6-prof
    • libc6-prof era uma libc6 com frame pointers incluídos

Overhead de desempenho e casos excepcionais

  • Na adoção em produção, o custo de adicionar frame pointers à libc e ao Java normalmente ficou abaixo de 1%
  • Houve uma aplicação excepcional com overhead de 10%
    • Era uma aplicação incomum que gerava stack traces com mais de 1000 frames via Groovy
    • O Linux perf também não conseguia lidar com isso, e Arnaldo Carvalho de Melo, da Red Hat, adicionou o sysctl kernel.perf_event_max_stack para o workload da Netflix
    • O ambiente era uma máquina virtual sem recursos de profiling de hardware de baixo nível, então não foi possível confirmar se os 10% vinham inteiramente dos frame pointers
  • Outros relatos apontaram custo em torno de 1% ou 2%
  • Microbenchmarks podem piorar até 10%
    • Ao executar repetidamente funções pequenas em loop, instruções extras podem afetar o aquecimento do cache L1 ou os limites de cache line
    • Nesses casos, o efeito pode surgir não exatamente por causa dos frame pointers, mas por acrescentar qualquer coisa a uma função hot
  • O benchmark Python scimark_sparse_mat_mult também podia chegar a 10%
    • Na análise de Andrii Nakryiko, tratava-se de um caso especial em que, numa função grande, o gcc usava offsets relativos a %rbp em vez de offsets a partir de %rsp, exigindo mais bytes e causando o problema de desempenho
    • Depois houve notícia de uma correção para permitir que o Python reativasse frame pointers por padrão
  • Melhorias de desempenho encontradas graças aos frame pointers variaram de 5% a 500%, e o custo normalmente inferior a 1% tende a ser justificável
  • Em dispositivos sem necessidade de profiling ou debugging, compilar sem frame pointers pode ser aceitável, mas o foco principal aqui é Linux corporativo e servidores de backend

Fedora e Ubuntu ativam por padrão

  • Grandes empresas como Meta, Google e Netflix já usavam internamente uma libc com frame pointers para garantir capacidade de profiling
  • A primeira tentativa de levar essa ativação padrão upstream para o Fedora gerou uma longa discussão
    • A discussão no Fedora virou uma thread com 116 mensagens
    • Um participante exigiu que Meta ou Netflix fornecessem infraestrutura de side repository para testes, benchmarks e medição de tamanho de código
    • Jonathan Corbet resumiu isso em Fedora's tempest in a stack frame
  • Mais tarde, o Fedora revisitou a proposta e a aceitou, tornando-se a primeira distribuição a reativar frame pointers
  • O Ubuntu também anunciou frame pointers por padrão no Ubuntu 24.04 LTS
  • Também foi acrescentada a informação de que o Arch Linux está no processo de ativar frame pointers
  • O stack walking nas bibliotecas do sistema operacional melhora com essa mudança, mas runtimes de aplicação ainda podem exigir configuração separada
    • O Java oferece a opção -XX:+PreserveFramePointer
    • O Go passou a ter suporte a frame pointers ativado por padrão há alguns anos

Candidatos para stack walking após os frame pointers

  • LBR (Last Branch Record): recurso de hardware da Intel com limite de 16 ou 32 frames; é insuficiente para a maioria das stacks de aplicação, mas pode servir como último recurso para obter parte das informações de stack
  • BTS (Branch Trace Store): recurso da Intel com menos limitação de profundidade de stack, mas com custo de operações de load/store em memória e do tratamento de interrupções por overflow do buffer BTS
  • AET (Architectural Event Trace): tracer baseado em JTAG capaz de rastrear eventos de baixo nível de CPU, BIOS e dispositivos, e aparentemente também utilizável para stack traces, embora o autor não tenha experiência prática direta com ele
  • DWARF: formato de debuginfo binário usado há muito tempo em debuggers
    • Foi apontado que já houve trabalho de JIT para DWARF em runtimes JIT
    • Não parece prático para uma JVM Java de produção muito ocupada, com c2 rodando continuamente
    • O próprio stack walking via DWARF também tem custo alto
  • eBPF stack walking: um tracer externo pode percorrer o interior de runtimes como a JVM sem apoio explícito do runtime
    • Isso pode exigir muitas leituras de user space do interior do runtime, elevando o overhead
    • Como é frágil a mudanças do runtime, o ideal é ser distribuído e mantido junto com o code base da linguagem
  • ORC (oops rewind capability): unwinder de stack leve do kernel Linux, que permite manter stack walking mesmo com a remoção de frame pointers nos kernels mais novos
  • SFrames (Stack Frames): método leve de unwinding de stack de usuário baseado em ORC
  • Shadow Stacks: recurso de segurança da Intel e da AMD em que o endereço de retorno de uma função é empilhado em uma stack de hardware separada e validado no retorno, além de parecer útil também para stack trace

O que muda agora

  • As razões de 2004 para omitir frame pointers — vantagem de desempenho no i386, avaliação de compatibilidade com debuggers da época e competição com o icc — não se encaixam mais no cenário de 2024
  • Mesmo na época, a remoção de frame pointers aplicada ao x86-64 já era vista por alguns como um ganho insuficiente diante da perda de capacidade de debugging
  • Com Fedora e Ubuntu revertendo essa decisão, quem usa releases de 2024 passa a ter CPU Flame Graphs mais fáceis de entender e pode aproveitar Off-CPU Flame Graphs corretamente pela primeira vez
  • Também fica mais fácil para profilers contínuos obter perfis mais completos sem exigir que clientes alterem o sistema operacional
  • No futuro, talvez SFrames ou Shadow Stacks permitam voltar a fazer stack tracing sem frame pointers, mas a melhoria prática disponível agora é ativá-los por padrão

1 comentários

 
GN⁺ 2024-03-18
Comentários do Hacker News
  • Lembro quando, no começo dos anos 2000, a omissão do ponteiro de quadro da pilha começou a se espalhar
    Na época, eu estudava ciência da computação em uma universidade de um país pobre do terceiro mundo e, por causa dos computadores velhos e lentos, a maioria dos trabalhos usava compiladores em vez de interpretadores
    Havia muitas aulas interessantes em que implementávamos estruturas de dados de baixo nível, compiladores, rotinas numéricas em assembly e até drivers de dispositivo do Minix; quando um programa se comportava de forma estranha, eu anexava o gdb e depurava no nível de assembly, seguindo a pilha manualmente
    Mas, de repente, -fomit-frame-pointer virou moda, e os rastreamentos de pilha começaram a deixar de fazer sentido; depurar segfaults ou illegal instructions ficou muito mais difícil
    No fim, para evitar sessões de depuração quebradas, passei a usar Python para quase tudo; perdi uma ou duas ordens de grandeza em desempenho, mas aprender Python acabou sendo útil depois

    • Fico me perguntando se você não conhecia -fno-omit-frame-pointer
  • Fico feliz que Fedora tenha sido mencionado. Manter ponteiros de quadro ativados em toda a distribuição foi uma batalha bem cansativa
    Ex.: https://pagure.io/fesco/issue/3084
    Continua existindo o mito de que o overhead dos ponteiros de quadro é grande, por causa de um caso em Python que ficou +10% mais lento, mas isso já foi corrigido
    O overhead realmente medido é inferior a 1%, e os benefícios obtidos em certos aplicativos são muito maiores

    • A afirmação de que “o overhead realmente medido é inferior a 1%” parece exagerada. O overhead depende muito da carga de trabalho medida, então é difícil aplicar isso de forma universal
      Também não bate muito com as medições do lado do kernel Linux, onde vi algo na faixa de 5~10%: https://lore.kernel.org/lkml/20170602104048.jkkzssljsompjdwy...
      Em várias cargas de trabalho, como netperf, microbenchmarks do alocador de páginas, pgbench e sqlite, ativar ponteiros de quadro gerou overhead de 5~10%; é importante notar que houve impacto observado em PostgreSQL e SQLite
      Um DBMS é uma das boas maneiras de pressionar fortemente o sistema
    • Para usar flame graphs corretamente no OCaml 5, é preciso usar ponteiros de quadro, ou LBR, que tem profundidade limitada e pode não funcionar dependendo da CPU, ou então alterar bastante o lado de perf/eBPF para lidar com as duas pilhas do OCaml
      O OCaml 5 usa pilhas separadas para código OCaml e código C; o GDB consegue conectá-las usando informações DWARF, mas o grafo de chamadas DWARF do perf não consegue: https://github.com/ocaml/ocaml/issues/12563#issuecomment-193...
      Se for preciso justificar a manutenção dos ponteiros de quadro em lançamentos futuros, o OCaml 5 pode servir de exemplo
      Eu só agora soube que o Fedora 39 já ativou ponteiros de quadro por padrão; minha profilagem cotidiana ainda era feita principalmente em sistemas parecidos com CentOS 7, usando perf record --call-graph dwarf -F 47 -a
    • Em 32 bits, ponteiros de quadro ainda são problemáticos, então não se encaixam em áreas como IoT hoje em dia
      O motivo para remover ponteiros de quadro não veio de um mito, mas da realidade da era anterior aos 64 bits, e isso nem foi há tanto tempo assim
      Mesmo hoje, para dar vida nova a sistemas 64 bits antigos, esse tipo de otimização faz sentido
      Idealmente, também deveria ser o padrão em sistemas em que segurança é importante, e nem tudo precisa ser otimizado para “observabilidade”
    • Acho que overheads “inferiores a 1%” se acumularam e tornaram a experiência de usar computadores mensuravelmente mais lenta do que há 30 anos. Esses pequenos overheads acabam se somando
  • Há uma coisa que a Apple fez bem no ARM: garantir que o ponteiro de quadro x29 sempre aponte para um registro de quadro válido
    Algumas funções, como leaf functions ou tail calls, podem não criar uma entrada na lista, mas, como resultado, mesmo sem informações de depuração, o rastreamento de pilha sempre continua significativo
    https://developer.apple.com/documentation/xcode/writing-arm6...

    • Nas plataformas Apple, há com frequência outros tipos de problemas de interpretabilidade. Apps em Objective-C / Swift costumam ter muitos blocos e closures profundamente aninhados, então é comum o backtrace ficar espalhado por várias threads, e ainda não conheço uma boa solução
  • Em 2005, eu estava do outro lado dentro do Google, e meu raciocínio na época era simples
    Mesmo que a $BIG_COMPANY decidisse compilar tudo com ponteiros de quadro, o restante da comunidade não faria isso, então acabaríamos presos em uma discussão impossível de vencer contra uma comunidade muito maior
    No fim, essa discussão virou praticamente uma discussão de 20 anos; depois acabei escrevendo patches para fazer libunwind funcionar no gperftools e mantive libunwind por alguns anos
    Agora migrei para outra área da computação e sou apenas um observador passivo, mas é interessante ler a história do ponto de vista do outro lado

    • Fico curioso sobre de que forma adicionar ponteiros de quadro atrapalha. Não sei que problema funcional isso causaria
  • Se vamos passar o RBP como ponteiro de frame, talvez seja até possível manter duas pilhas
    Uma apontada pelo RBP, armazenando os frames de ativação, e outra apontada pelo RSP, armazenando apenas os endereços de retorno
    Assim, a pilha de chamadas seria literalmente um array plano de endereços de retorno, então nem seria preciso “percorrer a pilha”
    Para começo de conversa, não sei por que o endereço de retorno é armazenado perto das variáveis locais, e as desvantagens parecem numerosas demais

    • É porque isso simplifica o gerenciamento de armazenamento. Um frame de pilha é um bump pointer simples que está sempre no cache, e para overflow só precisa de uma guard page
      A abordagem proposta precisaria de duas guard pages, dobraria a manipulação da pilha e também dobraria a chance de cache miss
    • O recurso de CPU mencionado brevemente no texto, a pilha sombra, é bem parecido com isso. Só que o objetivo principal é segurança
    • Também me pergunto por que a pilha cresce na “direção errada” e, por isso, programas que se comportam incorretamente acabam causando problemas de segurança
      Sei o motivo, mas, como muita coisa, ele provavelmente só fez sentido pela última vez uns 30 anos atrás, e o efeito disso foi interessante
    • Parece que você está pronto para gostar de Forth ;-)
      Estranhamente, o artigo da Wikipedia não parece destacar bem, como uma grande característica do modelo, o fato de Forth poder acessar tanto a pilha de parâmetros quanto a pilha de retorno
      https://en.wikipedia.org/wiki/Forth_(programming_language)
    • O benefício de armazenar endereços de retorno em outro lugar não é claro, a menos que o hardware dê suporte a algo como pilha sombra
      Seria preciso mostrar que o custo de movê-los para uma página separada e gerenciar dois ponteiros é, na prática, mais barato do que stack cookies/protetores de pilha, que já oferecem proteção apenas onde ela é necessária
      Também não há uma vantagem real de segurança em relação aos protetores de pilha atuais. Se leitura/escrita arbitrária for possível, no fim isso ainda pode levar a um bypass da integridade do fluxo de controle
  • Virgil não usa ponteiro de frame. Sem alocação dinâmica na pilha, o tamanho do frame de uma função específica é fixo e pode ser encontrado com uma simples busca em uma tabela binária
    A técnica do Virgil usa adicionalmente intervalos baseados em índices de página para restringir a busca, em média, com apenas algumas comparações, combina informações de unwind com stackmaps para o GC e usa pouquíssimo espaço
    O código principal está em https://github.com/titzer/virgil/blob/master/rt/native/Nativ..., e o restante do código no mesmo diretório implementa a decodificação dos metadados
    Acho que ponteiros de frame só fazem sentido quando o tamanho do frame é dinâmico, isto é, quando se alocam dados na pilha
    Usar um mecanismo dinâmico quando um mecanismo estático é suficiente é estranho, e parece acontecer principalmente por falta de acordo sobre uma ABI de codificação de metadados ou sobre rotinas de unwind
    A medição de 1–2% é plausível e fica em um nível parecido com o custo de verificação de limites de arrays
    Dar tratamento especial a um custo de 1% para depuração e profiling, mas rejeitar a adição de camadas de segurança, é uma prioridade muito estranha

    • Em C também é possível adicionar verificação de limites, mas isso custa muito mais do que 1–2%
      O std::vector do C++ desativa a verificação de limites por padrão, porque, na minha opinião, C++ foi projetado por pessoas completamente malucas e para pessoas assim
      Fora isso, não me vem à cabeça de imediato uma linguagem que não tenha verificação de limites
  • Ótimo texto. Fiquei triste quando os ponteiros de frame desapareceram
    Em muitos sistemas, e também no Linux, muita gente sofreu por muito tempo por causa da ausência de ponteiros de frame, e tentou preservá-los no maior número possível de ambientes
    Ver o Linux mainstream trazê-los de volta dá uma sensação de reconhecimento, mas ao mesmo tempo é um pouco frustrante

    • Tenho curiosidade genuína. Sei que fazer unwind da pilha com DWARF é incômodo, mas não entendo por que isso é tão ruim a ponto de justificar deixar o código de todo o sistema mais lento
      Em derivados do Debian, ele é lento porque, por motivos de licença, o perf só empacota o caminho de unwind lento; mas, quando a ferramenta é decente, quase não percebo diferença
      Fico curioso para saber o que estou deixando passar
  • No geral, sou a favor de frame pointers, mas há algo que percebi trabalhando alguns anos nessa área.
    Muitos unwinds de pilha baseados em frame pointer não levam em conta um problema que não existe nas informações de unwind do DWARF. A configuração do frame não é atômica e é feita com duas instruções, push $rbp e mov $rsp $rbp; se o snapshot for tirado durante o push, o frame pai pode ser perdido.
    Talvez seja possível mitigar isso inspecionando o código, mas também pode haver um push %rbp sem relação com o stack frame, então parece mais uma heurística.
    Também desenvolvemos a solução de unwind rápida dentro do kernel, baseada em BPF, que Brendan mencionou: https://web.archive.org/web/20231222054207/https://www.polar...
    Essa abordagem não usa o DWARF CFI diretamente; ela o converte para um formato de acesso aleatório que pode ser usado em BPF.
    Atualmente, ela só dá suporte a seções JIT que têm frame pointer, mas acredito que seja possível implementar o unwind do interpretador da JVM de modo integrado ao unwind nativo.
    Idealmente, a ativação de frame pointers deveria ser feita caso a caso, e benchmarking é essencial.
    Dependendo do setor e da natureza do software, os trade-offs entre desempenho, observabilidade e métricas de negócio podem variar muito.
    O pessoal do Fedora fez um trabalho muito bom e rigoroso aqui.
    Além disso, um sistema de build que permita alterar essa configuração no sistema inteiro, incluindo bibliotecas dependentes, é muito útil não só para testes, mas também para adoção em produção.
    Por fim, estou ansioso pelo SFrame, no qual a Indu está trabalhando. Parece que ele resolverá muitos dos problemas atuais, permitindo que o usuário escolha se quer usar frame pointers ou não, mas pode levar alguns anos até a infraestrutura estar pronta e todo mundo fazer upgrade.

    • Para obter bons flame graphs, é preciso ativar frame pointers em toda a distribuição Linux.
      Para entender o que realmente está acontecendo, é necessária uma análise do sistema inteiro e, na estrutura atual das distribuições Linux binárias como Fedora ou Debian, praticamente não há outra alternativa.
    • Também dá para fazer com uma única instrução: ENTER N,0 reserva N de espaço na pilha para variáveis locais e é aproximadamente equivalente a PUSH EBP, MOV ESP,ESP, SUB SP,N.
      Só não me lembro se ENTER existe em x86-64.
      Ainda assim, entre o CALL e a configuração do frame, isso não é atômico; se o snapshot for tirado depois do CALL e antes do ENTER, você não obtém a configuração do frame.
      O motivo de ENTER não ser muito usado é que ele foi considerado lento demais.
      LEAVE é usado porque é tão rápido quanto, ou mais rápido que, a sequência de instruções que substitui, mas ENTER tem o desempenho arruinado pelo segundo operando.
      Esse operando serve para que funções aninhadas acessem stack frames superiores, e o custo de usá-lo é muito alto.
    • A configuração de frame não atômica certamente é um problema para profilers de CPU, mas não é problema para profiling de alocação, profiling Off-CPU e outros tipos de profiling que não se baseiam em interrupções.
    • Também há bom progresso no lado do unwind da JVM.
  • É interessante finalmente entender por que apareciam aquelas montanhas de [unknown] nos perfis.
    Ainda assim, não é fácil justificar. Uma diferença de desempenho de 2% é, na prática, uma diferença bem grande.
    Seria bom ter um controle mais granular sobre incluir ou não frame pointers.
    Com profiling granular, daria para decidir se uma função específica ou uma unidade de compilação precisa de frame pointer.
    Eu não ficaria surpreso se os resultados mostrassem que só um pequeno número de tarefas fica dramaticamente mais lento ao incluir frame pointers, enquanto o restante quase não é afetado.

    • Uma diferença de desempenho de 2% não é tão grande assim. Especialmente se, com profiling, você puder encontrar gargalos e obter melhorias de mais de 10%.
    • No GCC, é possível ativar e desativar por função adicionando atributos à declaração da função. No LLVM, porém, isso não funciona.
      __attribute__((optimize("no-omit-frame-pointer")))
      __attribute__((optimize("omit-frame-pointer")))
    • Em aplicações reais, o custo de desempenho pode ser muito menor que 2%.
      Esse tipo de benchmark é um tanto artificial, então não se deve confiar totalmente nele, e aplicações reais muitas vezes apresentam resultados bem diferentes.
      Profiling é importante; já fiz profiling cuidadoso de código e acelerei vários trechos em até 20%.
      Se a aplicação for muito sensível a perda de desempenho, basta ativar frame pointers e fazer profiling no laboratório e, na versão distribuída aos clientes, omiti-los.
    • O overhead medido é um pouco menor que 1%. No passado houve casos raros em que frame pointers prejudicavam bastante o desempenho, mas isso já foi corrigido.
    • Normalmente é muito menor que 2%.
  • Infelizmente, o suporte a código JIT não é bom, mas o LLVM tem um ótimo hook para registrar cada método gerado e seu endereço.
    Com isso, é relativamente fácil criar um unwind de pilha em modo misto simples, mas isso é possível principalmente de dentro do próprio processo.
    A família DNN da Intel parece despejar as informações em um arquivo público que o perf consegue ler, mas os próprios kernels do oneDNN ficam reutilizando o RBP, então isso acaba sendo praticamente inútil.
    A afirmação do texto de que “runtimes JIT como a Java JVM não têm informações DWARF” também surpreende.
    Fico me perguntando se isso vem desativado por padrão ou se literalmente não está disponível.
    Ao pesquisar, geralmente se chega a conteúdos sobre querer incluir o lado JNI/C nas pilhas da JVM: https://github.com/async-profiler/async-profiler/issues/215