Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 564 – 나노초 해상도를 갖는 새 시간 함수 추가

Author:
Victor Stinner <vstinner at python.org>
Status:
Final
Type:
Standards Track
Created:
16-Oct-2017
Python-Version:
3.7
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

time 모듈에 기존 함수의 새로운 “나노초” 변형 여섯 개를 추가합니다: clock_gettime_ns(), clock_settime_ns(), monotonic_ns(), perf_counter_ns(), process_time_ns(), time_ns(). 기존의 _ns 접미사가 없는 함수와 비슷하지만, 이 함수들은 나노초 해상도를 제공합니다. 즉, 나노초 수를 Python int로 반환합니다.

Linux와 Windows에서 time.time_ns()의 해상도는 time.time()의 해상도보다 3배 더 좋습니다.

근거

104일로 제한되는 float 타입

데스크톱과 노트북 컴퓨터의 시계 해상도는 나노초 해상도에 점점 가까워지고 있습니다. 점점 더 많은 시계가 MHz 단위의 주파수를 가지며, CPU TSC 시계의 경우 GHz에 이릅니다.

Python time.time() 함수는 현재 시각을 부동소수점 수로 반환하며, 이는 보통 (IEEE 754 형식의) 64비트 이진 부동소수점 수입니다.

문제는 float 타입이 104일이 지나면 나노초를 잃기 시작한다는 점입니다. 변환 과정에서 정밀도가 손실되는지 확인하기 위해 나노초(int)를 초(float)로 변환한 다음 다시 나노초(int)로 변환하기:

# no precision loss
>>> x = 2 ** 52 + 1; int(float(x * 1e-9) * 1e9) - x
0
# precision loss! (1 nanosecond)
>>> x = 2 ** 53 + 1; int(float(x * 1e-9) * 1e9) - x
-1
>>> print(datetime.timedelta(seconds=2 ** 53 / 1e9))
104 days, 5:59:59.254741

time.time()은 UNIX 에포크, 즉 1970년 1월 1일 이후 경과한 초를 반환합니다. 이 함수는 1970년 5월(47년 전) 이후로 나노초 정밀도를 갖지 못했습니다:

>>> import datetime
>>> unix_epoch = datetime.datetime(1970, 1, 1)
>>> print(unix_epoch + datetime.timedelta(seconds=2**53 / 1e9))
1970-04-15 05:59:59.254741

이전에 거부된 PEP

5년 전, PEP 410은 decimal.Decimal 타입을 사용하여 나노초 해상도를 지원하도록 시간을 반환하는 모든 Python 함수에 크고 복잡한 변경을 제안했습니다.

이 PEP는 여러 가지 이유로 거부되었습니다:

  • 결과 타입을 바꾸기 위해 새로운 선택적 매개변수를 추가한다는 아이디어는 거부되었습니다. 이는 Python에서 흔치 않은 (그리고 나쁜?) 프로그래밍 관행입니다.
  • 하드웨어 시계가 실제로 1나노초의 해상도를 가지는지, 또는 그것이 Python 수준에서 의미가 있는지 분명하지 않았습니다.
  • decimal.Decimal 타입은 Python에서 흔하지 않으므로 이를 처리하도록 코드를 수정해야 합니다.

정밀도 손실로 인한 문제

예시 1: 장시간 실행되는 프로세스에서 시간 차이 측정하기

서버가 104일보다 오래 실행되고 있습니다. 런타임에 성능 문제를 감지하기 위해, 함수 실행 전후에 시계를 읽어 함수의 성능을 측정합니다. 이러한 벤치마크는 시계 해상도 때문이 아니라, 오직 시계가 사용하는 float 타입 때문에 정밀도를 잃습니다.

Python 마이크로벤치마크에서는 100 ns 미만이 걸리는 함수 호출을 흔히 볼 수 있습니다. 몇 나노초의 차이가 유의미해질 수 있습니다.

예시 2: 해상도가 다른 시간 비교하기

두 프로그램 “A”와 “B”가 같은 시스템에서 실행되며 시스템 시계를 사용합니다. 프로그램 A는 나노초 해상도로 시스템 시계를 읽고 나노초 해상도로 타임스탬프를 기록합니다. 프로그램 B는 나노초 해상도로 타임스탬프를 읽지만, 이를 더 낮은 해상도로 읽은 시스템 시계와 비교합니다. 예를 단순화하기 위해 B가 초 단위 해상도로 시계를 읽는다고 합시다. 이 경우 프로그램 B가 A가 기록한 타임스탬프를 “미래”의 것으로 볼 수 있는 1초의 구간이 존재합니다.

