# 400배 성능 격차를 없앤 40줄짜리 수정

> Clean Markdown view of GeekNews topic #25816. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=25816](https://news.hada.io/topic?id=25816)
- GeekNews Markdown: [https://news.hada.io/topic/25816.md](https://news.hada.io/topic/25816.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-01-15T03:33:10+09:00
- Updated: 2026-01-15T03:33:10+09:00
- Original source: [questdb.com](https://questdb.com/blog/jvm-current-thread-user-time/)
- Points: 30
- Comments: 5

## Summary

OpenJDK의 `ThreadMXBean.getCurrentThreadUserTime()`이 `/proc` 파일 파싱 대신 **`clock_gettime()` 호출로 대체**되며 최대 400배의 성능 향상을 이뤘습니다. 기존에는 `/proc/self/task/&lt;tid&gt;/stat` 파일을 열고 문자열을 파싱하는 복잡한 경로를 거쳤지만, 새 구현은 **Linux 커널의 `clockid_t` 비트 인코딩**을 직접 활용해 유저 타임만 빠르게 조회합니다. POSIX 표준의 한계를 넘어 커널 내부 ABI를 이해하고 응용함으로써, 단 40줄의 수정으로 대규모 성능 병목을 제거한 사례입니다.

## Topic Body

- OpenJDK의 `ThreadMXBean.getCurrentThreadUserTime()`이 **/proc 파일 파싱 대신 `clock_gettime()` 호출**로 교체되며 최대 **400배 성능 향상**을 달성  
- 기존 구현은 `/proc/self/task/&lt;tid&gt;/stat` 파일을 열고 읽고 파싱하는 **복잡한 I/O 경로**를 거쳤음  
- 새 구현은 **Linux 커널의 `clockid_t` 비트 인코딩**을 활용해 `pthread_getcpuclockid()`로 얻은 ID의 하위 비트를 조정, **유저 타임만 직접 조회**  
- 벤치마크 결과 평균 호출 시간이 **11μs → 279ns**로 감소, 이후 커널 fast-path 적용 시 **약 13% 추가 개선**  
- POSIX 제약을 넘어 **리눅스 내부 ABI 이해를 통한 최적화**가 가능함을 보여주는 사례  

---
### 기존 구현의 문제
- `getCurrentThreadUserTime()`은 `/proc/self/task/&lt;tid&gt;/stat` 파일을 열어 **13번째와 14번째 필드**를 파싱해 CPU 유저 타임을 계산  
  - 파일 경로 생성, 파일 열기, 버퍼 읽기, 문자열 파싱, `sscanf()` 호출 등 다단계 처리 필요  
  - 명령어 이름에 괄호가 포함될 수 있어 `strrchr()`로 마지막 `)`를 찾는 복잡한 로직 포함  
- 반면 `getCurrentThreadCpuTime()`은 단일 `clock_gettime(CLOCK_THREAD_CPUTIME_ID)` 호출만 수행  
- 2018년 버그 리포트(JDK-8210452)에 따르면 두 메서드 간 속도 차이는 **30~400배**에 달함  

### `/proc` 접근 경로와 `clock_gettime()` 경로 비교
- `/proc` 방식은 `open()`, `read()`, `sscanf()`, `close()` 등 **여러 시스템 호출과 커널 내부 문자열 생성**을 포함  
- `clock_gettime()` 방식은 **단일 시스템 호출**로 `sched_entity` 구조체에서 직접 시간 값을 읽음  
- 병렬 부하 시 `/proc` 접근은 **커널 락 경합**으로 인해 지연이 심화됨  

### 새로운 구현 방식
- POSIX 표준은 `CLOCK_THREAD_CPUTIME_ID`가 **유저+시스템 시간**을 반환하도록 정의되어 있음  
- Linux 커널은 `clockid_t`의 하위 비트로 **시계 종류를 인코딩**  
  - `00=PROF`, `01=VIRT(유저 전용)`, `10=SCHED(유저+시스템)`  
- `pthread_getcpuclockid()`로 얻은 `clockid`의 하위 비트를 `01`로 바꾸면 **유저 타임 전용 시계**로 전환 가능  
- 새 코드에서는 파일 I/O와 파싱을 제거하고, `clock_gettime()` 호출만으로 유저 타임을 반환  

### 성능 측정 결과
- 수정 전 평균 호출 시간 **11.186μs**, 수정 후 **0.279μs**로 약 **40배 개선**  
  - 16스레드 환경에서 측정, 원래 보고된 30~400배 범위와 일치  
- CPU 프로파일에서 **파일 열기·닫기 관련 시스템 호출이 사라지고**, 단일 `clock_gettime()` 호출만 남음  

### 커널 fast-path 추가 최적화
- 커널은 `clockid`에 **PID=0**이 인코딩된 경우 현재 스레드로 바로 접근하는 **fast-path**를 제공  
- JVM이 `pthread_getcpuclockid()` 대신 직접 `clockid`를 구성해 PID=0을 넣으면 **radix tree 탐색을 생략** 가능  
- 수동 구성한 `clockid` 사용 시 평균 시간 **81.7ns → 70.8ns**, 약 **13% 추가 개선**  
- 다만 `clockid_t` 크기 등 커널 내부 구현에 의존하므로 **가독성과 호환성 손실** 우려 존재  

### 결론 및 교훈
- 40줄 삭제로 **400배 성능 격차 제거**, 새로운 커널 기능 없이 **기존 ABI의 세부 구조 활용**만으로 달성  
- **커널 소스 코드 탐독의 가치** 강조: POSIX는 이식성을 보장하지만, 커널 코드는 **가능성의 한계**를 보여줌  
- **기존 가정 재검토의 중요성**: `/proc` 파싱은 과거에는 합리적이었으나, 현재는 비효율적임  
- 이 변경은 **JDK 26**(2026년 3월 출시 예정)에 포함되어, `ThreadMXBean.getCurrentThreadUserTime()` 호출 시 **자동 성능 향상** 제공

## Comments



### Comment 49241

- Author: crawler
- Created: 2026-01-15T09:01:27+09:00
- Points: 2

대단하네요.   
  
> 2배 빨라졌다면 똑똑한 짓을 한 것일 수 있고, 100배 빨라졌다면 멍청한 짓을 멈춘 것일 뿐  
  
완전 틀린 말은 아니라고 생각하지만, 커널과 엮인 경우에는 느린 걸 알아차리는 것부터 정말 어려웠을 거라고 생각합니다.

### Comment 49480

- Author: [hidden]
- Created: 2026-01-19T19:08:51+09:00
- Points: 1

[숨김 처리된 댓글입니다]

### Comment 49454

- Author: princox
- Created: 2026-01-19T10:15:47+09:00
- Points: 1

이런 것들은 프로젝트에서 어떻게 발견할 수 있을까요? AI 돌린다고 알기는 어려울 것 같은데..   
  
이런 사례들을 보면 저도 배워서 꼭 경험해보고 싶다는 생각을 합니다.

### Comment 49256

- Author: aobamisaki
- Created: 2026-01-15T12:11:52+09:00
- Points: 1

사실 코드 전체 갈아엎어서 2~3배 향상도 어려운 일인데, 단순히 몇줄 바꿔서 최대 400배 향상은 정말 대단하군요.

### Comment 49227

- Author: neo
- Created: 2026-01-15T03:33:10+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=46609630) 
- 작성자인 나임. 지난번 커널 버그 관련 글 이후, JVM이 자체적으로 스레드 활동을 보고하는 방식을 살펴봤음  
  “이 스레드의 CPU 사용 시간은 얼마인가?”라는 질문이 생각보다 **너무 비싼 연산**이라는 걸 알게 되었음
  - 나노초 단위의 측정을 논하려면 **시계의 안정성과 정확도**를 매우 잘 이해해야 함  
    원자시계 수준의 기준이 없다면 절대적인 수치를 주장하기 어렵다고 생각함
  - 분포가 여러 **자릿수 단위로 퍼져 있는 이유**를 살펴봤는지 궁금함. 그 자체로 흥미로운 현상임
  - 짧은 **TL;DR 요약**이 정말 고마웠음. 이런 요약이 글의 진입 장벽을 낮춰주고 읽는 동기를 만들어줌
  - “놀랍지 않음(Quelle Surprise)”이라는 반응을 남김
- `clock_gettime()`은 **vDSO**를 통해 컨텍스트 스위치를 피함. 그래서 flamegraph에서도 그 흔적이 보임
  - 하지만 일부 clock만 해당됨. `CLOCK_VIRT`나 `CLOCK_SCHED` 같은 경우는 여전히 **syscall 호출**이 필요함
  - vDSO 프레임 아래를 보면 여전히 syscall이 있음. 특정 clock id에 대한 **빠른 경로(fast path)** 가 구현되어 있지 않은 듯함
  - `CLOCK_THREAD_CPUTIME_ID`는 결국 커널로 넘어감. task struct를 참조해야 하기 때문임  
    관련 커널 소스는 [posix-cpu-timers.c](https://elixir.bootlin.com/linux/v6.18.5/source/kernel/time/posix-cpu-timers.c#L358),  
    [cputime.c](https://elixir.bootlin.com/linux/v6.18.5/source/kernel/sched/cputime.c#L844),  
    [gettimeofday.c](https://elixir.bootlin.com/linux/v6.18.5/source/lib/vdso/gettimeofday.c#L288) 참고
- `PERF_COUNT_SW_TASK_CLOCK`을 사용하면 약 **8ns 수준의 측정**도 가능함  
  `perf_event_mmap_page`를 통해 공유 페이지에서 읽고, `rdtsc` 호출로 델타를 계산하는 방식임  
  문서화가 잘 안 되어 있고 오픈소스 구현도 거의 없음
  - 정말 멋진 트릭임. 다만 `perf_event` 설정과 권한 요구가 크기 때문에 **긴 수명의 스레드**에 적합해 보임
  - `seqlock`이 필요한 이유를 물음. 페이지 값과 `rdtsc` 사이에서 **컨텍스트 스위치**가 일어나지 않도록 하기 위함인지 궁금함  
    아마 `rdtsc` 후 페이지 값을 다시 확인해 바뀌었으면 재시도하는 구조로 보임  
    참고로 `clock_gettime`도 vdso 기반 가상 syscall임
  - `clock_gettime`은 syscall이 아니라 vdso를 사용함
- **Flamegraph**는 정말 훌륭한 도구임  
  코드만 봤을 땐 괜찮아 보이지만, flamegraph를 보면 “이게 뭐야?!” 싶을 때가 많음  
  정적 초기화가 아닌 초기화, 한 줄짜리 로거 호출이 **비싼 직렬화**를 유발하는 등 여러 문제를 발견했음
  - 나는 **icicle graph**도 좋아함. flamegraph의 반대 방향으로 누적되어, 여러 경로가 공통 라이브러리를 호출할 때 병목을 보기 쉬움
  - [이 SVG 예시](https://questdb.com/images/blog/2026-01-13/before.svg)를 새 탭에서 열면 **인터랙티브 줌**이 가능함
  - 성능 프로파일링과 최적화 실험은 개발에서 가장 즐거운 부분 중 하나임. “왜 저게 이렇게 느리지?”라는 놀라움이 많음
  - 문자열 파싱과 **memoization**의 조합이 이상하게 들린다는 의견도 있었음. 실제로는 비싼 정규식 패턴 파싱을 캐싱하지 않아 생긴 문제였음
  - flamegraph를 처음 써보려는 사람에게는 기본 개념과 시작점을 물어봄
- “이미지 새 탭에서 열기”가 실제로 **SVG 인터랙션**을 제공한다는 점이 놀라웠음  
  - 이 기능은 [Brendan Gregg의 FlameGraph 스크립트](https://github.com/brendangregg/FlameGraph) 덕분임  
    평소엔 async-profiler의 HTML 생성기를 쓰지만, 이번엔 단일 SVG를 위해 Brendan의 도구를 사용했음
- OpenJDK 패치 작성자인 나임. `/proc`을 읽을 때의 **메모리 오버헤드**와 eBPF 프로파일링, 그리고 문서화가 부족한 user-space ABI의 역사를 다뤘음  
  자세한 내용은 [내 블로그 글](https://norlinder.nu/posts/User-CPU-Time-JVM/)에 정리했음
  - 왜 원래 구현이 그렇게 되어 있었는지 궁금하다는 질문을 받음. 매 호출마다 파일 IO와 문자열 파싱을 하는 건 비효율적이지만, 당시엔 이유가 있었을 것이라 생각함
  - Jaromir가 내 글을 보고 “나도 같은 시기에 초안을 썼다”며 **서로의 글을 링크**함. 내 글이 더 엄밀하다고 평가해줘서 기뻤음
- C나 C++ 같은 시스템 언어라고 해서 항상 빠른 건 아님. **무엇을 하느냐**에 따라 속도는 크게 달라짐
- vDSO를 통한 읽기는 커널 전환, 버퍼 직렬화, 파싱 과정을 피하기 때문에 **훨씬 빠름**
- “2배 빨라졌다면 똑똑한 짓을 한 것일 수 있고, 100배 빨라졌다면 **멍청한 짓을 멈춘 것**일 뿐”이라는 인용을 공유함  
  [출처 트윗](https://x.com/rygorous/status/1271296834439282690)
- QuestDB 팀은 이 분야에서 **최고 수준**임. 사람들도, 소프트웨어도 모두 훌륭함  
  Jaromir의 블로그도 정말 멋졌음
