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 모듈에 기존 함수의 새로운 “나노초” 변형 6개를 추가합니다: clock_gettime_ns(), clock_settime_ns(), monotonic_ns(), perf_counter_ns(), process_time_ns()time_ns(). _ns 접미사가 없는 기존 함수와 유사하지만, 나노초 해상도를 제공하며 Python int로 나노초 수를 반환합니다.

time.time_ns()의 해상도는 Linux와 Windows에서 time.time()의 해상도보다 3배 우수합니다.

근거

104일로 제한되는 float 형식

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

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

문제는 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 410decimal.Decimal 형식을 사용하여 나노초 해상도를 지원하도록 시간을 반환하는 모든 Python 함수에 대규모의 복잡한 변경을 제안했습니다.

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

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

정밀도 손실로 인해 발생하는 문제

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

서버가 104일 넘게 실행되고 있습니다. 런타임에 성능 문제를 감지하기 위해 함수 실행 전후에 시계를 읽어 성능을 측정합니다. 이러한 벤치마크에서 정밀도가 손실되는 유일한 원인은 시계의 해상도가 아니라 시계에 사용되는 float 형식입니다.

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

예제 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 구조체에는 나노초 단위 타임스탬프를 위한 3개의 새 필드(Python int)인 st_atime_ns, st_ctime_nsst_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입니다.

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

os.stat_result 구조체에는 나노초 단위 타임스탬프를 위한 3개의 필드(int)인 st_atime_ns, st_ctime_nsst_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 epoch를 기준으로 사용하는 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_utimeru_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 epoch를 기준으로 사용하면 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()을 더 나은 정밀도를 가진 다른 숫자 형식을 반환하도록 수정하자는 제안이 있었습니다.

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

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

또한 기존 함수를 새 형식을 반환하도록 변경하면 새 형식을 신중하게 설계하더라도 하위 호환성을 깨뜨릴 위험이 생깁니다.

다른 형식

더 큰 정밀도 또는 임의 정밀도를 지원하기 위한 새로운 형식에 관한 많은 아이디어가 제안되었습니다. 정수 기반의 분수, 구조체 또는 2-튜플, 고정 소수점 수 등이 그 예입니다.

다른 형식에 관한 이전의 긴 논의는 PEP 410도 참조하십시오.

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

Python int 유형은 잘 알려져 있고 지원이 잘 되며 조작하기 쉽고, dt = t2 - t1와 같은 모든 산술 연산을 지원합니다.

또한 나노초 단위의 정수를 받아 반환하는 것은 Python에서 새로운 개념이 아니며, os.stat_resultos.utime(ns=(atime_ns, mtime_ns))에서 이를 확인할 수 있습니다.

Note

Python float 유형의 크기가 더 커지면(예: decimal128 또는 float128), time.time()의 정밀도도 증가합니다.

다른 API

새 함수를 추가하지 않기 위해 time.time(ns=False) API를 추가하자는 제안이 있었습니다. Python에서 매개변수에 따라 결과 유형을 변경하는 것은 흔하지 않은(그리고 좋지 않을 수도 있는) 프로그래밍 관행입니다.

사용자가 시간 해상도를 선택할 수 있도록 여러 가지 옵션이 제안되었습니다. 각 Python 모듈이 서로 다른 해상도를 사용하면, 단순히 초(time.time()float를 반환함)와 나노초(time.time_ns()int를 반환함)를 사용하는 대신 서로 다른 해상도를 처리하기가 어려워질 수 있습니다. 또한 위에서 설명했듯이 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() 구문을 사용하도록 time.ns 하위 모듈이나 중첩 네임스페이스를 추가하는 것이지만, 동일한 문제를 겪습니다.

부록: 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에서 Python으로 측정한 시계 해상도입니다(커널 4.12):

함수 해상도
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,000Hz(1MHz)인 CLOCKS_PER_SECOND이며, 해상도는 1us입니다.
  • times()의 주파수는 100Hz인 os.sysconf("SC_CLK_TCK")(또는 HZ 상수)이며, 해상도는 10ms입니다.
  • resource.getrusage(), os.wait3()os.wait4()ru_usage 구조체를 사용합니다. ru_usage.ru_utimeru_usage.ru_stime 필드의 타입은 해상도가 1us인 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입니다. time.time()의 분해능은 시간이 지날수록 매일 시스템 시계에 86,400,000,000,000 나노초가 더해지면서 정밀도 손실이 커지기 때문에 점점 더 나빠질(값이 커질) 뿐입니다.

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

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

Note

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

링크