평범한 Windows에서 소프트 리얼타임을 최대한 구현하기 위한 실전 가이드

· 업데이트: · · Windows 개발, 소프트 리얼타임, 설계, 계측

수정 이력(7건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635093)
리뷰 지적에 대응해, 오늘 추가한 그림 중 폭이 너무 넓었던 것을 세로 구성으로 고치고, 일부 그림과 캡션 표현을 본문 기술에 맞춰 정확하게 했습니다. 본문 문장은 바꾸지 않았습니다.
원인 구도와 체크리스트 흐름을 그림으로도 따라갈 수 있도록 Mermaid 그림을 11개 추가했습니다(본문 500~750자당 1그림 규약에 맞춘 것입니다). 기존 그림에는 일련번호 캡션을 붙였습니다. 본문 문장은 바꾸지 않았습니다.
글 맨 앞에 "이 글의 지식 맵" 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건) 대응으로 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참조하세요.
주기 구간별로 어디부터 읽을지의 빠른 표를 결론 바로 뒤로 앞당기고, 6장과 겹치던 서술을 정리했습니다. 대상 독자와 코드 예제 언어의 역할을 맨 앞에 명시하고, SetProcessInformation의 C# 선언 예, 용어표 추가, WPR과 LatencyMon을 구하는 방법과 최소 절차를 더했습니다. 기존 숫자 예가 지표 읽는 법 설명용이며 실측값이 아님을 명시하고, 자기 환경에서 p99를 내는 절차를 추가했습니다.
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635092)

이 글은 Zenodo에 보관되어 있습니다. 항상 최신 버전으로 연결되는 DOI와 지금 보고 있는 버전에 고정된 DOI를 아래에 함께 제시합니다.

Go Komura (2026). 「평범한 Windows에서 소프트 리얼타임을 최대한 구현하기 위한 실전 가이드」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635092 https://comcomponent.com/ko/blog/windows-soft-realtime-practical-guide-natural/

DOI(최신 버전)
10.5281/zenodo.21635092
DOI(이 버전)
10.5281/zenodo.22217412

Windows에서 주기 처리, 음성 처리, 영상 처리, 계측, 장치 제어처럼 “늦으면 곤란한” 처리를 만들면 “Windows에서는 어렵지 않을까”라는 인상을 받기 쉽습니다. 이 인상은 반은 맞고 반은 틀립니다. Windows는 hard real-time OS가 아니지만, 설계, 구현, 계측, 운영을 제대로 다지면 soft real-time으로서 꽤 실용적인 상태까지 가져갈 수 있습니다.

이 글에서 다루는 것은 특별한 RTOS 확장이나 독자적인 커널 드라이버, 전용 컨트롤러를 전제하지 않는, 평범한 Windows 10 / 11입니다. 평소 쓰는 데스크톱 / 노트북 PC 위의 user-mode 앱에서, 지연과 지터를 어디까지 줄일 수 있는가라는 실무에 가까운 이야기입니다. 음성, 영상, 주기 제어, 데이터 취득은 세부에서 다르지만, 문제가 나기 쉬운 지점은 상당히 공통되므로, 이번에는 그 공통 부분을 체크리스트 형태로 정리했습니다.

대상 독자와 코드 예제의 언어

Windows에서 “늦으면 곤란한” 처리(주기 제어, 음성·영상, 계측, 장치 제어)를 만드는 개발자를 대상으로 씁니다. 가정하는 것은 user-mode 앱 개발이며, 커널 모드 드라이버 구현은 범위 밖입니다.

코드 예제 언어는 다음과 같이 나뉩니다.