오늘날 점점 더 많은 데이터베이스와 파일 시스템이 나노초 해상도로 시간을 저장하는 것을 지원합니다.

Note

이 문제는 파일 수정 시간에 대해서는 os.stat() 결과에 st_mtime_ns 필드를 추가하고 os.utime()에서 나노초를 받도록 함으로써 이미 해결되었습니다. 이 PEP는 그 해결책을 일반화할 것을 제안합니다.

지난 5년간의 CPython 개선 사항

PEP 410이 거부된 이후:

  • os.stat_result 구조체에 나노초 단위(Python int) 타임스탬프를 위한 새 필드 3개가 추가되었습니다: st_atime_ns, st_ctime_ns, st_mtime_ns.
  • PEP 418이 채택되어 Python 3.3에 새로운 시계 3개가 추가되었습니다: time.monotonic(), time.perf_counter(), time.process_time().
  • 시간을 다루는 CPython의 비공개 “pytime” C API는 이제 새로운 _PyTime_t 타입을 사용하며, 이는 단순한 64비트 부호 있는 정수(C int64_t)입니다. _PyTime_t의 단위는 구현 세부 사항이며 API의 일부가 아닙니다. 현재 단위는 1 nanosecond입니다.

나노초를 int로 사용하는 기존 Python API

os.stat_result 구조체에는 나노초 단위(int) 타임스탬프를 위한 필드 3개가 있습니다: st_atime_ns, st_ctime_ns, st_mtime_ns.

os.utime() 함수의 ns 매개변수는 나노초 단위의 (atime_ns: int, mtime_ns: int) 튜플을 받습니다.

변경 사항

새 함수

이 PEP는 time 모듈에 새 함수 6개를 추가합니다:

  • time.clock_gettime_ns(clock_id)
  • time.clock_settime_ns(clock_id, time: int)
  • time.monotonic_ns()
  • time.perf_counter_ns()
  • time.process_time_ns()
  • time.time_ns()

이 함수들은 _ns 접미사가 없는 버전과 비슷하지만, 나노초 수를 Python int로 반환합니다.

예를 들어, monotonic() 값이 정밀도를 잃지 않을 만큼 충분히 작다면 time.monotonic_ns() == int(time.monotonic() * 1e9)입니다.

이 함수들이 필요한 이유는 UNIX 에포크를 기준으로 사용하는 time.time()처럼 “큰” 타임스탬프를 반환할 수 있으며, 따라서 float를 반환하는 변형은 나노초 해상도에서 정밀도를 잃을 가능성이 높기 때문입니다.

변경되지 않는 함수

time.clock() 함수는 Python 3.3에서 폐지되었으므로 time.clock_ns()는 추가하지 않습니다.

Python에는 시간을 반환하는 다른 함수들도 있습니다. 이러한 다른 함수들에는 나노초 변형을 제안하지 않습니다. 내부 해상도가 1 us 이상이거나, 최댓값이 정밀도를 잃지 않을 만큼 충분히 작기 때문입니다. 예를 들어, time.clock_getres()의 최댓값은 1초일 것입니다.

변경되지 않는 함수의 예는 다음과 같습니다.

  • os 모듈: sched_rr_get_interval(), times(), wait3() 및 wait4()
  • resource 모듈: getrusage()의 ru_utime 및 ru_stime 필드
  • signal 모듈: getitimer(), setitimer()
  • time 모듈: clock_getres()

Annex: Clocks Resolution in Python도 참고하십시오.

운영 체제가 더 나은 해상도를 제공하는 새로운 함수를 노출한다면, 나중에 이러한 함수들의 나노초를 반환하는 새로운 버전이 추가될 수 있습니다.

대안과 논의

나노초 미만 해상도

time.time_ns() API는 이론적으로 미래에도 대비된 것은 아닙니다. 시계 해상도가 나노초 수준 아래로 계속 높아진다면, 새로운 Python 함수가 필요할 수 있습니다.

실제로는 현재 일반적인 모든 운영 체제 함수가 반환하는 모든 구조체에 1나노초 해상도로 충분합니다.

