ThreadMXBean.getCurrentThreadUserTime()do OpenJDK foi substituído de parsing de arquivo em/procpor chamada aclock_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_tdo kernel Linux para ajustar os bits inferiores do ID obtido porpthread_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>/state 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)
- 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
- Em contrapartida,
getCurrentThreadCpuTime()fazia apenas uma única chamada aclock_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
/procincluía várias chamadas de sistema e geração de strings dentro do kernel, comoopen(),read(),sscanf()eclose() - O método com
clock_gettime()lia diretamente o valor de tempo da estruturasched_entitycom uma única chamada de sistema - Sob carga paralela, o acesso a
/procsofria 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_IDdeve retornar o tempo de usuário + sistema - O kernel Linux codifica o tipo de relógio nos bits inferiores de
clockid_t00=PROF,01=VIRT(apenas usuário),10=SCHED(usuário + sistema)
- Ao trocar os bits inferiores do
clockidobtido porpthread_getcpuclockid()para01, é 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
clockidcontém PID=0 codificado - Se a JVM montar diretamente esse
clockidem vez de usarpthread_getcpuclockid(), é possível pular a busca na radix tree - Com
clockidmontado 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
/procfazia 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
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.
Comentários do Hacker News
Descobri que a pergunta “quanto tempo de CPU esta thread usou?” é uma operação muito cara mais do que eu imaginava
Sem uma referência no nível de um relógio atômico, acho difícil fazer afirmações sobre valores absolutos
clock_gettime()evita troca de contexto via vDSO. Por isso esse rastro também aparece no flamegraphCLOCK_VIRTouCLOCK_SCHED, ainda é necessário fazer chamada de syscallCLOCK_THREAD_CPUTIME_IDacaba indo para o kernel de qualquer forma, porque precisa consultar a task structVeja o código do kernel relacionado em posix-cpu-timers.c,
cputime.c,
gettimeofday.c
PERF_COUNT_SW_TASK_CLOCK, também dá para medir algo em torno de 8 nsA abordagem é ler da página compartilhada por meio de
perf_event_mmap_pagee calcular o delta com uma chamadardtscIsso é pouco documentado e quase não há implementações open source
perf_evente os requisitos de permissão são grandes, parece mais adequado para threads de longa duraçãoseqlocké necessário. Seria para evitar que ocorra uma troca de contexto entre os valores da página e ordtsc?Provavelmente a estrutura é verificar o valor da página de novo depois do
rdtsce, se tiver mudado, tentar novamenteComo referência,
clock_gettimetambém é uma syscall virtual baseada em vdsoclock_gettimenão usa syscall, e sim vdsoOlhando 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
Normalmente uso o gerador HTML do async-profiler, mas desta vez usei a ferramenta do Brendan para obter um único SVG
/proc, profiling com eBPF e a história de uma ABI de user space pouco documentadaOrganizei mais detalhes neste post do meu blog
Tweet de origem
O blog do Jaromir também foi realmente ótimo
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.
Impressionante.
Acho que não é totalmente falso, mas, quando envolve o kernel, imagino que perceber que estava lento já deve ter sido muito difícil.