Windows 소프트 리얼타임 실전 가이드 - 지연을 줄이기 위한 체크리스트

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

수정 이력(1건, 최종 수정 2026년 09월 01일)

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

일본어 원문의 완역으로 다시 번역했습니다. 기존 한국어판은 원문의 일부만 옮긴 축약본이라 절, 표, Mermaid 그림, 그림 설명, FAQ가 빠져 있었습니다. 일본어 원문에 맞춰 이들을 모두 복원했고, 기술적인 주장은 일본어판과 같습니다. 수정 전 버전 보기 (DOI: 10.5281/zenodo.21635093)
최초 공개
이 글을 인용하기(DOI: 10.5281/zenodo.21635092)

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

小村 豪 (2026). 「Windows 소프트 리얼타임 실전 가이드 - 지연을 줄이기 위한 체크리스트」. 합동회사 코무라소프트. https://doi.org/10.5281/zenodo.21635092 https://comcomponent.com/ko/blog/2026/03/09/000-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으로서 「늦기 어렵고, 늦어도 무너지지 않는」 구성입니다.
  • 가장 효과가 큰 것은 핫 패스를 짧게, 고정 길이로, 논블로킹으로 만드는 것입니다.
  • 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에서는 우선순위를 올리는 것보다, 늦어지는 이유를 설계로 줄이는 것이 더 효과적입니다. 우선순위나 전원 설정도 중요하지만 그것만으로 안정성이 만들어지지는 않습니다.

안정성을 만드는 것은 우선순위보다 설계핫 패스를 짧고 고정 길이로 논블로킹으로 만들어 지연 원인을 설계로 줄이는 것이 가장 효과적이며, 우선순위나 전원 설정은 중요하지만 그것만으로는 안정성을 만들 수 없다는 결론을 보여준다.핫 패스를 짧고 고정 길이로지연 원인이 설계에서 줄어든다지연이 적고 늦어도 무너지지 않는 구성우선순위나 전원 설정 조정중요하지만 그것만으로는 부족하다

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

1.1. 주기 범위별 조견표(어디부터 읽을지)

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

주기·요건 먼저 구성할 것 중점적으로 읽을 절
10~20ms급으로, 가끔의 흔들림은 흡수 가능 fast path / slow path 분리, 고정 길이 큐, 일반~약간 높은 우선순위, 이벤트 구동. 이것으로 충분한 경우가 많음 4.1, 4.2
1~5ms급으로, 지속적으로 맞추고 싶음 위에 더해 핫 패스의 무할당화, 전용 스레드, 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. 페이지 폴트와 메모리

핫 패스에서 page fault(필요한 페이지가 메모리에 없어 가지러 가는 것)가 일어나면 지연이 단번에 커집니다.

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

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

주기 처리의 본체에서는 필요한 메모리를 먼저 확보하고, 기동 시 한 번 접근해 두는 정도가 딱 좋습니다.

핫 패스에서 page fault로 인한 지연핫 패스에서 접근한 페이지가 메모리에 없으면 page fault로 가져오게 되어 지연이 단번에 커지므로, 필요한 메모리를 미리 확보해 기동 시 한 번 접근해 두는 것이 좋음을 보여준다.있음없음핫 패스에서 메모리에 접근페이지가 메모리에 있는가그대로 진행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()의 실행 시간을 더하는 루프입니다. 게다가 대기 오버슈트가 그대로 누적됩니다.

절대 기한 기반상대 시간 기반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로 내립니다.

체크리스트

  • 핫 패스에서 파일 쓰기, 네트워크 송신, DB 쓰기를 하지 않는다
  • 핫 패스에서 무거운 로그 출력, 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필요하면 높은 우선순위 또는 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 / 초기 비용

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

기동필요한 버퍼를 확보한 번 접근해 페이지를 데움JIT / DLL 로드 / 최초 I/O를 끝냄그 후 본계측 / 본운전

그림 14: 기동 시에 버퍼 확보·페이지 데우기·초기 비용을 끝낸 뒤 본계측·본운전에 들어간다.

체크리스트

  • 핫 패스에서 매번 메모리 확보 / 해제를 하지 않는다
  • 필요한 버퍼를 기동 시에 사전 확보한다
  • 기동 시에 한 번 접근해 페이지를 데운다
  • 최초 JIT, 최초 DLL 로드, 최초 I/O를 본계측에 섞지 않는다
  • 루프 중에 거대한 구조나 가변 길이 로그를 키우지 않는다
  • VirtualLock을 사용하더라도 아주 작은 크리티컬 영역만으로 한정한다

