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

3 weeks ago 9

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

기존 구현의 문제

  • getCurrentThreadUserTime()은 /proc/self/task/<tid>/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() 호출 시 자동 성능 향상 제공

Read Entire Article