1나노초보다 나은 해상도를 가진 하드웨어 시계는 이미 존재합니다. 예를 들어, CPU TSC 시계의 주파수는 CPU 기본 주파수이므로, 3 GHz로 동작하는 CPU의 해상도는 약 0.3 ns입니다. 그러나 그러한 하드웨어를 사용할 수 있고 정말로 나노초 미만의 해상도가 필요한 사용자는 필요에 맞게 Python을 확장할 수 있습니다. 이처럼 드문 사용 사례가 나노초 미만의 해상도를 지원하도록 Python 표준 라이브러리를 설계하는 것을 정당화하지는 않습니다.

CPython 구현에서는 나노초 해상도가 편리합니다. 표준적이고 널리 지원되는 int64_t 타입을 사용하여 나노초 정밀도의 타임스탬프를 저장할 수 있기 때문입니다. 이 타입은 -292년부터 +292년까지의 시간 범위를 지원합니다. 따라서 UNIX 에포크를 기준으로 하면 1677년부터 2262년까지의 시간을 표현할 수 있습니다.:

>>> 1970 - 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
1677.728976954687
>>> 1970 + 2 ** 63 / (10 ** 9 * 3600 * 24 * 365.25)
2262.271023045313

time.time() 결과 타입 수정

더 나은 정밀도를 가진 다른 숫자 타입을 반환하도록 time.time()을 수정하자는 제안이 있었습니다.

PEP 410은 이미 존재하며 임의 정밀도를 지원하는 decimal.Decimal을 반환하자고 제안했지만, 거부되었습니다. decimal.Decimal을 제외하면, 현재 Python에는 더 나은 정밀도를 가진 이식 가능한 실수 타입이 없습니다.

내장 Python float 타입을 변경하는 것은 이 PEP의 범위를 벗어납니다.

또한 기존 함수가 새로운 타입을 반환하도록 변경하면, 새 타입을 신중하게 설계하더라도 하위 호환성이 깨질 위험이 생깁니다.

다른 타입들

더 크거나 임의의 정밀도를 지원하기 위해 분수, 정수를 사용하는 구조체나 2-튜플, 고정 소수점 수 등 새로운 타입에 대한 많은 아이디어가 제안되었습니다.

다른 타입에 대한 이전의 긴 논의는 PEP 410도 참고하십시오.

새로운 타입을 추가하면 기존 int 타입을 재사용하는 것보다 이를 지원하는 데 더 많은 노력이 필요합니다. 표준 라이브러리, 서드파티 코드, 애플리케이션이 이를 지원하도록 수정되어야 할 것입니다.

Python int 타입은 잘 알려져 있고, 잘 지원되며, 다루기 쉽고, dt = t2 - t1과 같은 모든 산술 연산을 지원합니다.

게다가 os.stat_result와 os.utime(ns=(atime_ns, mtime_ns))에서 볼 수 있듯이, 나노초 단위의 정수를 받거나 반환하는 것은 Python에서 새로운 개념이 아닙니다.

Note

Python float 타입이 더 커진다면(예: decimal128 또는 float128), time.time()의 정밀도도 함께 높아집니다.

다른 API

새 함수를 추가하지 않기 위해 time.time(ns=False) API가 제안되었습니다. 매개변수에 따라 결과 타입을 바꾸는 것은 Python에서 흔치 않은 (그리고 나쁜?) 프로그래밍 관행입니다.

사용자가 시간 해상도를 선택할 수 있도록 하는 여러 방안이 제안되었습니다. 각 Python 모듈이 서로 다른 해상도를 사용한다면, 단순히 초(float를 반환하는 time.time())와 나노초(int를 반환하는 time.time_ns())만 다루는 대신 서로 다른 여러 해상도를 처리해야 하므로 어려워질 수 있습니다. 게다가 앞서 설명했듯이, Python 표준 라이브러리에서는 실제로 1나노초보다 나은 해상도가 필요하지 않습니다.

새 모듈

다음 함수들을 포함하는 새 time_ns 모듈을 추가하자는 제안이 있었습니다:

  • time_ns.clock_gettime(clock_id)
  • time_ns.clock_settime(clock_id, time: int)
  • time_ns.monotonic()
  • time_ns.perf_counter()
  • time_ns.process_time()
  • time_ns.time()

첫 번째 질문은 time_ns 모듈이 time 모듈과 정확히 같은 API(상수, 함수 등)를 노출해야 하는지입니다. time 모듈을 두 가지 형태로 유지 관리하는 것은 고통스러울 수 있습니다. 사용자는 이 두 모듈 중 어느 것을 어떻게 선택해야 합니까?