.NET 측 체크

  • 시각 계측에 Stopwatch / Stopwatch.GetTimestamp()를 사용한다
  • 핫 패스에서 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();을 1회 실행합니다. 프로세스 핸들에는 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는 최후의 수단으로 다룬다
  • 장시간 운전에서 온도, 클록, 서멀 스로틀링을 확인한다
  • 노트북의 정음 모드나 저소음 모드를 확인한다

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

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

특정 코어 고정은 효과가 있어 보이지만, OS가 빠져나갈 여지을 줄이므로 안이하게 사용하면 오히려 융통성이 없어지는 경우가 있습니다.

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

「가끔 max만 폭발한다」「평균은 좋은데 p99.9가 나쁘다」는 경우에는 앱 코드 이외의 외란도 의심하는 편이 좋습니다.

아니오아니오아니오아니오late / miss / max spike가 발생자신의 처리 시간도 긴가?핫 패스 단축 / 할당 삭감 / 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. 핫 패스에서는 기록만 합니다. Stopwatch.GetTimestamp()(C++라면 QueryPerformanceCounter)로 lateness와 실행 시간을 얻어, 사전 확보한 배열에 씁니다. 여기서 평균이나 정렬을 계산해서는 안 됩니다
  2. 계측을 멈춘 뒤에 집계합니다. 정렬해서 백분위 위치의 값을 꺼냅니다
  3. 같은 절차를 조건을 바꿔 반복합니다. 워밍업 전후, AC / 배터리, UI 전면 / 최소화, 다른 프로세스의 부하 유무(5.4)
  4. 변경을 하나 넣을 때마다 같은 조건으로 다시 얻어 비교합니다
// C# / .NET 8. 집계는 계측을 멈춘 뒤에 수행
using System.Diagnostics;

// 핫 패스에서는 배열에 쓰기만 함(할당 제로)
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장의 수치로 삼습니다.

  • 핫 패스를 더 이상 단축할 수 없는데도 p99.9와 max가 요구를 계속 넘고 있다
  • 외란(DPC / ISR, 드라이버, 다른 프로세스)이 원인이어서 앱 측 대책으로는 미치지 못한다(4.7의 분리로 확인)
  • 요구가 「가끔 놓쳐도 무너지지 않는다」가 아니라 「단 한 번도 놓칠 수 없다」로 되어 있다

전부를 1프로세스에 동거시키고 싶을 때

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으로서 지연과 지터를 줄이고, 기한 위반이 일어나도 무너지지 않는 구성으로 만드는 것
  • 가장 효과가 큰 것은 우선순위 조정보다 핫 패스의 정리

구현에서 효과가 있는 것은 이 정도입니다.

  • fast path / slow path를 나눈다
  • 고정 길이 큐와, 넘쳤을 때의 방침을 먼저 정한다
  • QPC로 측정하고, event / waitable timer(대기 가능 타이머)로 기다린다
  • 핫 패스에서는 할당, 블로킹 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 이상 기다린 뒤에 처리 시간을 더하는」 동작이 되어, 대기 오버슈트가 그대로 누적되기 때문입니다. 주기 루프는 next += period의 절대 기한으로 돌리고, 대기는 디바이스 이벤트나 고정밀 waitable timer를 우선하며, 마지막 미세 조정만 아주 짧은 busy-spin으로 한정하는 것이 안정적입니다. timeBeginPeriod는 필요한 동안만 사용하고, 끝나면 되돌립니다.
지연이나 지터를 줄이는 데 가장 효과적인 것은 무엇인가요?
우선순위를 올리는 것보다 핫 패스를 짧고, 고정 길이로, 논블로킹으로 만드는 것입니다. fast path(취득·제어)와 slow path(저장·통신·UI)를 나누어 고정 길이 큐로 연결하고, 핫 패스에서는 파일 쓰기, 네트워크 송신, 무거운 로그 출력, 매번의 메모리 확보를 피합니다. 운영 측면에서는 AC 급전, 전원 모드 재검토, EcoQoS 확인, 백그라운드 부하 정리가 효과적입니다.
주기 처리의 안정성은 어떤 지표로 평가하면 좋은가요?
평균값만이 아니라 p99, p99.9, max, missed deadline 수로 봅니다. 예를 들어 평균이 0.8ms여도 max가 28ms라면 평소에는 빠르지만 가끔 큰 스파이크가 있는 상태이며, 평범한 Windows에서는 이 p99에서 max로 이어지는 꼬리 부분에 진짜 문제가 드러납니다. 함께 DPC/ISR 스파이크, page fault, queue 깊이, 온도·클록 변동도 기록하고, 워밍업 전후, 장시간 운전, 최소화 상태, 배터리 구동 등의 조건을 나누어 평가합니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기