벤치마크는 밀리초 단위로 돌리세요

Benchmark in Milliseconds

matklad.github.io ▲ 126 댓글 36 surprisetalk

요약

마이크로벤치마크는 입력 크기를 조절해 한 번에 약 300밀리초가 걸리게 맞추는 것이 좋습니다.

개발자 알렉세이 클라도프(matklad)가 블로그에 쓴 경험칙입니다. 마이크로벤치마크(작은 코드 조각의 속도를 재는 시험)를 얼마나 오래 돌려야 하느냐는 물음에 네 가지 이유를 들어 답했습니다.

왜 중요한가

  • 벤치마크의 목적은 정밀한 측정이 아니라 성능 판단에 쓸 감각을 얻는 것이라는 관점을 내세웁니다.
  • 복잡한 통계 기법 없이도 쓸 만한 측정값을 얻는 간단한 기준을 제시합니다.

핵심 내용

  • 밀리초는 1~999의 정수로 읽혀서 작은 개선도 보이고 한눈에 비교하기 쉽습니다.
  • 10밀리초보다 짧으면 인터프리터 시작 같은 고정 비용이 결과를 왜곡할 수 있습니다.
  • 수백 밀리초면 고정 비용이 묻혀서 따로 보정할 필요가 없다고 했습니다.
  • 사람이 체감할 수 있는 시간이라 숫자뿐 아니라 감각으로도 빨라진 것을 느낄 수 있습니다.
  • 1초를 넘기면 실험 주기가 늘어지므로, 열 번쯤 연달아 돌려 편차를 보는 일도 빨라야 한다고 했습니다.

HN 반응

  • 절대 시간보다 대조군과 같은 실행에서 번갈아 재고 신뢰 구간(결과가 들어갈 범위)을 비교하자는 의견이 많았습니다.
  • 손대지 않은 코드에서 몇 배 개선을 찾을 때는 쓸 만하지만, 작은 개선이나 JVM처럼 예열이 필요한 환경에서는 부족하다는 반론이 나왔습니다.

댓글

