Debugging False Sharing

이 글은 넷플릭스 테크 블로그의 Seeing through hardware counters: a journey to threefold performance increase를 제가 이해한 대로 요약하고, 관련 배경 지식을 나름대로 정리해 덧붙인 글입니다. 원문이 정확한 출처이니, 구체적인 수치나 세부 사항이 궁금하시다면 반드시 원문을 확인해주세요. 문제 발생 넷플릭스가 운영하던 어떤 서비스가 CPU 부족을 겪어서, 노드 스펙을 3배로 늘리는 scale-up을 진행했습니다. CPU-intensive한 워크로드였으니 처리량도 그에 비례해서 늘어날 거라 기대했지만, 실제로 개선된 폭은 25% 남짓이었습니다. 심지어 tail latency는 오히려 더 나빠졌습니다. ...

2025년 12월 12일 · 4 분 · Donghyung Ko

Accidental Quadratic Hashmap Iteration

이 글은 Rust hash iteration+reinsertion 글과 관련 Rust 이슈/PR을 제가 이해한 대로 정리한 글입니다. 원문이 정확한 소스이니, 정확한 내용은 원문을 확인해주세요. Rust의 HashMap에서 발생했던 흥미로운 버그를 하나 소개하려고 합니다. 겉보기에는 지극히 평범한 코드인데, 특정 조건이 갖춰지면 O(n)이어야 할 연산이 O(n²)으로 폭발합니다. 버그 재현 다음 코드를 보겠습니다. 첫 번째 해시맵(one)에 1부터 5,000,000까지의 값을 삽입한 뒤(T1), 그 해시맵을 순회하면서 두 번째 해시맵(two)에 값을 그대로 재삽입(T2)하는 것이 전부입니다. use std::collections::hash_set::HashSet; fn main() { println!("inserting..."); let mut one = HashSet::new(); for i in 1..5000000 { one.insert(i); } println!("reinserting..."); let mut two = HashSet::new(); for v in one { two.insert(v); } } T1과 T2 모두 원소를 하나씩 삽입하는 동작이니, 둘 다 O(n)이어야 할 것 같습니다. 그런데 실제로 실행해보면 T1은 예상대로 O(n)에 끝나지만, T2는 O(n²)에 가까운 시간이 걸립니다. 같은 일을 하는 코드인데 왜 이런 차이가 발생할까요? ...

2025년 12월 8일 · 4 분 · Donghyung Ko
가상 메모리 주소를 물리 메모리 주소로 매핑하는 Page Table 구조

Linux Transparent Huge Pages

요약 대형 페이지(Huge Page) 기능을 활용하여 TLB 미스로 인한 오버헤드를 최소화할 수 있습니다. 워킹셋 크기가 크고(수십 MB 이상), 메모리 접근이 매우 빈번한 워크로드에서 유용한 최적화 기법입니다. 주로 데이터베이스 시스템과 관련된 레퍼런스(Oracle, Postgres, AWS RDS)가 많습니다. 대형 JVM 애플리케이션 하이퍼바이저(KVM/QEMU) 대규모 분석·검색 시스템(예: ClickHouse) CPU 및 I/O 바운드가 지배적인 워크로드에서는 별다른 효용이 없을 수 있습니다. 배경 모든 프로세스는 가상 메모리 주소 공간을 할당받습니다. 프로세스에서 가상 메모리 주소에 접근하면 CPU는 이를 실제 물리 메모리 주소로 치환하여 처리합니다. OS는 가상 메모리 주소를 물리 메모리 주소로 매핑하기 위한 Page Table을 관리합니다. ...

2025년 9월 16일 · 4 분 · Donghyung Ko

JVM Lock Coarsening