앞으로 os 모듈에도 다른 나노초 변형이 필요해진다면, 새 os_ns 모듈도 추가해야 합니까? 시간과 관련된 함수는 time, os, signal, resource, select 등 여러 모듈에 있습니다.

또 다른 아이디어는 time.ns 하위 모듈이나 중첩 네임스페이스를 추가하여 time.ns.time() 문법을 얻는 것이지만, 이 역시 같은 문제를 겪습니다.

부록: Python에서의 클록 해상도

이 부록에는 운영 체제가 공표하는 해상도나 운영 체제가 사용하는 내부 구조체의 해상도가 아니라, Python에서 측정한 클록 해상도가 담겨 있습니다.

스크립트

0인 차이는 무시하고, time.time()과 time.time_ns()를 각각 두 번 읽었을 때의 가장 작은 차이를 측정하는 스크립트 예시:

import math
import time

LOOPS = 10 ** 6

print("time.time_ns(): %s" % time.time_ns())
print("time.time(): %s" % time.time())

min_dt = [abs(time.time_ns() - time.time_ns())
          for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min time_ns() delta: %s ns" % min_dt)

min_dt = [abs(time.time() - time.time())
          for _ in range(LOOPS)]
min_dt = min(filter(bool, min_dt))
print("min time() delta: %s ns" % math.ceil(min_dt * 1e9))

Linux

Fedora 26(커널 4.12)에서 Python으로 측정한 클록 해상도:

함수 해상도
clock() 1 us
monotonic() 81 ns
monotonic_ns() 84 ns
perf_counter() 82 ns
perf_counter_ns() 84 ns
process_time() 2 ns
process_time_ns() 1 ns
resource.getrusage() 1 us
time() 239 ns
time_ns() 84 ns
times().elapsed 10 ms
times().user 10 ms

해상도에 관한 참고 사항:

  • clock()의 주파수는 1,000,000 Hz(1 MHz)인 CLOCKS_PER_SECOND이며, 해상도는 1 us입니다.
  • times()의 주파수는 100 Hz인 os.sysconf("SC_CLK_TCK")(또는 HZ 상수)이며, 해상도는 10 ms입니다.
  • resource.getrusage(), os.wait3(), os.wait4()는 ru_usage 구조체를 사용합니다. ru_usage.ru_utime 필드와 ru_usage.ru_stime 필드의 타입은 해상도가 1 us인 timeval 구조체입니다.

Windows

Windows 8.1에서 Python으로 측정한 클록 해상도:

함수 해상도
monotonic() 15 ms
monotonic_ns() 15 ms
perf_counter() 100 ns
perf_counter_ns() 100 ns
process_time() 15.6 ms
process_time_ns() 15.6 ms
time() 894.1 us
time_ns() 318 us

perf_counter()와 perf_counter_ns()의 주파수는 QueryPerformanceFrequency()에서 가져옵니다. 주파수는 보통 10 MHz이며, 이는 100 ns의 분해능에 해당합니다. 구버전 Windows에서는 주파수가 3,579,545 Hz(3.6 MHz)인 경우도 있었으며, 이는 279 ns의 분해능에 해당합니다.

분석

time.time_ns()의 분해능은 time.time()보다 훨씬 좋습니다. Linux에서는 84 ns(2.8배 향상) 대 239 ns, Windows에서는 318 us(2.8배 향상) 대 894 us입니다. 매일 시스템 시계에 86,400,000,000,000 나노초가 더해지면서 정밀도 손실이 커지므로, time.time()의 분해능은 해가 지날수록 커질(나빠질) 뿐입니다.

이 간단한 스크립트는 실행 시간이 1분이 채 되지 않고, 스크립트를 실행한 컴퓨터의 가동 시간도 1주일 미만이었기 때문에 time.perf_counter(), time.monotonic(), time.process_time()과 각각의 나노초 변형 사이의 차이는 드러나지 않습니다. 가동 시간이 104일 이상에 이르면 상당한 차이가 나타날 수 있습니다.

resource.getrusage()와 times()는 분해능이 1마이크로초 이상이므로 나노초 분해능을 갖는 변형이 필요하지 않습니다.

Note

내부적으로 Python은 일부 플랫폼에서 monotonic()과 perf_counter() 시계를 0에서 시작하며, 이는 간접적으로 정밀도 손실을 줄여 줍니다.

링크