30 pontos por GN⁺ 2026-01-15 | 5 comentários | Compartilhar no WhatsApp
  • ThreadMXBean.getCurrentThreadUserTime() do OpenJDK foi substituído de parsing de arquivo em /proc por chamada a clock_gettime(), alcançando até 400x de ganho de desempenho
  • A implementação anterior passava por um caminho complexo de I/O, abrindo, lendo e fazendo parsing do arquivo /proc/self/task/<tid>/stat
  • A nova implementação aproveita a codificação em bits de clockid_t do kernel Linux para ajustar os bits inferiores do ID obtido por pthread_getcpuclockid(), permitindo consultar diretamente apenas o tempo de usuário
  • Nos benchmarks, o tempo médio por chamada caiu de 11μs para 279ns e, depois, com a aplicação do fast-path do kernel, houve cerca de 13% de melhoria adicional
  • É um caso que mostra como é possível otimizar indo além das restrições do POSIX, por meio da compreensão da ABI interna do Linux

Problemas da implementação anterior

  • getCurrentThreadUserTime() abria o arquivo /proc/self/task/<tid>/stat e fazia o parsing do 13º e 14º campos para calcular o tempo de CPU em modo usuário
    • Era necessário passar por múltiplas etapas, como criar o caminho do arquivo, abrir o arquivo, ler o buffer, fazer parsing da string e chamar sscanf()
    • Como o nome do comando pode conter parênteses, havia também uma lógica complexa com strrchr() para localizar o último )
  • Em contrapartida, getCurrentThreadCpuTime() fazia apenas uma única chamada a clock_gettime(CLOCK_THREAD_CPUTIME_ID)
  • Segundo o bug report de 2018 (JDK-8210452), a diferença de velocidade entre os dois métodos chegava a 30x a 400x

Comparação entre o caminho de acesso via /proc e o caminho com clock_gettime()

  • O método via /proc incluía várias chamadas de sistema e geração de strings dentro do kernel, como open(), read(), sscanf() e close()
  • O método com clock_gettime() lia diretamente o valor de tempo da estrutura sched_entity com uma única chamada de sistema
  • Sob carga paralela, o acesso a /proc sofria atrasos maiores por causa da contenção de locks no kernel

Nova forma de implementação

  • O padrão POSIX define que CLOCK_THREAD_CPUTIME_ID deve retornar o tempo de usuário + sistema
  • O kernel Linux codifica o tipo de relógio nos bits inferiores de clockid_t
    • 00=PROF, 01=VIRT (apenas usuário), 10=SCHED (usuário + sistema)
  • Ao trocar os bits inferiores do clockid obtido por pthread_getcpuclockid() para 01, é possível convertê-lo em um relógio exclusivo de tempo de usuário
  • No novo código, o I/O de arquivo e o parsing foram removidos, e o tempo de usuário passa a ser retornado apenas com uma chamada a clock_gettime()

Resultados de medição de desempenho

  • Antes da correção, o tempo médio por chamada era de 11,186μs; depois da correção, caiu para 0,279μs, uma melhora de cerca de 40x
    • Medido em um ambiente com 16 threads, em linha com a faixa originalmente reportada de 30x a 400x
  • No perfil de CPU, desaparecem as chamadas de sistema relacionadas a abertura e fechamento de arquivos, restando apenas uma única chamada a clock_gettime()

Otimização adicional com o fast-path do kernel

  • O kernel oferece um fast-path que acessa diretamente a thread atual quando o clockid contém PID=0 codificado
  • Se a JVM montar diretamente esse clockid em vez de usar pthread_getcpuclockid(), é possível pular a busca na radix tree
  • Com clockid montado manualmente, o tempo médio caiu de 81,7ns para 70,8ns, cerca de 13% de melhoria adicional
  • No entanto, como isso depende de detalhes internos da implementação do kernel, como o tamanho de clockid_t, há preocupação com perda de legibilidade e compatibilidade

Conclusão e lições

  • A remoção de 40 linhas eliminou uma diferença de desempenho de 400x, sem novos recursos do kernel, apenas aproveitando detalhes da ABI existente
  • Reforça o valor de estudar o código-fonte do kernel: o POSIX garante portabilidade, mas o código do kernel mostra os limites do que é possível
  • Destaca também a importância de revisar premissas antigas: fazer parsing de /proc fazia sentido no passado, mas hoje é ineficiente
  • Essa mudança será incluída no JDK 26 (previsto para março de 2026), trazendo ganho automático de desempenho nas chamadas de ThreadMXBean.getCurrentThreadUserTime()

5 comentários

 
crawler 2026-01-15

Impressionante.

Se ficou 2x mais rápido, pode ter sido algo inteligente; se ficou 100x mais rápido, foi só parar de fazer algo idiota.

Acho que não é totalmente falso, mas, quando envolve o kernel, imagino que perceber que estava lento já deve ter sido muito difícil.

 
[Este comentário foi ocultado.]
 
princox 2026-01-19

Como será que dá para descobrir esse tipo de coisa em um projeto? Acho que seria difícil perceber só rodando IA...

Quando vejo casos assim, penso que eu também quero aprender e viver esse tipo de experiência com certeza.

 
aobamisaki 2026-01-15