Lock Coarsening이란? 같은 모니터(객체)에 대해 락을 연속적으로 잡았다 풀었다 하는 동작을 더 큰 단위로 묶어서 한 번에 잡고 푸는 형태로 바꿈으로써, 락을 획득하고 해제하는 데 따르는 오버헤드를 최소화하는 JIT 최적화 기법입니다. -XX:+EliminateLocks 옵션이 활성화되어 있으면 적용됩니다. 핵심 아이디어 자바의 synchroinzed는 바이트코드로 monitorenter/moniterexit으로 표현됩니다. 반복문이나 인접한 코드 영역에서 같은 객체에 대해 monitorenter/moniterexit가 짧은 간격으로 반복되면, JIT 컴파일러는 이를 한 번만 처리하도록 변경합니다. 이를 통해 CAS(Compare-and-Set) 연산, 메모리 배리어, 스택 및 헤더 변경과 같은 락 비용을 최소화합니다. 예시 Lock Coarsening 전 for (int i = 0; i < n; i++) { synchronized (lock) { doWork(i); } } Lock Coarsening 후 synchronized (lock) { for (int i = 0; i < n; i++) { doWork(i); } } 검증 아래와 같이 테스트 코드를 설정했습니다. @Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"}) @State(Scope.Benchmark) public class LockRoach { int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int c = 0; c < 1000; c++) { synchronized (this) { x += 0x42; } } } } -prof perfasm를 사용하여 디스어셈블리를 분석했습니다. 분석 결과, JIT 컴파일러가 Loop Unrolling을 적용하여 락 획득과 해제 빈도가 감소한 것을 확인했습니다. 다만, 루프 전체에 대해 lock coarsening이 적용되지는 않았습니다. JVM은 과도한 코어스닝이 한 스레드가 락을 오래 독점하게 만들 수 있어 위험하다고 판단했습니다. ↗ 0x00007f455cc708c1: lea 0x20(%rsp),%rbx │ < blah-blah-blah, monitor enter > ; <--- coarsened! │ 0x00007f455cc70918: mov (%rsp),%r10 ; load $this │ 0x00007f455cc7091c: mov 0xc(%r10),%r11d ; load $this.x │ 0x00007f455cc70920: mov %r11d,%r10d ; ...hm... │ 0x00007f455cc70923: add $0x42,%r10d ; ...hmmm... │ 0x00007f455cc70927: mov (%rsp),%r8 ; ...hmmmmm!... │ 0x00007f455cc7092b: mov %r10d,0xc(%r8) ; LOL Hotspot, redundant store, killed two lines below │ 0x00007f455cc7092f: add $0x108,%r11d ; add 0x108 = 0x42 * 4 <-- unrolled by 4 │ 0x00007f455cc70936: mov %r11d,0xc(%r8) ; store $this.x back │ < blah-blah-blah, monitor exit > ; <--- coarsened! │ 0x00007f455cc709c6: add $0x4,%ebp ; c += 4 <--- unrolled by 4 │ 0x00007f455cc709c9: cmp $0x3e5,%ebp ; c < 1000? ╰ 0x00007f455cc709cf: jl 0x00007f455cc708c1 - Loop Unrolling이란? - 루프 본문을 복제하여 반복 횟수와 분기 비용을 줄이는 컴파일러의 루프 최적화 기법입니다. - Loop Unrolling이 적용된 코드의 예시는 다음과 같습니다. @Fork(..., jvmArgsPrepend = {"-XX:-UseBiasedLocking"}) @State(Scope.Benchmark) public class LockRoach { int x; @Benchmark @CompilerControl(CompilerControl.Mode.DONT_INLINE) public void test() { for (int c = 0; c < 1000; c += 4) { synchronized (this) { // 동일한 본문을 4번 복제 x += 0x42; x += 0x42; x += 0x42; x += 0x42; } } } } Loop Unrolling을 통해 락 획득과 해제 빈도를 1/4 수준으로 줄일 수 있었습니다. Loop Unrolling 기능을 제한하면 성능이 4배 저하되는 것을 확인했습니다. Benchmark Mode Cnt Score Error Units # Default LockRoach.test avgt 5 5331.617 ± 19.051 ns/op # -XX:LoopUnrollLimit=1 LockRoach.test avgt 5 20679.043 ± 3.133 ns/op // 4배 느려짐 레퍼런스 https://shipilev.net/jvm/anatomy-quarks/1-lock-coarsening-for-loops/

2025년 9월 16일 · 3 분 · Donghyung Ko