내용 언어 위치
주기 루프, MMCSS, 전원 QoS 등 Win32 API를 직접 호출하는 부분 C++(Win32) 4.1, 4.3, 4.5
같은 Win32 API를 C#에서 호출하는 작성법 C#(P/Invoke) 4.5
시각 계측, GC, 할당 주의점 .NET(C#) 4.4의 “.NET 쪽 체크”, 5.2

3장까지의 원인 이야기와 4장의 체크리스트 본체는 언어에 의존하지 않습니다. C#만 쓰는 분은 C++ 코드를 “어떤 API를 어떤 순서로 호출하는가”의 설명으로 읽으면 충분합니다.

목차

  1. 먼저 결론 (한 줄로)
    • 1.1. 주기 구간별 빠른 표 (어디부터 읽을지)
  2. 평범한 Windows에서 “소프트 리얼타임”이란 무엇인가
    • 2.1. 이 글에서 말하는 “평범한 Windows”
    • 2.2. 무엇이 가능하고, 어디서부터 어려워지는가
    • 2.3. 용어를 먼저 한 줄로
  3. 지연과 지터의 주요 원인
    • 3.1. 스케줄러와 우선순위
    • 3.2. DPC / ISR과 드라이버
    • 3.3. 페이지 폴트와 메모리
    • 3.4. 타이머 분해능과 전원 관리
    • 3.5. 코어 이동과 열
  4. 평범한 Windows에서 지연을 줄이는 실전 체크리스트
    • 4.1. 주기 루프와 대기 방법
    • 4.2. fast path / slow path와 고정 길이 큐
    • 4.3. 우선순위 / MMCSS / background mode
    • 4.4. 메모리 / GC / 첫 실행 비용
    • 4.5. 전원 설정 / EcoQoS / timer resolution
    • 4.6. CPU 배치 / 코어 이동 / 열
    • 4.7. 드라이버 / DPC / ISR / 외란 분리
  5. 계측과 평가
    • 5.1. 무엇을 기록하는가
    • 5.2. p99 / p99.9 / max를 읽는 법
    • 5.3. 무엇으로 보는가
    • 5.4. 테스트 방법
  6. 대략적인 구분
  7. 정리
  8. 참고 자료

이 글의 지식 맵

평범한 Windows에서 소프트 리얼타임을 노리려면, 기한 위반 제로를 보증하는 하드 리얼타임을 요구하지 않고 지연과 지터를 작게 해서 기한을 벗어나도 무너지지 않는 설계로 만듭니다. 주기 대기는 QueryPerformanceCounter로의 계측과 고정밀 waitable timer(대기 가능 타이머)를 토대로 하며, 필요한 동안만 timeBeginPeriod로 타이머 분해능을 올립니다. 전원 스로틀링으로 EcoQoS로의 격하와 타이머 분해능 무시를 명시적으로 해제하지 않으면, DPC/ISR, page fault, 서멀 스로틀링과 함께 지터가 늘어나는 한 원인이 됩니다. 음성·영상 같은 연속 처리에서는 MMCSS가 우선적인 CPU 배분으로 기한 초과를 막고, CPU 배치는 CPU Sets 같은 느슨한 지정을 우선순위 높은 고정보다 먼저 시도해야 하며, 원인 분리에는 WPR/WPA와 LatencyMon을 사용합니다.

평범한 Windows의 소프트 리얼타임 지식 맵소프트 리얼타임을 뒷받침하는 waitable timer(대기 가능 타이머)나 MMCSS, 전원 스로틀링과 EcoQoS의 관계, DPC/ISR나 page fault가 지터를 낳는 경로, CPU Sets에서 CPU 고정으로 가는 단계, WPR/WPA와 LatencyMon에 의한 계측 수단의 관계를 보여주는 그림.이용한다이용한다이용한다양립하지 않는다이용한다방지한다원인이 될 수 있다이용한다완화한다완화한다원인이 될 수 있다사용은 비권장이용한다보다 먼저 해야 한다원인이 될 수 있다원인이 될 수 있다원인이 될 수 있다에서 확인할 수 있다에서 확인할 수 있다에서 확인할 수 있다소프트 리얼타임QPC(QueryPerformanceCounter)대기 가능 타이머(waitable timer)timeBeginPeriod(타이머 분해능 요청)Power Throttling(전력 조절)EcoQoS지터MMCSS(Multimedia Class Scheduler Service)데드라인 미스(기한 위반)스레드 우선순위/우선순위 클래스CPU SetsCPU 선호도서멀 스로틀링DPC/ISR페이지 폴트WPR/WPA(Windows Performance Recorder/Analyzer)LatencyMon

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

1. 먼저 결론 (한 줄로)

  • 평범한 Windows에서 목표로 하는 것은 hard real-time 보증이 아니라, soft real-time으로서 “늦기 어렵고, 늦어도 잘 깨지지 않는” 구성입니다.
  • 효과가 가장 큰 것은 hot path를 짧게·고정 길이로·non-blocking으로 만드는 것입니다.
  • fast path(취득 / 제어)와 slow path(저장 / 통신 / UI)를 나누고, 사이는 고정 길이 큐로 잇습니다.
  • 주기 루프는 Sleep(1)에 맡기지 않고 절대 기한으로 돌립니다.
  • 음성이나 영상처럼 연속 스트림에서는 먼저 MMCSS를 검토합니다.
  • 시간 계측은 QueryPerformanceCounter(QPC), .NET이라면 Stopwatch를 씁니다.
  • 대기는 디바이스 이벤트고정밀 waitable timer(대기 가능 타이머)를 우선합니다.
  • timeBeginPeriod는 필요한 동안만 씁니다. 항상 켜 둔다는 전제로 설계하지 않습니다.
  • 실운영에서는 AC 전원 / 전원 모드 / EcoQoS 다루기 / 백그라운드 부하 정리가 효과적입니다.
  • 평가는 평균값만이 아니라 p99(100번 재서 느린 쪽 1번이 보이기 시작하는 경계) / p99.9 / max / miss 횟수 / DPC / ISR / page fault / queue 깊이로 봅니다.

요컨대 평범한 Windows에서는 우선순위를 올리는 것보다, 늦는 이유를 설계로 줄이는 쪽이 더 잘 먹힙니다. 우선순위와 전원 설정은 중요하지만, 그것만으로 안정성은 만들어지지 않습니다.

안정성을 만드는 것은 우선순위보다 설계hot path를 짧게 고정 길이로 non-blocking으로 만들어 늦는 이유를 설계로 줄이는 것이 가장 효과적이고, 우선순위나 전원 설정은 중요하지만 그것만으로는 안정성을 만들 수 없다는 결론을 나타낸다.hot path를 짧게·고정 길이로늦는 이유가 설계에서 줄어든다늦기 어렵고 늦어도 깨지지 않는 구성우선순위나 전원 설정 조정중요하지만 그것만으로는 부족하다

그림1: 효과가 가장 큰 것은 우선순위 조정이 아니라, 늦는 이유 자체를 설계로 줄이는 것.

1.1. 주기 구간별 빠른 표 (어디부터 읽을지)

긴 글이므로 자기 사례에 해당하는 행만 읽을 수 있게 먼저 빠른 표를 둡니다. 예전에는 말미(6장)에 있던 내용을 여기로 앞당겼습니다.

주기·요구 먼저 짜는 구성 중점적으로 읽을 절
10~20ms급이고, 가끔의 흔들림은 흡수할 수 있다 fast path / slow path 분리, 고정 길이 큐, 보통~다소 높은 우선순위, 이벤트 구동. 이것으로 충분한 경우가 많다 4.1, 4.2
1~5ms급이고, 계속 맞춰 가고 싶다 위에 더해 hot path를 할당 없이 만드는 것, 전용 스레드, MMCSS 또는 신중한 우선순위 조정, 고정밀 waitable timer, AC 전원과 전원 설정 재검토 4.1 ~ 4.5
1ms 미만에 가까워지고, 장시간·고부하에서도 놓치고 싶지 않다 평범한 Windows의 user-mode 단독으로는 상당히 어렵다. 크리티컬한 부분을 다른 곳(디바이스 측 펌웨어, 전용 컨트롤러, FPGA, RTOS)으로 빼는 설계를 먼저 검토한다 2.2, 6
GUI / 로그 / 통신 / DB를 한 프로세스에 모두 함께 두고 싶다 “전부 1프로세스 1루프”로 끌어안지 말고 책임을 나눈다. 뒷단 사정이 앞단 기한을 깨기 쉽다 4.2, 4.3, 6

원인 분리와 계측 방법은 어느 구간에서든 공통입니다(3장과 5장).

2. 평범한 Windows에서 “소프트 리얼타임”이란 무엇인가

2.1. 이 글에서 말하는 “평범한 Windows”

여기서 말하는 평범한 Windows는 대략 다음을 전제로 합니다.

  • Windows 10 / 11의 일반적인 데스크톱 / 노트북 PC
  • 독자적인 RTOS 확장 없음
  • 독자적인 커널 모드 드라이버 개발 없음
  • 평범한 user-mode 앱
  • 일반적인 Windows API와 설정으로 조정한다

“전용기를 한 대 통째로 리얼타임 제어용으로 만드는” 이야기가 아니라, “평범한 Windows PC 위에서 어디까지 현실적으로 밀어붙일 수 있는가”라는 이야기입니다.

평범한 Windows 10 / 11 PCuser-mode 앱soft real-time을 목표로 한다지연을 낮춘다지터를 작게 한다deadline miss를 관측하고 깨지지 않게 한다기한 위반 제로를 보증하고 싶다RTOS / 전용 컨트롤러 / FPGA / 디바이스 측 처리

그림2: 평범한 Windows의 user-mode 앱에서 노리는 것은 soft real-time이며, 기한 위반 제로 보증은 RTOS 등의 영역이다.

2.2. 무엇이 가능하고, 어디서부터 어려워지는가

평범한 Windows에서도, 예를 들어 이 정도의 처리라면 상당히 현실적으로 “늦기 어려운” 상태를 만들 수 있습니다.

  • 수 밀리초~수십 밀리초의 주기 처리
  • 음성 / 영상 버퍼 구동
  • 센서 취득과 제어 루프
  • 소프트 PLC 풍의 일정 주기 처리
  • UI와 다른 스레드에서 도는 저지연 파이프라인

다만 여기서 말하는 “할 수 있다”는 가끔의 지연 스파이크를 완전히 제로로 만들 수 있다는 뜻이 아니며, 노리는 것은 어디까지나 이런 상태입니다.

  • 평상시 지연을 낮춘다
  • 지터를 작게 한다
  • 가끔 기한을 놓쳐도 깨지지 않는다
  • 놓친 사실을 관측할 수 있다

반대로 다음 같은 요구가 되면, 평범한 Windows의 user-mode만으로 충족하기는 상당히 어려워집니다.

  • 기한 위반 제로를 보증하고 싶다
  • 수백 마이크로초 이하를 장시간 안정적으로 지키고 싶다
  • 무거운 GUI, 네트워크, 스토리지와 함께 돌리고 싶다
  • 배터리 구동이나 절전 우선 그대로 하고 싶다
  • 드라이버나 디바이스에서 오는 스파이크도 허용되지 않는다

이 지점은 정말로 시간에 엄격한 부분만 디바이스 측 펌웨어, 전용 컨트롤러, FPGA, RTOS로 옮기는 것도 생각하는 편이 안전합니다.

soft real-time에서 노리는 상태와 밖으로 빼는 판단평범한 Windows에서 노리는 것은 평상시 지연을 낮추고 지터를 작게 하며, 가끔 기한을 놓쳐도 깨지지 않고 놓친 사실을 관측할 수 있는 상태이며, 기한 위반 제로 같은 엄격한 요구는 시간에 엄격한 부분만 밖으로 옮기는 것을 나타낸다.soft real-time에서 노리는 상태평상시 지연을 낮게지터를 작게놓쳐도 깨지지 않고 관측할 수 있다기한 위반 제로 요구디바이스나 RTOS로 옮긴다

그림3: “할 수 있다”의 내용은 스파이크 제로가 아니라, 늦기 어렵고·깨지기 어렵고·관측할 수 있는 상태라는 뜻이다.

2.3. 용어를 먼저 한 줄로

이 글에 나오는 용어의 뜻을 먼저 잡아 둡니다.

용어 한 줄로 말하면 실무에서의 보는 법
soft real-time 가끔의 지연은 있을 수 있지만, 지연을 작게 하고 늦어도 깨지지 않게 하는 생각 평범한 Windows에서 먼저 노리는 것은 이것
hard real-time 기한 위반 제로를 보증하고 싶은 세계 평범한 Windows의 user-mode 단독으로 노리는 대상이 아니다
지터 주기나 응답 시간의 흔들림 평균이 좋아도 지터가 크면 실운영에서는 불안정
deadline miss 예정 시각까지 처리가 끝나지 않는 것 숨기지 말고 세고, 로그에 남긴다
p99 / p99.9 느린 쪽 꼬리를 보는 지표 p99는 “100번 중, 느린 쪽 1번이 보이기 시작하는 경계”
DPC / ISR 드라이버나 인터럽트 주변의 커널 측 처리 길면 user-mode 스레드는 기다리게 된다
MMCSS 음성 / 영상처럼 시간에 민감한 처리에 CPU를 배분하는 Windows 메커니즘 버퍼를 끊고 싶지 않은 처리에서 유력
QPC QueryPerformanceCounter를 가리킨다 경과 시간 계측의 기본. 벽시계가 아니라 고정밀 카운터
waitable timer(대기 가능 타이머) 지정 시각에 signaled가 되는 커널 객체. CreateWaitableTimerExWCREATE_WAITABLE_TIMER_HIGH_RESOLUTION을 붙이면 고정밀 버전이 된다 Sleep보다 주기 대기의 토대로 맞다(4.1)
EcoQoS “절전을 우선해도 된다”고 분류된 상태. CPU 주파수를 낮추거나 전력 효율이 좋은 코어로 몰린다 시간에 민감한 처리에서는 피한다. 명시하지 않으면 OS가 자동으로 추측한다(4.5)
IGNORE_TIMER_RESOLUTION 프로세스의 타이머 분해능 요구를 무시해도 된다는 지정(PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION) 이것이 켜지면 timeBeginPeriod 효과가 사라진다. Windows 11에서는 화면에서 가려지면 자동으로 적용되는 경우가 있다(4.5)
CPU Sets 스레드나 프로세스를 “이 코어 그룹에서 돌아 주었으면 한다”고 부드럽게 지정하는 메커니즘 특정 코어 고정보다 먼저 시도한다(4.6)
ETW / WPR / WPA Windows 표준 트레이스 기반(ETW)과, 그 기록 도구(WPR)·분석 GUI(WPA) context switch, DPC / ISR, page fault를 파고들 때 쓴다(5.3)
LatencyMon 드라이버 기인 지연을 보기 위한 서드파티 도구 DPC / ISR 실행 시간을 드라이버별로 보며 감을 잡는다(5.3)

3. 지연과 지터의 주요 원인

평범한 Windows에서 주기 처리가 늦는 이유는, 대략 이 그림의 어딘가로 귀결됩니다.

주기 처리가 늦는다스케줄러 / 우선순위DPC / ISR / 드라이버페이지 폴트 / 메모리타이머 분해능 / 전원 관리코어 이동 / 열

그림4: 주기 처리가 늦는 원인은 스케줄러, DPC/ISR, 메모리, 타이머와 전원, 코어 이동과 열의 5계통으로 귀결된다.

3.1. 스케줄러와 우선순위

Windows 스레드는 우선순위로 실행 순서가 정해집니다. 같은 우선순위면 라운드로빈으로 돌고, 더 높은 우선순위의 스레드가 실행 가능해지면 낮은 우선순위 스레드는 밀려납니다.

즉 주기 스레드를 성실하게 써도,

  • 다른 스레드
  • 다른 프로세스
  • OS 내부 처리
  • 보안 제품
  • 디바이스 보조 처리
  • 백그라운드 동기화

가 먼저 도는 일은 흔합니다.

우선순위 스케줄링에 의한 밀어내기같은 우선순위 스레드는 라운드로빈으로 순회하고, 더 높은 우선순위 스레드가 실행 가능해지면 낮은 우선순위 스레드는 밀려나므로, 주기 스레드 앞에 다른 프로세스나 OS 내부 처리가 도는 일은 흔하다는 것을 나타낸다.같은 우선순위끼리라운드로빈으로 순회한다더 높은 우선순위가 실행 가능해진다낮은 우선순위는 밀려난다다른 프로세스나 OS 내부 처리 등

그림5: 주기 스레드를 성실하게 써도, 더 높은 우선순위 일이 먼저 도는 것은 흔한 일이다.

3.2. DPC / ISR과 드라이버

여기는 상당히 중요합니다. 앱 측 우선순위를 맞춰도 DPC(Deferred Procedure Call)나 ISR(Interrupt Service Routine)이 길면, 그동안 user-mode 스레드는 실행할 수 없습니다.

원인이 되기 쉬운 것은 이런 디바이스와 드라이버입니다.

  • USB
  • Wi-Fi / Bluetooth
  • 스토리지
  • 오디오
  • GPU
  • ACPI / 전원 주변

앱 코드가 나쁘지 않아도, 드라이버나 하드웨어 사정으로 멈출 수 있습니다. 여기는 “앱 우선순위를 더 올리면 이길 수 있겠지”라고 생각하면 대체로 고생합니다.

DPC와 ISR이 user-mode를 멈추는 구도USB나 Wi-Fi나 GPU 같은 디바이스와 드라이버의 인터럽트 처리인 ISR이나 DPC가 길면, 그동안 user-mode 스레드는 실행할 수 없고, 앱 우선순위를 올려도 이길 수 없음을 나타낸다.USB·Wi-Fi·GPU 등의 디바이스ISR이나 DPC가 커널 측에서 돈다그동안 user-mode는 실행할 수 없다우선순위를 올려도 이길 수 없다

그림6: 앱 코드가 나쁘지 않아도, 드라이버나 하드웨어 사정으로 user-mode는 멈출 수 있다.

3.3. 페이지 폴트와 메모리

hot path에서 page fault(필요한 페이지가 메모리에 없어 가져오러 가는 것)가 일어나면 지연이 한순간에 커집니다.

특히 피하고 싶은 패턴을 적어 둡니다.

  • 첫 접근에서의 페이지 커밋
  • 지연 로드
  • 메모리 맵 파일의 페이지인
  • 필요 이상의 동적 할당
  • 큰 객체나 파편화된 힙

주기 처리 본체에서는 필요한 메모리를 미리 확보하고, 시작 시 한 번 접근해 두는 정도가 알맞습니다.

hot path에서의 page fault에 의한 지연hot path에서 접근한 페이지가 메모리에 없으면 page fault로 가져오러 가게 되어 지연이 한순간에 커지므로, 필요한 메모리를 미리 확보하고 시작 시 한 번 접근해 두는 것이 좋음을 나타낸다.있다없다hot path에서 메모리에 접근한다페이지는 메모리에 있는가그대로 진행한다page fault로 가져오러 간다지연이 한순간에 커진다미리 확보하고 시작 시 한 번 접근한다

그림7: page fault가 일어나는지에 따라 지연이 뛴다. 대책은 사전 확보와 기동 시 워밍업이다.

3.4. 타이머 분해능과 전원 관리

“1ms마다 돌리고 싶으니 Sleep(1)“은 대부분의 경우 잘되지 않습니다. Windows의 대기 정밀도는 타이머 분해능, 스케줄링, 전원 상태의 영향을 받습니다.

게다가 타이머 분해능을 올리는 설정은 대기 정밀도를 조금 개선할 수 있는 한편, 소비 전력이나 시스템 전체 동작에 부작용이 있다는 점도 놓치면 안 됩니다.

3.5. 코어 이동과 열

스레드가 코어 사이를 옮기면 캐시를 다시 워밍업하게 됩니다. 이것 자체는 OS가 잘 처리하는 경우도 많지만, 부하가 높은 환경에서는 흔들림의 원인이 됩니다.

게다가 오래 돌리면 열도 무시할 수 없습니다. 서멀 스로틀링이 들어가면, 그동안 안정하던 주기가 무너지는 일이 있습니다.

코어 이동과 열이 주기를 무너뜨리는 경로스레드가 코어 사이를 옮기면 캐시 워밍업이 다시 일어나 고부하 환경에서는 흔들림의 원인이 되고, 장시간 운전으로 열이 쌓이면 서멀 스로틀링으로 그동안 안정하던 주기가 무너짐을 나타낸다.스레드가 코어 사이를 옮긴다캐시를 다시 워밍업한다고부하 환경에서는 흔들림의 원인장시간 운전으로 열이 쌓인다서멀 스로틀링안정하던 주기가 무너진다

그림8: 코어 이동은 지터로, 열은 스로틀링으로, 장시간 운전의 안정성을 깎는다.

4. 평범한 Windows에서 지연을 줄이는 실전 체크리스트

여기부터가 실무 파트입니다. 앞 절에서 본 원인에 대해 평범한 Windows에서 무엇을 확인하고, 무엇을 피하고, 무엇을 먼저 정해야 하는지를 체크리스트 형태로 정리합니다.

4.1. 주기 루프와 대기 방법

먼저 전형적인 안티패턴은 이것입니다.

while (running)
{
    Sleep(1);
    Step();
}

이것은 “1ms 주기”가 아니라, 대략 1ms 이상 기다린 뒤, 그 위에 Step()의 실행 시간을 더하는 루프입니다. 게다가 대기 overshoot가 그대로 누적됩니다.

절대 기한 기반상대 시간 기반WaitUntil(next - margin)next += period필요하면 짧은 spinFastStep()Step()Sleep(1)대기 오차와 실행 시간이 조금씩 쌓인다드리프트를 쌓기 어렵다

그림9: Sleep(1)에 의존한 상대 시간 루프는 오차가 쌓이고, 절대 기한으로 도는 루프는 드리프트를 쌓기 어렵다.

체크리스트

  • Sleep(1)을 주기 루프의 토대로 쓰지 않는다
  • 주기는 next += period절대 기한으로 돌린다
  • 대기는 디바이스 이벤트waitable timer(대기 가능 타이머)를 우선한다
  • 마지막 미세 조정만, 아주 짧은 busy-spin(공회전 대기)으로 한정한다
  • timeBeginPeriod는 필요한 동안만 쓰고, 끝나면 되돌린다
  • 최소화 / 숨김 / 보이지 않는 상태에서도 동작을 확인한다

주기 루프는 상대 시간이 아니라 절대 기한으로 돌리는 편이 안정적입니다.

int64_t next = QpcNow() + periodTicks;

while (running)
{
    WaitUntil(next - wakeMarginTicks);

    while (QpcNow() < next)
    {
        CpuRelax(); // 마지막만 짧게 spin
    }

    int64_t started = QpcNow();
    FastStep();
    int64_t finished = QpcNow();

    RecordTiming(next, started, finished);

    next += periodTicks;

    while (finished > next)
    {
        ++missedDeadlines;
        next += periodTicks;
    }
}

4.2. fast path / slow path와 고정 길이 큐

구성의 기본은 fast path에는 “기한에 민감한 일”만 두고, 나머지는 slow path로 내보내는 것입니다.

디바이스 / 취득 이벤트fast path: 취득·제어·최소한의 복사고정 길이 큐slow path: 저장·송신·UI·집계lateness / miss / queue depth를 기록

그림10: fast path에는 기한에 민감한 일만 두고, 고정 길이 큐를 끼워 slow path로 내보내는 기본 구성.

fast path에서 할 일은 이 정도로 좁힙니다.

  • 데이터 취득
  • 제어값 계산
  • 필요 최소한의 복사
  • 타임스탬프
  • 큐 투입
  • miss / overrun 기록

나머지는 slow path로 내립니다.

체크리스트

  • hot path에서 파일 쓰기, 네트워크 송신, DB 쓰기를 하지 않는다
  • hot path에서 무거운 로그 출력, Flush, 동기 RPC를 하지 않는다
  • fast path / slow path를 스레드나 책임으로 명확히 나눈다
  • 큐는 고정 길이로 한다
  • 큐가 넘쳤을 때의 방침을 먼저 정한다
  • miss 횟수, drop 수, queue depth를 관측한다
  • UI 갱신이나 로그 집약은 낮은 주기로 분리한다

큐가 가득 찼을 때는 방침을 모호하게 두지 않는 편이 안전합니다.

최신값이 중요하다모든 항목이 중요하다로그 용도큐가 가득 참무엇을 지킬 것인가?오래된 요소를 버리고 최신을 남긴다알림 / 정지 / 상류 제어오래된 요소를 버리고 drop 수만 기록

그림11: 큐가 가득 찼을 때 무엇을 지킬지는 용도별로 먼저 정해 둔다.

4.3. 우선순위 / MMCSS / background mode

우선순위의 기본은 전부를 올리지 않는 것입니다. 평범한 Windows에서는 “중요한 스레드만 올리고, 뒷일은 제대로 내린다”가 잘 먹힙니다. background mode는 CPU만이 아니라 I/O 등 자원도 낮은 우선순위 쪽으로 다루기 위한 메커니즘입니다.

일을 나눈다기한에 민감한 스레드저장 / 송신 / 압축 / 집계UI필요하면 높은 우선순위 or MMCSSbackground mode / 낮은 우선순위보통 우선순위처음부터 REALTIME_PRIORITY_CLASS로는 하지 않는다

그림12: 기한에 민감한 스레드만 올리고, 뒷일은 제대로 내리는 우선순위 할당 방식.

체크리스트

  • 모든 스레드를 높은 우선순위로 두지 않는다
  • 정말로 시간이 빠듯한 스레드만 올린다
  • 저장, 송신, 압축, 동기화 같은 뒷일은 background mode로 내린다
  • 음성, 영상, 캡처, 재생처럼 연속 버퍼 처리에서는 MMCSS를 검토한다
  • 프로세스 전체보다 먼저 스레드 단위로 생각한다
  • REALTIME_PRIORITY_CLASS는 필요성이 분명해질 때까지 쓰지 않는다

MMCSS(Multimedia Class Scheduler Service)는 음성 / 영상처럼 “일정 시간 안에 버퍼를 채우고 싶은” 처리에서 특히 유효합니다. 단순히 높은 우선순위 스레드를 항상 돌리는 것보다 Windows 설계에 맞습니다.

코드 분위기는 이런 식입니다.

DWORD taskIndex = 0;
HANDLE avrt = AvSetMmThreadCharacteristicsW(L"Pro Audio", &taskIndex);
if (!avrt)
{
    throw std::runtime_error("AvSetMmThreadCharacteristicsW failed");
}

// 시간에 민감한 루프를 돌린다

if (!AvRevertMmThreadCharacteristics(avrt))
{
    throw std::runtime_error("AvRevertMmThreadCharacteristics failed");
}
MMCSS를 쓰는 스레드의 등록과 해제시간에 민감한 루프 앞에 AvSetMmThreadCharacteristicsW로 MMCSS에 등록하고, 루프가 끝나면 AvRevertMmThreadCharacteristics로 되돌리는 흐름과, MMCSS가 시간에 민감한 처리에 CPU를 배분함을 나타낸다.AvSetMmThreadCharacteristicsW시간에 민감한 루프를 돌린다AvRevertMmThreadCharacteristicsMMCSS가 CPU를 우선 배분한다

그림13: MMCSS는 등록과 해제의 짝으로 쓴다. 연속 버퍼 처리라면 항상 높은 우선순위로 도는 것보다 Windows 설계에 맞다.

4.4. 메모리 / GC / 첫 실행 비용

hot path에서 매번 new / malloc / List<T>.Add / 문자열 연결 / LINQ를 쓰면, 언젠가 회수나 재배치 사정이 겉으로 나옵니다. GC(가비지 컬렉션) 자체가 나쁜 것은 아니지만, 할당이 많은 코드를 쓰면 그 영향은 지터로 표면화합니다.

기동필요 버퍼를 확보한 번 접근해 페이지를 워밍업한다JIT / DLL 로드 / 첫 I/O를 끝낸다그다음에 본 계측 / 본 운전

그림14: 시작 시 버퍼 확보·페이지 워밍업·첫 실행 비용을 끝낸 뒤, 본 계측·본 운전에 들어간다.

체크리스트

  • hot path에서 매번 메모리 할당 / 해제를 하지 않는다
  • 필요한 버퍼를 기동 때 사전 확보한다
  • 시작 시 한 번 접근해 페이지를 워밍업한다
  • 첫 JIT, 첫 DLL 로드, 첫 I/O를 본 계측에 섞지 않는다
  • 루프 안에서 거대한 구조나 가변 길이 로그를 키우지 않는다
  • VirtualLock을 쓰더라도 아주 작은 크리티컬 영역만으로 한정한다

.NET 쪽 체크

  • 시각 계측에 Stopwatch / Stopwatch.GetTimestamp()를 쓴다
  • hot path에서 LINQ, 문자열 연결, ToString(), 거대한 로그 생성을 하지 않는다
  • async/await를 hot path에 끌어들이지 않는다
  • 워밍업 전과 후를 나누어 평가한다

4.5. 전원 설정 / EcoQoS / timer resolution

눈에 잘 띄지 않지만 효과가 있습니다. 코드를 다져도 상위 전원 제어가 강하게 먹으면 결과는 안정되지 않습니다.

평범한 Windows의 전원 관련AC 전원으로 실행전원 모드: 최적 성능 쪽필요하면 운영용 전용 전원 플랜시간에 민감한 프로세스는 EcoQoS를 피한다타이머 해상도 요구 처리를 확인

그림15: 전원 쪽에서 확인할 점. 전원 공급, 전원 모드, EcoQoS, 타이머 해상도 요구 처리.

체크리스트

  • 운영 평가는 먼저 AC 전원으로 한다
  • [설정] > [시스템] > [전원 & 배터리] > [전원 모드]최적 성능 쪽으로 둔다
  • battery saver / 절전 우선 모드를 실행 중에 쓰지 않는다
  • 벤더 독자 유틸리티의 저소음 / eco / battery 우선 모드를 확인한다
  • 시간에 민감한 프로세스를 함부로 EcoQoS(절전 쪽 QoS)로 두지 않는다
  • IGNORE_TIMER_RESOLUTION이 시간에 민감한 프로세스 쪽에서 켜져 있지 않다
  • 최소화 / 숨김 때 타이머 분해능 요구의 효과가 바뀌지 않는지 확인한다
  • 평소용과 운영 / 계측 / 데모용 전원 설정을 나눈다

timeBeginPeriod는 정리해서 쓰면 도움이 되지만, 만능약은 아닙니다.

  • 필요한 직전에 호출한다
  • 끝나면 timeEndPeriod로 되돌린다
  • Windows 10 version 2004 이후는 예전 같은 완전한 전역 동작이 아니다
  • Windows 11에서는 창을 가진 프로세스가 완전히 가려지거나 / 최소화되거나 / 보이지 않거나 / 들리지 않는 상태면, 높은 분해능이 보증되지 않는 경우가 있다
  • 분해능을 올려도 QPC의 정밀도가 올라가는 것은 아니다

전원이나 QoS 영향이 의심되면 SetProcessInformation으로 power throttling 상태를 확인합니다.

PROCESS_POWER_THROTTLING_STATE state{};
state.Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
state.ControlMask =
    PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
    PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION;
state.StateMask = 0; // HighQoS(성능 우선 쪽) + 타이머 분해능 요구를 존중

if (!SetProcessInformation(
        GetCurrentProcess(),
        ProcessPowerThrottling,
        &state,
        sizeof(state)))
{
    throw std::runtime_error("SetProcessInformation failed");
}

ControlMask는 “어떤 메커니즘을 스스로 제어할지”, StateMask는 “그 메커니즘을 on 할지 off 할지”입니다. 위 예는 두 메커니즘을 제어 대상으로 고른 뒤 둘 다 off로 둡니다. 즉 EcoQoS로 떨어뜨리지 않는다(HighQoS 쪽)는 것과, 타이머 분해능 요구를 무시시키지 않는다는 선언입니다. 반대로 ControlMask0으로 하면 둘 다 OS에 맡기는(기본 동작) 상태로 돌아갑니다.

power throttling 설정의 두 마스크 역할ControlMask로 스스로 제어할 메커니즘을 고르고, StateMask로 그 메커니즘을 on 할지 off 할지를 정해 EcoQoS로 떨어뜨리지 않는 것과 타이머 분해능 요구를 무시시키지 않는 것을 선언할 수 있음을 나타낸다.ControlMask로 제어 대상을 고른다StateMask로 on과 off를 정한다EcoQoS로 떨어뜨리지 않는다는 선언타이머 분해능 요구를 무시시키지 않는다ControlMask가 0이면 OS에 맡긴다

그림16: SetProcessInformation의 두 마스크 역할. 제어 대상을 고른 뒤 on / off를 선언한다.

C#에서 호출하는 경우

같은 일을 C#에서 하려면 P/Invoke 선언은 이렇게 됩니다.

// C# / .NET 8
using System.Runtime.InteropServices;

internal static class PowerQos
{
    [StructLayout(LayoutKind.Sequential)]
    private struct PROCESS_POWER_THROTTLING_STATE
    {
        public uint Version;
        public uint ControlMask;
        public uint StateMask;
    }

    private const uint PROCESS_POWER_THROTTLING_CURRENT_VERSION = 1;
    private const uint PROCESS_POWER_THROTTLING_EXECUTION_SPEED = 0x1;
    private const uint PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 0x4;

    // PROCESS_INFORMATION_CLASS의 5번째(0부터 세면 4)가 ProcessPowerThrottling
    private const int ProcessPowerThrottling = 4;

    [DllImport("kernel32.dll", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool SetProcessInformation(
        IntPtr hProcess,
        int processInformationClass,
        ref PROCESS_POWER_THROTTLING_STATE processInformation,
        uint processInformationSize);

    [DllImport("kernel32.dll")]
    private static extern IntPtr GetCurrentProcess();

    /// <summary>절전 쪽 다루기와 타이머 분해능 요구 무시를 둘 다 해제합니다.</summary>
    public static void OptOutOfPowerThrottling()
    {
        var state = new PROCESS_POWER_THROTTLING_STATE
        {
            Version = PROCESS_POWER_THROTTLING_CURRENT_VERSION,
            ControlMask =
                PROCESS_POWER_THROTTLING_EXECUTION_SPEED |
                PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION,
            StateMask = 0,
        };

        if (!SetProcessInformation(
                GetCurrentProcess(),
                ProcessPowerThrottling,
                ref state,
                (uint)Marshal.SizeOf<PROCESS_POWER_THROTTLING_STATE>()))
        {
            throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
        }
    }
}

호출은 시간에 민감한 루프를 시작하기 전에 PowerQos.OptOutOfPowerThrottling();을 한 번 실행합니다. 프로세스 핸들에는 PROCESS_SET_INFORMATION 접근 권한이 필요하지만, GetCurrentProcess()가 돌려주는 자기 프로세스의 의사 핸들이면 문제 없습니다.

4.6. CPU 배치 / 코어 이동 / 열

CPU 배치는 처음부터 특정 코어에 고정(hard affinity / CPU pinning)하기보다, “되도록 이 코어 그룹에서 돌아 주었으면 한다”는 soft affinity에 가까운 지정부터 시작하는 편이 잘되는 경우가 많습니다.

아니요먼저 계측SetThreadIdealProcessor / CPU Sets충분히 개선되었는가?거기서 멈춘다마지막에 SetThreadAffinityMask를 검토동시에 온도 / 클럭 / 장시간 운전도 확인

그림17: CPU 배치는 계측부터 시작하고, 느슨한 지정으로 충분하면 거기서 멈추며, 특정 코어 고정은 마지막 수단으로 둔다.

체크리스트

  • 먼저 계측한 뒤에 CPU 배치를 건드린다
  • 처음부터 특정 코어에 고정하지 않는다
  • 먼저 SetThreadIdealProcessor나 CPU Sets를 시험한다
  • SetThreadAffinityMask는 마지막 수단으로 다룬다
  • 장시간 운전에서 온도, 클럭, 서멀 스로틀링을 확인한다
  • 노트북의 저소음 모드나 Quiet 모드를 확인한다

순서로는 이 흐름이 안전합니다.

  1. 먼저 계측한다
  2. 필요하면 ideal processor / CPU Sets
  3. 그래도 개선이 필요하면 특정 코어 고정

특정 코어 고정은 효과가 있어 보이지만, OS의 피할 길을 줄이므로 쉽게 쓰면 오히려 OS가 유연하게 대응하기 어려워질 수 있습니다.

4.7. 드라이버 / DPC / ISR / 외란 분리

“가끔 max만 폭발한다”“평균은 좋은데 p99.9가 나쁘다”일 때는 앱 코드 밖의 외란도 의심하는 편이 좋습니다.

아니요아니요아니요아니요late / miss / max spike가 나왔다자기 처리 시간도 긴가?hot path 단축 / 할당 축소 / I/O 제거DPC / ISR 스파이크가 있는가?USB / Wi-Fi / Bluetooth / GPU / Audio / Storage / ACPI / 드라이버 업데이트를 확인page fault / GC / 첫 실행 비용이 있는가?사전 확보 / 워밍업 / 힙 부하 축소배터리 / 절전 / 열의 영향이 있는가?AC 전원 / 전원 설정 / 냉각 / 장시간 테스트ETW / WPA / LatencyMon으로 깊게 판다

그림18: 스파이크가 났을 때의 분리 순서. 자기 처리, DPC/ISR, 메모리, 전원과 열 순으로 의심한다.

체크리스트

  • Wi-Fi / Bluetooth / USB / 스토리지 / GPU / 오디오 주변 드라이버를 확인한다
  • 불필요한 클라우드 동기화, 인덱싱, 자동 업데이트를 멈추고 비교한다
  • 최소화하면 무너지는지, 화면을 끄면 무너지는지도 시험한다
  • LatencyMon이나 ETW로 DPC / ISR 경향을 본다
  • “자기 처리가 무거운지”와 “밖에서 멈춰 있는지”를 나누어 본다

5. 계측과 평가

5.1. 무엇을 기록하는가

최소한 이것만은 남겨 두고 싶은 부분입니다.

  • 주기 예정 시각
  • 실제 시작 시각
  • 실제 종료 시각
  • lateness(예정 시작에 비해 얼마나 늦게 시작했는지)
  • 실행 시간
  • missed deadline 수
  • 연속 missed deadline 수
  • queue depth
  • drop 수
  • CPU 사용률
  • 코어별 치우침
  • DPC / ISR 스파이크
  • page fault
  • 온도 / 클럭 변동

평균만 봐서는 본질을 잡기 어렵습니다. 운영에서 곤란한 것은 가끔 나오는 큰 지연 스파이크입니다.

5.2. p99 / p99.9 / max를 읽는 법

p99 같은 지표는 느린 쪽 꼬리를 보기 위한 것입니다. 평균만으로는 가끔 나오는 큰 지연이 가려집니다.

지표 의미 10,000번 쟀을 때의 이미지
평균 전체의 고른 값 스파이크가 묻히기 쉽다
p50 가운데 값 평소 체감에 가깝다
p95 느린 쪽 5%가 보이기 시작하는 경계 느린 500번을 뺀 경계
p99 느린 쪽 1%가 보이기 시작하는 경계 느린 100번을 뺀 경계
p99.9 느린 쪽 0.1%가 보이기 시작하는 경계 느린 10번을 뺀 경계
max 최악값 가장 늦었던 1번

예를 들어 다음처럼 나왔다고 합시다(지표 읽는 법을 설명하기 위한 예이며, 특정 장비에서의 실측값이 아닙니다).

  • 평균: 0.8ms
  • p99: 1.2ms
  • p99.9: 3.5ms
  • max: 28ms

이것은 평소에는 빠르지만, 가끔 큰 스파이크가 있는 상태입니다. 평범한 Windows에서는 대체로 이 p99부터 max의 꼬리에 진짜 문제가 드러납니다.

평균과 꼬리 쪽 지표의 차이평균만 보면 가끔 나오는 큰 지연이 가려지는 반면, p99나 p99.9나 max를 보면 느린 쪽 꼬리가 보이고, 평범한 Windows에서는 진짜 문제가 거기에 드러남을 나타낸다.평균만 본다가끔의 큰 지연이 가려진다p99·p99.9·max를 본다느린 쪽 꼬리가 보인다진짜 문제는 p99부터 max에 드러난다

그림19: 평균이 좋아도 max가 뛰면 “평소에는 빠르지만 가끔 뛴다” 상태. 꼬리 쪽 지표로 잡는다.

참고로 이 글에서는 권장 구성의 효과를 숫자로 보여 주지 않습니다. 지연과 지터는 CPU, 드라이버, 상주 소프트웨어, 전원 설정, 부하 거는 방식에 따라 쉽게 바뀌므로, 남의 환경 숫자를 그대로 자기 환경의 근거로 쓸 수 없습니다. 대신 자기 환경에서 같은 형태의 표를 만드는 절차를 둡니다.

자기 환경에서 p99를 내는 최소 절차

  1. hot path에서는 기록만 합니다. Stopwatch.GetTimestamp()(C++라면 QueryPerformanceCounter)로 lateness와 실행 시간을 취해, 사전 확보한 배열에 씁니다. 여기서 평균이나 정렬을 계산하면 안 됩니다
  2. 계측을 멈춘 뒤에 집계합니다. 정렬해서 퍼센타일 위치의 값을 꺼냅니다
  3. 같은 절차를 조건을 바꿔 반복합니다. 워밍업 전후, AC / 배터리, UI 앞면 / 최소화, 다른 프로세스 부하 있음 / 없음(5.4)
  4. 변경을 하나 넣을 때마다 같은 조건으로 다시 재서 비교합니다
// C# / .NET 8. 집계는 계측을 멈춘 뒤에 한다
using System.Diagnostics;

// hot path에서는 배열에 쓰기만 한다(할당 제로)
long[] latenessTicks = new long[100_000];
int count = 0;

// 예: 주기 루프 안
// latenessTicks[count++] = Stopwatch.GetTimestamp() - scheduledTimestamp;

static double PercentileMs(long[] ticks, int count, double percentile)
{
    long[] sorted = ticks.AsSpan(0, count).ToArray();
    Array.Sort(sorted);

    int index = (int)Math.Ceiling(percentile / 100.0 * count) - 1;
    index = Math.Clamp(index, 0, count - 1);

    return sorted[index] * 1000.0 / Stopwatch.Frequency;
}

// 사용법
// Console.WriteLine($"p50={PercentileMs(latenessTicks, count, 50):F3}ms");
// Console.WriteLine($"p99={PercentileMs(latenessTicks, count, 99):F3}ms");
// Console.WriteLine($"p99.9={PercentileMs(latenessTicks, count, 99.9):F3}ms");
// Console.WriteLine($"max={PercentileMs(latenessTicks, count, 100):F3}ms");

Stopwatch.Frequency는 1초당 카운트 수이므로, 나눈 뒤 1000을 곱하면 밀리초가 됩니다. 샘플 수가 적으면 p99.9는 의미가 없습니다. p99.9를 말하려면 최소한 10,000 샘플(가능하면 10만)은 모아야 합니다.

5.3. 무엇으로 보는가

도구 구성은 대략 정해져 있습니다.

  • 앱 내 계측 먼저 스스로 period / lateness / execution time / queue depth / drop을 잡는다
  • ETW / WPR / WPA CPU, context switch, DPC / ISR, page fault를 판다
  • LatencyMon 드라이버 기인 흔들림의 감을 잡는다
  • 온도 / 클럭 감시 열의 영향을 본다
앱 내 계측p50 / p95 / p99 / p99.9 / maxmiss / drop / queue depthETW / WPR / WPAcontext switch / DPC / ISR / page fault온도 / 클럭 감시개선 우선순위를 정한다

그림20: 앱 내 계측·ETW·온도 감시 결과를 맞춰 보고, 개선 우선순위를 정한다.

WPA까지 가면 조금 손이 가지만, DPC / ISR이 원인인지, 단순히 자기 처리가 무거운지를 나누는 데에는 상당히 유효합니다.

구하는 방법과 최소 사용법

도구 구하는 방법 최소 절차
WPR / WPA(Windows Performance Toolkit) Windows ADK(Windows Assessment and Deployment Kit)설치 때 “Windows Performance Toolkit”을 고르면 들어갑니다. 기본 위치는 C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit 관리자로 명령 프롬프트를 열고, (1) wpr -start CPU로 기록 시작, (2) 문제의 처리를 수십 초 돌리고, (3) wpr -stop trace.etl "주기 지연 조사"로 저장. 그다음 trace.etl을 WPA로 엽니다. 쓸 수 있는 프로필 이름은 wpr -profiles로 확인할 수 있습니다
LatencyMon Resplendence Software 사이트에서 다운로드합니다. 개인용 Home Edition이 무료 버전입니다 기동해 계측을 시작하고, 대상 처리를 돌린 채로 몇 분 둡니다. 커널 타이머의 최대 지연과 드라이버별 ISR / DPC 실행 시간, hard pagefault가 집계되므로, 실행 시간이 두드러진 드라이버를 적어 둡니다

WPA에서 먼저 볼 것은 CPU 관련 그래프 중 DPC / ISR 실행 시간context switch입니다. “자기 스레드가 돌지 못한 시간대에 어느 드라이버가 CPU를 쥐고 있었는지”가 늘어나므로, 5.1에서 기록한 지연 발생 시각과 맞춰 봅니다.

참고로 이 글에는 화면 스크린샷을 싣지 않았습니다. 버전마다 모습이 바뀌기 쉬우므로, 위 절차대로 조작해 실제 화면에서 확인하면 됩니다.

5.4. 테스트 방법

테스트는 조용한 벤치 환경만으로는 부족합니다. 적어도 이런 조건은 나누어 보고 싶은 부분입니다.

  • 기동 직후 워밍업 전
  • 워밍업 후
  • 장시간 연속 운전
  • UI 앞면
  • UI 최소화 / 숨김에 가까운 상태
  • AC 전원
  • 배터리 구동
  • 네트워크나 디스크에 부하가 있는 상태

벤치 환경만으로 평가하면 실운영에서 나오는 문제를 놓치기 쉽습니다. 평범한 Windows는 “쓰이는 방식”에 동작이 끌리기 쉬우므로, 실제로 쓰는 조건에 맞춰 확인하는 것이 중요합니다.

조건을 나눈 테스트의 생각조용한 벤치 환경만으로 평가하면 실운영 문제를 놓치므로, 워밍업 전후나 장시간 운전, UI 앞면과 최소화, AC 전원과 배터리 구동, 부하 유무 같은 조건을 나누어 확인함을 나타낸다.조용한 벤치 환경만의 평가실운영 문제를 놓친다조건을 나누어 평가한다워밍업·장시간UI 앞면과 최소화전원 공급과 부하 조건

그림21: 평범한 Windows는 쓰이는 방식에 동작이 끌린다. 실제로 쓰는 조건에 맞춰 평가한다.

6. 대략적인 구분

주기 구간별 빠른 표는 읽으면서 되돌아갈 수 있게 1.1로 앞당겼습니다. 여기서는 그 표만으로는 정해지지 않는 두 판단을 보충합니다.

어디서 “평범한 Windows에서는 무리”라고 판단하는가

1ms 미만을 장시간·고부하에서 지켜야 하게 되면, 튜닝을 계속하기보다 시간에 엄격한 부분만 밖으로 내는 판단이 더 빠른 경우가 많습니다. 빼낼 곳 후보는 디바이스 측 펌웨어, 전용 컨트롤러, FPGA, RTOS입니다. 판단 재료는 주관이 아니라 5장의 숫자로 둡니다.

  • hot path를 더 짧게 할 수 없는데 p99.9와 max가 요구를 계속 넘는다
  • 외란(DPC / ISR, 드라이버, 다른 프로세스)이 원인이라 앱 측 대책으로는 닿지 않는다(4.7의 분리로 확인)
  • 요구가 “가끔 놓쳐도 깨지지 않는다”가 아니라 “한 번도 놓치면 안 된다”가 되어 있다

전부를 한 프로세스에 함께 두고 싶을 때

GUI, 로그, 통신, DB를 같은 프로세스의 같은 루프로 끌어안으면 뒷단 사정이 앞단 기한을 깨뜨립니다. 파일 플러시 대기, DB 재연결, UI 다시 그리기는 모두 수십 밀리초 단위로 늘어날 수 있기 때문입니다. fast path / slow path 분리(4.2)를 스레드가 아니라 프로세스 단위까지 넓히는 것도 선택지가 됩니다. 프로세스를 나누면 뒷단이 굳어도 앞단 주기는 계속 돕니다.

함께 두는 구성과 프로세스 분리의 대비GUI나 로그나 통신이나 DB를 같은 프로세스의 같은 루프로 끌어안으면 뒷단 사정이 앞단 기한을 깨므로, fast path와 slow path 분리를 프로세스 단위까지 넓히면 뒷단이 굳어도 앞단 주기는 계속 돎을 나타낸다.전부를 1프로세스 1루프로 끌어안는다뒷단 사정이 앞단 기한을 깨뜨린다플러시 대기·재연결·다시 그리기앞단과 뒷단을 프로세스로 나눈다뒷단이 굳어도 앞단은 계속 돈다

그림22: 꼭 한 프로세스에 함께 둬야 하면, fast path / slow path 분리를 프로세스 단위까지 넓히는 선택지가 있다.

7. 정리

잡아 두고 싶은 전제는 두 가지입니다.

  • 평범한 Windows에서 목표로 하는 것은 hard real-time 보증이 아니라, soft real-time으로서 지연과 지터를 작게 하고, 기한 위반이 일어나도 깨지지 않는 구성으로 만드는 것
  • 효과가 가장 큰 것은 우선순위 조정보다 hot path 정리

구현에서 효과적인 것은 이 정도입니다.

  • fast path / slow path를 나눈다
  • 고정 길이 큐와, 넘쳤을 때의 방침을 먼저 정한다
  • QPC로 재고, event / waitable timer(대기 가능 타이머)로 기다린다
  • hot path에서는 할당, 블로킹 I/O, 무거운 락을 피한다

운영 면에서는 이 정도가 효과적입니다.

  • AC 전원으로 돌린다
  • 운영용 전원 설정을 나눈다
  • 불필요한 백그라운드 부하를 줄인다
  • p99 / p99.9 / max와 miss 횟수로 평가한다

평범한 Windows에서의 소프트 리얼타임은 우선순위 설정만으로 정해지는 것이 아니며, 설계, 구현, 전원 설정, 계측, 운영을 나누어 다져 가면 상당히 안정된 시스템을 만들 수 있습니다.

8. 참고 자료

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Windows에서 리얼타임 처리가 가능한가요?
hard real-time(기한 위반 제로) 보증은 할 수 없지만, 설계·구현·계측·운영을 제대로 다지면 soft real-time으로서 꽤 실용적인 상태까지 가져갈 수 있습니다. 수 밀리초~수십 밀리초의 주기 처리, 음성/영상 버퍼 구동, 센서 취득과 제어 루프 등은 평범한 Windows 10/11에서도 현실적입니다. 반대로 기한 위반 제로 보증이나 수백 마이크로초 이하의 장시간 안정이 필요하면 RTOS·전용 컨트롤러·FPGA·디바이스 측 처리로 옮기는 것을 검토해야 합니다.
주기 처리에서 Sleep(1)을 쓰면 안 되는 이유는 무엇인가요?
Sleep(1)은 "1ms 주기"가 아니라 "대략 1ms 이상 기다린 뒤 처리 시간을 더하는" 동작이 되어, 대기 overshoot가 그대로 누적되기 때문입니다. 주기 루프는 next += period의 절대 기한으로 돌리고, 대기는 디바이스 이벤트나 고정밀 waitable timer를 우선하며, 마지막 미세 조정만 아주 짧은 busy-spin으로 한정하는 편이 안정적입니다. timeBeginPeriod는 필요한 동안만 쓰고, 끝나면 되돌립니다.
지연이나 지터를 줄이는 데 가장 효과적인 것은 무엇인가요?
우선순위를 올리는 것보다, hot path를 짧게·고정 길이로·non-blocking으로 만드는 것입니다. fast path(취득·제어)와 slow path(저장·통신·UI)를 나누어 고정 길이 큐로 잇고, hot path에서는 파일 쓰기, 네트워크 송신, 무거운 로그 출력, 매번 메모리 할당을 피합니다. 운영 면에서는 AC 전원, 전원 모드 재검토, EcoQoS 확인, 백그라운드 부하 정리가 효과적입니다.
주기 처리의 안정성은 어떤 지표로 평가하면 되나요?
평균값만이 아니라 p99·p99.9·max·missed deadline 수로 봅니다. 예를 들어 평균 0.8ms라도 max가 28ms이면, 평소에는 빠르지만 가끔 큰 스파이크가 있는 상태이며, 평범한 Windows에서는 이 p99부터 max의 꼬리에 진짜 문제가 드러납니다. 더불어 DPC/ISR 스파이크, page fault, queue 깊이, 온도·클럭 변동도 기록하고, 워밍업 전후·장시간 운전·최소화 상태·배터리 구동 등의 조건을 나누어 평가합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기