Na verdade, já é difícil conseguir uma melhora de 2 a 3 vezes refazendo o código inteiro, então obter até 400 vezes de ganho de desempenho simplesmente mudando algumas linhas é realmente impressionante.

 
GN⁺ 2026-01-15
Comentários do Hacker News
  • Sou o autor. Depois do post anterior sobre um bug no kernel, fui investigar como a JVM reporta a atividade das threads por conta própria
    Descobri que a pergunta “quanto tempo de CPU esta thread usou?” é uma operação muito cara mais do que eu imaginava
    • Para discutir medições em nanossegundos, é preciso entender muito bem a estabilidade e a precisão do relógio
      Sem uma referência no nível de um relógio atômico, acho difícil fazer afirmações sobre valores absolutos
    • Fiquei curioso se foi analisado por que a distribuição se espalha por várias ordens de grandeza. Isso por si só já é um fenômeno interessante
    • Agradeci muito pelo resumo TL;DR curto. Esse tipo de resumo reduz a barreira de entrada do texto e dá motivação para ler
    • Deixaram a reação “não é surpreendente (Quelle Surprise)”
  • clock_gettime() evita troca de contexto via vDSO. Por isso esse rastro também aparece no flamegraph
    • Mas isso vale só para alguns clocks. Em casos como CLOCK_VIRT ou CLOCK_SCHED, ainda é necessário fazer chamada de syscall
    • Olhando abaixo do frame de vDSO, ainda existe syscall. Parece que não foi implementado um fast path para certos clock ids
    • CLOCK_THREAD_CPUTIME_ID acaba indo para o kernel de qualquer forma, porque precisa consultar a task struct
      Veja o código do kernel relacionado em posix-cpu-timers.c,
      cputime.c,
      gettimeofday.c
  • Usando PERF_COUNT_SW_TASK_CLOCK, também dá para medir algo em torno de 8 ns
    A abordagem é ler da página compartilhada por meio de perf_event_mmap_page e calcular o delta com uma chamada rdtsc
    Isso é pouco documentado e quase não há implementações open source
    • É um truque realmente excelente. Ainda assim, como a configuração de perf_event e os requisitos de permissão são grandes, parece mais adequado para threads de longa duração
    • Perguntaram por que o seqlock é necessário. Seria para evitar que ocorra uma troca de contexto entre os valores da página e o rdtsc?
      Provavelmente a estrutura é verificar o valor da página de novo depois do rdtsc e, se tiver mudado, tentar novamente
      Como referência, clock_gettime também é uma syscall virtual baseada em vdso
    • clock_gettime não usa syscall, e sim vdso
  • Flamegraph é uma ferramenta realmente excelente
    Olhando só o código, tudo parece ok, mas quando se vê o flamegraph muitas vezes a reação é “que diabos é isso?!”
    Já encontrei vários problemas assim, como inicialização que não era estática e uma chamada de logger de uma linha causando serialização cara
    • Eu também gosto de icicle graph. Ele acumula na direção oposta do flamegraph, então fica mais fácil ver gargalos quando vários caminhos chamam bibliotecas em comum
    • Se abrir este exemplo em SVG em uma nova aba, dá para usar zoom interativo
    • Experimentos de profiling e otimização de performance são uma das partes mais divertidas do desenvolvimento. Há muitos momentos de “por que isso está tão lento?”
    • Houve também a opinião de que a combinação de parsing de strings com memoization soa estranha. Na prática, o problema era não fazer cache do parsing de padrões de regex caros
    • Para quem vai usar flamegraph pela primeira vez, perguntaram sobre conceitos básicos e por onde começar
  • Fiquei surpreso que “abrir imagem em nova aba” realmente fornece interação com SVG
    • Esse recurso existe graças ao script FlameGraph de Brendan Gregg
      Normalmente uso o gerador HTML do async-profiler, mas desta vez usei a ferramenta do Brendan para obter um único SVG
  • Sou o autor do patch do OpenJDK. Falei sobre o overhead de memória ao ler /proc, profiling com eBPF e a história de uma ABI de user space pouco documentada
    Organizei mais detalhes neste post do meu blog
    • Perguntaram por que a implementação original era daquele jeito. Fazer IO de arquivo e parsing de string a cada chamada é ineficiente, mas imagino que na época houvesse um motivo
    • Jaromir viu meu texto e disse “eu também escrevi um rascunho na mesma época”, então linkamos os textos um do outro. Fiquei feliz por ele considerar o meu mais rigoroso
  • Só porque é uma linguagem de sistema como C ou C++ não significa que sempre será rápida. A velocidade varia muito conforme o que está sendo feito
  • Leitura via vDSO é muito mais rápida porque evita transição para o kernel, serialização de buffer e parsing
  • Compartilharam a citação: “se ficou 2x mais rápido, talvez você tenha feito algo inteligente; se ficou 100x mais rápido, você só parou de fazer algo idiota
    Tweet de origem
  • A equipe do QuestDB é de altíssimo nível nessa área. Tanto as pessoas quanto o software são excelentes
    O blog do Jaromir também foi realmente ótimo