사건이 언제 일어났는지 기록할 때는 UTC 등 기준이 명확한 시각을, 시작과 끝 사이의 경과시간을 잴 때는 perf_counter나 monotonic 계열을 사용한다. Python의 time.time은 달력상의 시각과 연결되지만 시스템 시계가 바뀌면 그 차이로 계산한 실행 시간도 영향을 받을 수 있다.
이 글의 예제는 CPython 3.14.3·Windows 11에서 짧게 대기하며 측정했다. OS 시계를 바꾸거나 PC를 절전·재부팅하지 않았다. 뒤의 시계 보정 사례는 정수로 구성한 모의값이며 실제 PC에서 음의 실행 시간이 관측됐다는 뜻이 아니다.
기록할 시각과 잴 시간을 먼저 구분한다
| 질문 | 예제에서 쓸 시계 | 읽는 값 |
|---|---|---|
| 이 사건은 언제였나 | UTC를 명시한 datetime / time 계열 | 달력상의 시각 |
| 시작부터 얼마나 지났나 | perf_counter_ns | 같은 시계의 끝−시작 |
| 현재 실행에서 남은 제한시간은 | monotonic_ns | 같은 기준의 기한−현재 |
| 프로세스가 쓴 CPU 시간은 | process_time_ns | 프로세스 CPU 사용 시간의 차이 |
Python time 문서에서 perf_counter는 짧은 경과시간 측정용이고, sleep으로 대기한 시간도 포함한다고 설명한다. process_time은 프로세스가 쓴 사용자·시스템 CPU 시간이며 sleep 시간은 포함하지 않는다. 따라서 대기 많은 작업의 지연을 CPU 시간만으로 평가하면 다른 질문에 답하게 된다.
로그에는 datetime.now 공식 문서처럼 UTC가 붙은 aware datetime을 사용할 수 있다. 시간대를 적는 일은 서버 시계 자체의 정확도나 여러 서버의 동기화를 보장하는 일과 다르다. 원문에 날짜만 있을 때 임의의 시각·시간대를 덧붙이는 문제는 문서 날짜·시간대 정규화에서 별도로 다룬다.
같은 시계의 두 값을 빼고, 반환 단위와 해상도를 구별한다
시작은 time.time으로, 끝은 perf_counter로 읽어 빼면 기준점이 달라 의미 있는 경과시간이 되지 않는다. 예제는 시작·끝 모두 perf_counter_ns를 쓴다. 문서상 CPython 3.14의 perf_counter는 monotonic과 같은 시계를 사용하지만, 이 사실을 다른 구현·버전·OS의 모든 시계에 일반화하지 않는다.
_ns 함수는 정수 나노초를 반환해 float 변환에 따른 정밀도 손실을 피한다. 그렇다고 실제 시계의 해상도·정확도가 1ns라는 보장은 아니다. get_clock_info의 implementation·resolution·monotonic·adjustable을 확인해 어떤 시계를 읽었는지 남길 수 있다. adjustable 속성은 점프식 조정 가능 여부이며 점진적 NTP 속도 보정까지 뜻하지는 않는다.
코드는 CPython 3.14.3·Windows 11에서 실행했다. 설명은 Python 3.14 계열 공식 문서와 대조했으며, 문서 사이트의 현재 패치 판본은 실행한 인터프리터와 다를 수 있다.
import json
import time
from datetime import datetime, timezone
event_utc = datetime.now(timezone.utc).isoformat(timespec="milliseconds")
wall_start = time.time_ns()
elapsed_start = time.perf_counter_ns()
cpu_start = time.process_time_ns()
time.sleep(0.02) # 짧은 대기: PC 절전이나 OS 시계 변경 실험이 아니다.
cpu_ns = time.process_time_ns() - cpu_start
elapsed_ns = time.perf_counter_ns() - elapsed_start
wall_ns = time.time_ns() - wall_start
clocks = {}
for name in ["time", "perf_counter", "monotonic", "process_time"]:
info = time.get_clock_info(name)
clocks[name] = {
"implementation": info.implementation,
"monotonic": info.monotonic,
"adjustable": info.adjustable,
"resolution_seconds": info.resolution,
}
print("LOCAL_MEASUREMENT", json.dumps({
"event_utc": event_utc,
"wall_delta_ns": wall_ns,
"elapsed_ns": elapsed_ns,
"process_cpu_ns": cpu_ns,
"clocks": clocks,
}, ensure_ascii=False))
# 아래는 정수로 만든 모의값이다. 시스템 시계를 수정하지 않는다.
wall_begin_ns = 1_700_000_000_000_000_000
duration_ns = 200_000_000
wall_adjustment_ns = -1_000_000_000
wall_end_ns = wall_begin_ns + duration_ns + wall_adjustment_ns
mono_begin_ns = 5_000_000_000
mono_end_ns = mono_begin_ns + duration_ns
print("SIMULATED_WALL_DELTA_MS", (wall_end_ns - wall_begin_ns) / 1_000_000)
print("SIMULATED_MONO_DELTA_MS", (mono_end_ns - mono_begin_ns) / 1_000_000)
# 같은 실행의 단조시계 기한 계산만 한다. 5초를 기다리지 않는다.
deadline_ns = time.monotonic_ns() + 5_000_000_000
remaining_ns = max(0, deadline_ns - time.monotonic_ns())
print("REMAINING_MS", remaining_ns / 1_000_000)
이 환경에서 확인한 짧은 실행
다음은 이 코드의 한 번 실행에서 얻은 로컬 관측이다. 사건 UTC는 2026-11-02T16:54:43.070+00:00였다.
| 측정값 | 이번 실행의 ms |
|---|---|
| perf_counter_ns의 경과 | 20.3492 |
| time_ns의 차이 | 20.3506 |
| process_time_ns의 CPU 시간 | 0.0000 |
이 환경의 perf_counter 구현은 QueryPerformanceCounter(), 보고된 해상도는 1e-07초였다. 이 값은 같은 OS 이름의 모든 PC에 적용할 보장값이 아니다.
이 한 번의 값은 벤치마크나 보장된 sleep 길이가 아니다. 세 시계의 시작·끝을 읽는 시점도 조금씩 달라 차이가 서로 같을 필요는 없다. 대기 요청은 20ms지만 스케줄링과 실행 부하 때문에 실제 경과는 달라질 수 있다. process_time의 차이에는 같은 프로세스의 다른 CPU 작업도 포함될 수 있고, 짧은 실행에서는 눈에 띄는 증가가 없을 수도 있다. 코드 마지막의 기한 예제는 계산만 하고 5초를 기다리지 않는다.
벽시계가 뒤로 보정되면 차이는 어떻게 바뀌나
코드의 모의 구간은 실제 경과를 200ms, 벽시계 보정을 −1,000ms로 놓았다. 따라서 벽시계 값의 차이는 −800ms, 단조시계 값의 차이는 200ms다. 단위와 기준을 고정한 산술 예이며 OS 시계를 수정한 실험이 아니다.
단조시계의 기준점은 문서상 정해진 달력 시각이 아니다. 다른 서버의 값과 절대 시각처럼 비교하거나, 재부팅 뒤에도 같은 기준이라고 가정해 영속 기한으로 저장하지 않는다. 오래 보관할 사건 시각과 현재 실행의 경과시간·남은 기한을 별도 필드로 두면 두 목적을 혼동하기 어렵다.
짧은 time.sleep 대기는 시스템 절전·최대절전과 다르다. 절전 구간의 포함 여부가 필요한 요구라면 해당 OS·시계의 계약과 실제 환경을 추가로 확인해야 한다. 여기서 실행한 Windows의 짧은 측정만으로 다른 플랫폼의 절전 동작까지 입증하지 않는다.
“파이썬 실행 시간 측정: time.time과 perf_counter의 역할”에 대한 1개의 생각