30개 표시 · 전체 36개
  1. spankalee HN

    저라면 "신뢰구간을 두고 벤치마크하라"나 "비교 대상을 두고 벤치마크하라"라고 말하겠습니다.

    절대적인 숫자 하나만으로는 말할 수 있는 게 거의 없습니다. 다른 대안이나 대조군과 비교해야 하는데, CPU 부하, 스로틀링, GC 같은 온갖 변수가 있으니 그 대조군과 같은 실행 안에서 비교해야 합니다. 그리고 노이즈가 각 구현에 공평하게 퍼지도록 여러 번 실행에 걸쳐 번갈아 돌리는(round robin) 것이 중요합니다.

    측정값이 여럿 모이면 분포가 생기므로, 평균만 가져다 비교하지 말고 95% 신뢰구간 같은 값을 계산해야 합니다. 신뢰구간이 겹치면 어느 쪽이 더 빠른지 실제로는 모를 수 있습니다. 겹치지 않으면 어느 쪽이 빠른지 아마 알 수 있습니다.

    좋은 벤치마크라면 실행 횟수를 늘려서 신뢰구간을 좁힐 수 있고, 실행 시간이 길어지는 대신 아주 작은 개선도 가려낼 수 있습니다. 반대로 신뢰구간이 좁아지지 않는다면 신호 대 잡음비의 한계에 부딪힌 것입니다.

    아주아주 통제된 하드웨어 실험실이 아닌 곳에서 제가 믿을 만하고 실행에 옮길 수 있는 벤치마크를 얻은 방법은 이것뿐이었습니다. 구글의 Tachometer 벤치마크 러너가 바로 이렇게 하는데, 이렇게 하는 러너가 더 많았으면 좋겠습니다: https://github.com/google/tachometer

  2. kccqzy HN

    사용자에게 신뢰구간을 주는 대신, p값을 지정하게 하세요. 그런 다음 웰치의 t 검정 같은 걸 돌리면 됩니다.

  3. spider-mario HN
  4. taylorbuley HN

    제 지론은 "부하나 벽시계 시간에 영향받지 않는 증거로 벤치마크하라"입니다.

    솔깃하긴 한데, 그렇지 않으면 현재 환경의 한계를 증명하는 데 그칠 뿐이죠.

  5. vlovich123 HN

    무엇을 벤치마크하는지, 측정이 얼마나 믿을 만해야 하는지, 어떤 분야를 벤치마크하는지에 따라 달라집니다. 예를 들어 criterion은 측정값을 안정시켜야 하기 때문에 시간이 꽤 걸리기도 합니다. 글쓴이는 "그런 정확도는 필요 없다"고 주장하지만, 저는 이 노이즈 때문에 헛것을 쫓느라 시간을 낭비하거나, 사실은 성능 개선이 없었거나 오히려 나빠졌는데 개선했다고 주장하는 경우를 봤습니다.

    10ms보다 빠른 작업은 고정 비용(예: 인터프리터 시작 시간) 때문에 결과가 왜곡될 위험이 있다.

    글쓴이는 경험이 전적으로 Python에 한정된 사람 같습니다. 예를 들어 Java라면 JIT가 프로그램을 충분히 최적화했는지부터 확인해야 합니다.

    또 벤치마크할 만한 대표성 있는 데이터셋을 만드는 데 정말 오래 걸리고, 성능을 평가하는 데도 시간이 드는 경우가 많습니다(예: 데이터베이스). 짧고 빠른 마이크로벤치마크는 출발점으로는 유용하지만, 결국 어느 시점에는 전체 시스템의 정상 상태(steady state) 성능을 평가해야 합니다. 게임 렌더링 성능도 마찬가지입니다. 300ms짜리 샘플로는 25분 뒤에 프레임 드롭이 생기는지, 메모리 누수가 있는지 전혀 알 수 없습니다.

  6. thadt HN

    도메인과 시간 단위가 아주 중요하다는 데는 동의합니다. 나노초가 중요한 시스템을 다룰 때는 시간 문제에 상당히 신경을 썼습니다.

    하지만 일반적으로는 원글 작성자에게 동의합니다. 대부분은 밀리초를 신경 쓰니까요.

    글쓴이는 경험이 전적으로 Python에 한정된 사람 같다.

    어, [1] 아닌데요 [2].

    [1] https://github.com/rust-lang/rust-analyzer

    [2] https://github.com/tigerbeetle/tigerbeetle

  7. hinkley HN

    호이스팅 같은 일부 최적화는 벤치마크 결과가 불분명하더라도 코드 가독성을 높여 줍니다. 그리고 코드가 실제 환경(in vivo)에서 동작하는 방식과 테스트에서 동작하는 방식은 양쪽 어느 방향으로든 꽤 다를 수 있습니다. 특히 시간과 공간을 맞바꾸는 경우에는, 내 코드와 동시에 돌아가는 다른 코드 때문에 캐시 압박이 줄기도 하고 늘기도 합니다.

    최적화에 관한 제 생각의 상당수는 프로파일러가 중복된 함수 호출 하나가 어떤 작업 실행 시간의 5%를 차지한다고 알려 줬던 일에서 시작됐습니다. 그걸 없앴더니 실행 시간이 20% 줄었어요. 그러고 나서 프로파일러가 거짓말할 수 있는 온갖 경우를 생각해 봐야 했습니다. 시간이 이렇게 흘렀는데도 여전히 흑마술 같은 영역입니다.

    도구는 어떤 문제를 들여다볼 가치가 있을지 알려 줄 뿐이고, 내 작업물을 남길지 말지는 전혀 다른 문제입니다. 안타깝게도 어떤 사람들은 매몰 비용 오류에 빠지거나 체면 잃는 걸 걱정해서, 일단 한 방향으로 나아가면 효과가 있든 없든 코드베이스에 머지되는 걸 봐야 직성이 풀립니다. 게다가 복권에 당첨되듯 테스트를 한 번 돌렸을 때 자기 쪽이 훨씬 빠르게 나오면 더 세게 밀어붙입니다. 그다음 열 번의 실행에서는 정반대 결과가 나왔다는 건 아랑곳하지 않고요.

  8. tialaramex HN

    how a piece of code behaves in vivid and under test can vary quite a bit

    "in vivid"를 이런 식으로 쓰는 건 처음 봅니다. 혹시 "in vivo"를 말씀하시려던 건가요? 라틴어로 "생명 안에서", "살아 있는 상태에서"라는 뜻이고, 실험실에서 과학 실험을 할 때 쓰는 유리 페트리 접시나 비커를 가리키는 "유리 안에서"라는 뜻의 라틴어 "in vitro"와 구별되는 말이죠.

  9. hinkley HN

    그건 애플 자동 고침이 인간은 너무 멍청해서 주도권을 맡기면 안 된다는 걸 증명하려는 장기 사기극이라서 그래요. 우리는 영어도 제대로 못 쓰잖아요. 저는 "in vivid"라고 입력하지 않았다고 장담합니다.

  10. jonhohle HN

    처리량이 많은 시스템에서 10ms는 좀 말도 안 되는 값입니다. 저는 운영 지표를 ms 단위로 잡는 시스템을 운영해 봤는데, 서버 쪽 지연은 1ms 미만이었고(비즈니스 로직 계산이거나 캐시가 잘 된 데이터였습니다), 클라이언트 쪽은 3~5ms 정도였습니다. 말씀하신 대로 Java였습니다.

    이런 측정값을 어떻게 집계하는지도 신경 써야 합니다. 평균은 거의 항상 아무것도 알려 주지 못합니다. 부하 상태의 높은 백분위수(95%, 99%, 99.9%)는 평균이나 중앙값과는 완전히 다른 모습을 보여 줄 수 있습니다.

  11. ashf023 HN

    그래도 요청 100개를 벤치마크해서 그 시간을 재면 됩니다. 예컨대 JVM이라면 JIT 효과와 GC처럼 나중에야 일어나는 일을 고려해야 하니까, 1ms짜리 요청 100개는 여전히 아주 작은 벤치마크라서 아마 믿기 어려울 겁니다.

  12. hinkley HN

    human-perceptible range allows me to use my intuitive sense of time and speed

    저는 커리어 초반에 성능을 전문으로 하려던 건 아니었는데 어쩌다 보니 그렇게 됐습니다. 따분한 과제도 제 나름대로 재미있게 만들려고 튜닝하곤 했죠. 그런데 학교를 졸업하고 첫 직장을 잡아 멀리 이사해서 출근했더니, UI가 너무 느리게 그려져서 화가가 손으로 그림 그리는 영상을 빨리 감기로 돌리는 것 같았습니다. 이 형편없는 물건에 제 이름이 걸린다는 생각에 당황했지만 티를 안 냈고, 어려운 버그를 몇 개 고쳐서 바보가 아니라는 걸 증명하자마자 일에 착수했습니다.

    처음 열두어 개의 변경에는 벤치마크가 필요 없었습니다. 사무실에서 제일 느린 기계를 쓰고 있었던 덕도 있어요. 머릿속으로 말 그대로 초를 셀 수 있었고, 예전 버전에서는 한두 음절 더 센 뒤에야 끝나던 게 이제 그전에 끝나니까 한 동작에서 1/4초 넘게 줄였다는 걸 알 수 있었습니다. 나중에는 휴대용 기기의 스톱워치 기능을 써서 100ms 차이까지 잡아냈습니다. 코드에 (end - start)를 콘솔에 출력하는 코드를 처음 넣은 건 거기 들어간 지 두 달 가까이 지나서였습니다.

    제가 고칠 줄 아는 게 바닥나기 시작할 즈음에는 데이터가 쌓이면서 데이터 필터링 연산에 심각한 확장성 문제가 드러났고, 그래서 고전적인 아키텍처 잘못으로 돌아가게 됐습니다. 서로 다른 기준으로 스캔을 두 번 하고 그 결과를 이차 시간(quadratic time) 비교로 맞춰 보는, 2n logn² 짜리 교집합 테스트가 있었어요. 이 코드가 변수 이름과 파라미터 마샬링만 조금씩 다른 채로 프로젝트 곳곳에 수십 군데나 복붙되어 있었습니다. 연휴 주말 동안 곁에 아무도 없어서 그 복사본을 전부 filter(filter(x)) 하는 함수 하나로 교체했습니다. 코드는 500줄이 줄었고, 수년 치 데이터셋에서 호출 시간의 증가 기울기도 훨씬 완만해졌습니다.

  13. linsomniac HN

    제가 아는 가장 똑똑한 사람 중 한 명이 이런 말을 했습니다. 벤치마크를 오래 돌릴수록, 관련 없는 다른 활동까지 함께 잡혔을 가능성이 그만큼 커진다고요.

    그 사람 이론은 벤치마크를 짧은 시간씩 여러 번 돌리고, 가장 빠른 실행을 벤치마크 결과로 삼으라는 것이었습니다.

    저는 Python Need For Speed Sprint 때 벤치마크를 아주 많이 했는데, 그 조언이 그때 우리에게 잘 맞았던 것 같습니다.

  14. cb321 HN

    이게 기본적으로 https://github.com/c-blake/bu/blob/main/doc/tim.md 의 철학입니다. 다만 거기서는 dt 표본의 최솟값(어떤 의미에서 "조금" 높게 나올 수밖에 없는 값)보다는 좀 더 정교한 최솟값 추정량을 씁니다.

    그 문서에는 원글(TFA)의 200~400ms처럼 특정 시간을 목표로 하면서 문제 규모를 쉽지만 부주의하게 키울 때(각주 4의 벤 호이트 예시처럼) 생길 수 있는 측정상의 함정에 관한 다른 벤치마킹 요점도 있습니다.

    그 도구로 사용자 공간에서 CPU 주파수만 고정해 놓고 쓰면, 같은 머신에서 CPU 바운드 시간이 달마다 한 자릿수 마이크로초 단위로 안정적으로 나오는 걸 일상적으로 봅니다. 10ms 규모 작업에서 0.01%~0.2%의 효과(네, 1~20bps)도 보이고요. 보고되는 불확실성도 대체로 변동을 제대로 잡아냅니다. 다만 분포가 정말로 가우시안/정규분포는 아니라서 모양 매개변수가 더 필요하거나, 이 스레드 다른 곳에서 말한 것처럼 95% 신뢰구간 같은 걸 쓰고 싶어질 겁니다.

  15. vardump HN

    그런 식의 벤치마킹은 CPU 코어 클럭이 계속 조정되고, 시스템 인터럽트와 SMI 같은 것들이 끼어들어서 자주 망가집니다.

    하이퍼스레딩을 끄고, 스케일링 거버너, 부스트, CPU 주파수 고정까지 만져 가며 고쳐 보려 했지만 소용없었습니다. 지터가 너무 심해서 결과를 재현할 수 없어서 그냥 포기했어요.

    물론 환경에 따라 다를 수 있습니다. 제 경우는 AMD Zen 3 CPU였습니다.

  16. kccqzy HN

    인텔이 정밀한 벤치마크를 위한 가이드를 공개한 적이 있습니다. 요약하면 커널 안에서 코드를 실행하고, 인터럽트를 끄고, cpuid 명령으로 비순차 실행(out-of-order execution)을 막고, rdtsc 대신 rdtscp 명령을 쓰는 식입니다.

  17. ozgrakkurt HN

    prevent out-of-order execution

    그러면 결과가 부정확해지지 않나요?

  18. kccqzy HN

    벤치마크 대상 코드가 끝나기 전에 CPU가 종료 시각을 잡아 버리는 것, 또는 시작 시각을 잡기 전에 벤치마크 코드를 먼저 실행해 버리는 것만 막으려는 겁니다.

  19. marginalia_nu HN

    이걸 우회하는 방법은(시스템을 최대한 예측 가능하게 만드는 노력은 기본이고), 데이터에 노이즈가 있다는 걸 받아들인 다음 여러 번 시행하고 그 결과를 통계적으로 분석하는 것입니다.

    원하는 신뢰 수준에 따라 워밍업, 시행 횟수, 벤치마크 지속 시간 등을 정할 수 있습니다.

    분포가 다봉(multimodal)으로 나온다면 백분위수를 추적해 볼 만합니다.

  20. vardump HN

    워밍업 라운드도 돌려서 캐시 등을 데워 놨습니다. 통계도 이것저것 만져 봤는데, 제 기억으로는 단순 평균이 전반적으로 가장 나은 결과를 줬어요. 가장 낮은 n개 결과만 남기는 게 최선일 거라 생각했는데 틀린 생각이었습니다.

    결과는 실제로 다봉이었습니다.

    제가 원한 건 코드를 바꿀 때마다 성능이 시간에 따라 어떻게 달라지는지 볼 수 있는, 재현 가능한 벤치마크 하나뿐이었어요.

  21. tomsmeding HN

    저도 최근에 벤치마크를 하고 있는데, 특정 워크로드에서는 CPU 코어가 서로 동등하지 않다는 걸 확인했습니다. (5년 전 인텔 CPU이고, 스펙상으로는 모든 코어가 같습니다.) 아주 안 좋은 어떤 경우에는 (물리) 코어 0과 코어 5 사이에 재현 가능한 50% 성능 차이가 납니다.

    특히 거의 무작위에 가까운 메모리 접근이 많은 연산 코드를 보고 있다면 taskset으로 특정 코어에 고정해 두는 것도 고려해 보세요.

  22. hliyan HN

    2008년쯤에 10마이크로초 범위에서 돌아야 하는 연산(HFT 쪽)에 이런 작업을 하곤 했습니다. 결과가 아주 예측 가능했는데, 이유는 이렇습니다.

    a) 언어가 가비지 컬렉션이 없는 C++였고

    b) 시작 시점에 객체 풀을 미리 할당해서 핵심 경로에서 힙 락 경합을 피했고

    c) I/O 작업은 별도 스레드로 넘기고, 뮤텍스로 잠그는 연결 리스트로 연결했고

    d) 처리 스레드는 자기만의 CPU 코어에 고정했습니다.

    우리가 만들 수 있는 결정성(determinism)은 그 정도가 최대였습니다.

  23. vardump HN

    그런 건 전부 해 봤습니다. 지터를 줄이려고요. 제 생각에는 CPU 클럭이 안정적이지 않은 게 문제였던 것 같습니다.

    그리고 코어 0이 가장 시끄러워서 피했고, 테스트 워크로드에는 cgroups로 코어 고정도 했습니다.

    테스트 실행 중에는 페이지 폴트가 0이었고, 해당 CPU 코어를 다른 스레드가 경합하지도 않았습니다.

    2008년의 CPU는 지금처럼 전력과 발열 관리에 유난스럽지 않았던 것 같아요.

  24. justinhj HN

    아이디어가 마음에 듭니다. 기준선이 300ms인데 20배 빨라지는 기쁜 경우에는 잘 안 맞지만, 그럴 때는 그때 가서 조정하면 되겠죠.

    저는 글쓴이의 이전 아이디어 일부도 제 벤치마크에 넣었습니다. 이걸 보세요: https://matklad.github.io/2025/12/09/do-not-optimize-away.html

  25. spacedcowboy HN

    'xc'의 벤치마크는 "제" 컴파일러 이야기인데, 이질적(heterogeneous) 컴퓨팅(GPU와 CPU를 같은 언어로 쓰고, 둘 사이의 데이터 해저드는 컴파일러가 관리하는 방식)을 위한 것으로, 가장 오래 걸리는 쪽 기준으로 전부 1초에서 몇 초 정도입니다.

    그래서 특정 아키텍처(arm64나 x86_64)에서 (예를 들어) C++, Swift, ObjC와 비교한다면 그 정도 시간 단위로 보고 싶습니다. 조금 짧아도, 조금 길어도 괜찮고요.

    물론 (요즘은 [씩]) 2D 행렬 곱셈이 자동 벡터화되고, 대부분의 컴파일러가 활용하지 못하는 arm64의 SME/SME2 타깃이 있어서 clang/g++보다 150배 빨라지면, 비교할 만한 숫자를 얻으려고 좀 더 오래 돌려야 할 수도 있죠 :)

    1: https://compile-xc.org/compiler/performance/

  26. bhouston HN

    WebGPU용 마이크로벤치마크인 https://github.com/bhouston/webgpu-bench 를 만들면서 알게 된 건데, 통제할 수 없는 브라우저에서 돌릴 때는 정확한 결과를 얻으려면 10ms보다 오래 돌려야 합니다. 최신 브라우저는 기본적으로 가장 가까운 1ms 단위로 반올림합니다. [1] 그래서 브라우저 벤치마크는 보통 실행 시간을 50~100ms 정도 확보하려고 합니다.

    [1] https://developer.mozilla.org/en-US/docs/Web/API/Performance/now#security_requirements

  27. cchianel HN

    Java를 벤치마크한다면 Java Microbenchmark Harness( https://github.com/openjdk/jmh )가 있습니다. 이 도구는 다음과 같은 일을 해 줍니다.

    • 워밍업 실행을 여러 번 해서, 인터프리터 코드나 컴파일 시간이 아니라 JIT로 최적화된 코드를 벤치마크하게 합니다.

    • JVM 포크를 여러 개 만들어 JVM 실행 간 편차를 없앱니다.

    • 죽은 코드 제거를 막는 Blackhole, 설정 작업을 하고 상수 폴딩을 막는 State 같은 유틸리티를 제공합니다.

  28. 1a527dd5 HN

    criteria를 직접 만들지 마세요. 측정은 잘 확립된 프레임워크에 맡기세요. 예를 들어 dotnet 쪽에는 https://benchmarkdotnet.org/ 가 있습니다.

  29. hyperpape HN

    이 글은 https://xkcd.com/2400/ 와 함께 읽어야 합니다. 손쉬운 개선 지점을 쫓으면서, 한 번도 최적화된 적 없는 대상에서 3배나 10배 속도 향상을 찾는 중이라면 이 조언이 아마 괜찮을 겁니다.

    하지만 더 밀어붙일수록, 더 작은 개선을 찾아야 할수록, 이 조언은 믿고 의지할 수 없는 경험칙이 되어 갑니다.

  30. hinkley HN

    2% 미만의 성능 개선을 자랑한 프로젝트는 제가 본 것 중 딱 두 개뿐인데, crate와 v8의 포인터 압축 작업을 하던 사람이었습니다.

    저는 대체로 더 잘 알고 있었기 때문에, 변경 여섯 개에 걸쳐 15%처럼 종합한 숫자로 보고하거나, TTFB가 600ms에서 590ms로 줄었다는 식으로 밀리초 단위로 보고했습니다. 앞쪽 방식의 까다로운 점은 같은 호출 트리나 횡단 관심사에 속하는 변경끼리 묶어야 한다는 겁니다. 그래야 변경 하나를 추가할 때마다 늘어나는 테스트 범위가, 첫 변경으로 이미 치른 비용 위에 얹히는 작은 증분에 그칩니다. 본질적으로는 같은 상호작용에 대한 작은 변경 몇 개에 걸쳐서, 잡아내지 못한 회귀와 릴리스 프로세스 지연의 가능성을 분할 상환(amortize)하는 겁니다.

    작은 변경을 코드베이스 전체에 흩어 놓으면 테스트 비용과 결함이 빠져나갈 위험이 부풀어 오르기 때문입니다. 사람들이 점진적 개선 자체를 아예 막으려 드는 이유가 바로 이거죠.

    이걸 깨달은 프로젝트에서는 2년 동안 마일스톤마다 30%씩 개선을 내놨고, 그 뒤에 바닥을 긁기 시작했습니다. 서브시스템에서 서브시스템으로 옮겨 다니며 제가 고칠 줄 아는 건 전부 고쳤어요. 어떨 때는 변경이 세 개였고, 어떨 때는 열 개였습니다. 두 번째 순회는 깨달은 교훈을 안고 이전에 손댔던 영역으로 돌아가는 것이었지만, 주로 새 기능과 버그 수정에서 생긴 회귀를 찾는 일이었습니다.

Hacker News에서 보기 ↗