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

Python 개선 제안 한국어 번역

부록: 구현

객체 상태

객체의 상태와 객체를 소유하는 ThreadGroup 또는 보호하는 뮤텍스의 ID를 기록하려면 객체 헤더에 공간이 필요합니다. 상태는 단일 바이트로 인코딩할 수 있습니다. ID는 모든 ThreadGroup 및 뮤텍스 ID를 처리해야 하므로 16비트로는 충분하지 않을 가능성이 큽니다. 32비트면 충분합니다.

이러한 필드를 사용하면 PyObject 헤더는 현재 PEP 703에 구현된 것보다 작고, 기본값(GIL 사용) 빌드보다는 커야 합니다.

가능한 객체 헤더는 다음과 같습니다:

uint32_t owner_id;
uint32_t ref_count_shared;
PyTypeObject *ob_type;
uint8_t ref_count_local;  // For biased reference counting
uint8_t state;
uint16_t flags;
uint32_t gc_info; // Additional info for the cycle GC

참조 카운팅

작성자는 PEP 703에서 사용된 편향 참조 카운팅 메커니즘을 사용할 것으로 예상합니다. 앞서 언급한 PEP 703과 마찬가지로, 경합을 최소화하기 위해 필요한 경우 스레드별 참조 카운팅과 지연 참조 카운팅도 사용합니다.

객체 상태 확인

CPython은 스택 머신입니다. 즉, 스레드가 객체에 대한 참조를 획득하려면 해당 객체가 힙이나 API 호출에서 가져와져 스택에 푸시되어야 합니다. C 확장이 보아서는 안 되는 객체를 보지 못하도록 모든 C API 함수는 반환 값을 검증해야 합니다. 또한 인터프리터는 힙에서 직접 가져온 모든 값을 스택에 푸시하기 전에 확인해야 합니다.

이는 새 검사가 많이 추가될 가능성이 있으므로 성능에 큰 영향을 주지 않으려면 이러한 검사의 비용을 낮게 유지해야 합니다. 다음과 같이 할 수 있습니다:

  • 검사를 저렴하게 만듭니다. 검사는 메모리 액세스를 최소화하면서 단 하나 또는 두 개의 간단한 비교로만 구성되어야 합니다.
  • 바이트코드 컴파일러와 JIT 컴파일러 모두에서 정적 분석을 사용하여 가능한 한 많은 검사를 제거합니다.

특수화를 사용하면 모든 유효한 상태를 확인하는 대신 가장 가능성이 높은 상태에 대해서만 하나의 검사를 수행할 수 있습니다. 로컬 객체를 예상하는 경우 객체의 스레드 ID를 현재 ThreadGroup ID와 비교하기만 하면 됩니다. 반대로 불변 객체를 예상하는 경우 객체가 불변인지 확인하기만 하면 됩니다.

JIT 컴파일러는 동일한 객체에 대한 중복 검사를 잠재적으로 제거할 수 있습니다.

액세스 제어 함수

local 객체가 가장 가능성이 높다고 가정하므로 스레드 상태를 사용할 수 있다면 이를 먼저 확인합니다.:

PyObject *PyObject_CheckAccessThread(PyObject *op, PyThread t)
{
    PyThreadState *tstate = PyThreadStateFromThread(t);
    if (op->owner_id == tstate->threadgroup_id) {
        return op;
    }
    if (op->state >= SYNCHRONIZED) {
        return op;
    }
    // Check for protected and stop the world cases...
}

반면 스레드 상태를 저렴하게 사용할 수 없다면 공유 가능 사례를 먼저 확인합니다.:

PyObject *PyObject_CheckAccess(PyObject *op)
{
    if (op->state >= SYNCHRONIZED) {
        return op;
    }
    PyThreadState *tstate = PyThreadState_GET();
    if (op->owner_id == tstate->threadgroup_id) {
        return op;
    }
    // Check for protected and stop the world cases...
}

다른 잠금이 이미 유지되고 있을 때 많은 잠금을 획득할 가능성은 낮아 보입니다. 교착 상태가 발생하기 너무 쉽기 때문입니다. 따라서 유지 중인 뮤텍스 집합은 작으며 LIFO 배열(스택)로 구현할 수 있습니다. 일반적으로 객체에 대응하는 뮤텍스는 첫 번째 또는 두 번째 항목이므로 검사는 저렴해야 합니다.

C API

예를 들어 다음과 같은 가상의 API 함수를 생각해 보십시오: PyObject *PyObject_Foo(PyObject *op).

액세스 제어를 지원하도록 PyObject_Foo를 변환하려면 현재 구현의 이름을 먼저 PyObject_FooUnchecked로 바꾼 다음, PyObject_Foo를 다음과 같이 구현합니다.:

PyObject *
PyObject_Foo(PyObject *op)
{
    PyObject *result = PyObject_FooUnchecked(op);
    return _PyObject_CheckAccessNullable(result);
}

여기서 _PyObject_CheckAccessNullable은 접근 제어 검사를 제공하는 내부 함수입니다. 객체 참조가 NULL이 아님이 알려진 경우를 위해 _PyObject_CheckAccess 변형이 제공됩니다.

이 기계적인 변환으로 인해 코드베이스에 일부 비효율성이 남을 가능성이 높으므로, 이후 다시 최적화하려면 추가 작업이 필요합니다.

모든 API 함수는 현재 스레드를 확인해야 하므로, 호출할 때마다 스레드 참조를 가져오는 오버헤드를 줄이기 위해 스레드 참조를 받는 새로운 API가 추가됩니다. 예를 들어 PyObject_GetAttr에는 PyObject_GetAttrThread 변형이 추가됩니다.:

PyObject *PyObject_GetAttrThread(PyObject *v, PyObject *name, PyThread t);

스레드 포인터를 받는 _PyObject_CheckAccess 변형이 추가됩니다.

많은 API 함수는 수정할 필요가 없습니다. 예를 들어 PyObject_Str은 변경 불가능한 str을 항상 반환하므로 추가 접근 제어 검사가 필요하지 않습니다. PyObject_SetItem은 객체를 반환하지 않으므로 추가 검사가 필요하지 않습니다.

인터프리터

힙에서 로드하는 모든 코드에는 접근 제어가 필요합니다. 또한 일부 지역 변수 로드에도 검사가 필요합니다.

지역 변수 접근 속도를 저하시키고 싶지 않으므로, 필요한 곳에만 검사를 삽입하도록 바이트코드 컴파일러에 의존하며, 필요한 경우 LOAD_FAST 대신 LOAD_FAST_MAYBE_UNPROTECTED 명령어를 추가합니다.

힙 또는 C API에서 유래한 객체를 참조하는 참조를 스택에 푸시하는 명령어에는 검사를 추가해야 합니다. 이는 명령어 끝에 검사를 추가하는 것만큼 간단할 수 있으며, 마이크로 연산을 사용하면 추가 마이크로 연산을 추가하는 것만큼 간단할 수 있습니다. 예를 들면 다음과 같습니다.:

macro(LOAD_ATTR_MODULE) =
    unused/1 +
    _LOAD_ATTR_MODULE +
    POP_TOP +
    unused/5 +
    _PUSH_NULL_CONDITIONAL;

다음과 같이 됩니다.:

macro(LOAD_ATTR_MODULE) =
    unused/1 +
    _LOAD_ATTR_MODULE +
    POP_TOP +
    TOS_ACCESS_CHECK +
    unused/5 +
    _PUSH_NULL_CONDITIONAL;

바이트코드 컴파일러

평가 스택의 모든 값은 안전하게 접근할 수 있어야 하고 지역 변수에 저장하는 유일한 방법은 평가 스택에서 가져오는 것이므로, 모든 지역 변수 접근이 안전하다고 보일 수 있습니다. 그러나 실제로는 그렇지 않습니다. with 문에서 값이 지역 변수에 저장된 경우 with 문 밖에서는 보호되지 않을 수 있습니다.

모든 지역 변수 읽기의 속도를 저하시키고 싶지 않으므로, 필요한 곳에 추가 검사를 삽입하기 위해 정적 분석을 수행해야 합니다. 이미 필요한 곳에서만 LOAD_FAST_CHECK를 사용하도록 이러한 검사를 수행하고 있으며, 여기서의 접근 방식도 매우 유사합니다.

알고리즘은 다음과 같이 작동합니다.

  • with 문에서 할당되는 모든 지역 변수를 “보호되지 않음”으로 표시합니다.
  • 데이터 흐름을 사용하여 이 값이 LOAD_FAST로 전달되는 위치를 탐지합니다.
  • “보호되지 않은” LOAD_FAST를 모두 LOAD_FAST_MAYBE_UNPROTECTED로 바꿉니다.
  • 모든 LOAD_FAST_MAYBE_UNPROTECTED는 지역 변수를 다시 보호된 상태로 표시합니다.

with 문의 빈도와 LOAD_FASTLOAD_FAST_BORROW로 변환하는 효과를 고려하면, LOAD_FASTLOAD_FAST_MAYBE_UNPROTECTED로 남는 경우는 극히 적을 것입니다.

동기화된 컬렉션, 불변 컬렉션 및 로컬 컬렉션

기존 컬렉션과 매우 유사한 새로운 클래스를 서너 개 추가하고, 기존 컬렉션 클래스의 코드를 수정합니다. 새로운 코드를 많이 추가하지 않으면서 이를 올바르게 구현하고자 합니다.

set을 예로 들어 보겠습니다(dictlist도 비슷합니다). 기존 메서드에 접근 제어를 추가하고 새로운 클래스인 SynchronizedSet을 추가해야 합니다.

  1. 세 클래스 모두 동일한 레이아웃과 해당 레이아웃을 설명하는 C 구조체를 사용해야 합니다.
  2. 변경하지 않는 메서드는 동기화 없이 접근 제어를 추가한 핵심 함수로 분리해야 합니다.
  1. frozenset은 해당 구현을 직접 사용할 수 있습니다.
  2. SynchronizedSet은 다음 작업 전에 내부 뮤텍스를 획득해야 합니다. 함수를 호출한 후 뮤텍스를 해제해야 합니다.
  3. set은 로컬이므로 기본 구현도 직접 사용할 수 있습니다.
  1. 변경하는 메서드도 동기화 없이 접근 제어를 추가한 핵심 함수로 분리해야 합니다.
  1. frozenset은 변경 메서드를 구현하지 않습니다.
  2. SynchronizedSet은 다음 작업 전에 뮤텍스를 획득해야 합니다. 함수를 호출한 후 뮤텍스를 해제해야 합니다.
  3. set은 로컬이므로 기본 구현을 직접 사용할 수 있습니다.
  1. 다른 동기화된 객체를 인자로 받는 SynchronizedSet메서드는 교착 상태를 방지하기 위해 내부 뮤텍스를 올바른 순서로 획득하도록 보장해야 합니다.

최적화

로컬 객체에 기존 최적화 재사용

로컬 객체는 하나의 ThreadGroup만 액세스할 수 있으므로 현재의 모든 최적화를 변경 없이 적용할 수 있습니다.

세계 정지에 가까운 불변성

일부 객체는, 예를 들어 함수는 하위 호환성을 위해 동기화된 상태이지만 거의 변경되지 않습니다.

이러한 객체에는 세계 정지 잠금을 사용하면서, GIL 사용 빌드에 이미 구현된 것과 동일한 최적화를 JIT에서 적용할 수 있습니다. 이러한 객체 중 하나라도 변경되면 다른 모든 스레드가 협력적으로 중지됩니다. 중지된 후 변경이 수행됩니다. 다른 스레드는 세계 정지 이벤트를 가능한 탈출로 간주하므로 변경에 대비해 가드됩니다.

불변 객체를 위한 가드 없는 최적화

이미 일부 최적화에서 불변성을 활용하고 있지만, 이는 임시방편적인 방식으로 수행됩니다. 불변성이 VM에서 강제되는 속성이 되면, 알려진 불변성을 활용하여 JIT에서 더 많은 가드 제거를 수행할 수 있습니다.

구현 전략

이 PEP를 구현할 때의 가장 큰 과제는 이 PEP의 기능을 추가하면서도 CPython의 기본 빌드가 계속 작동하도록 유지하는 것입니다. 추가해야 할 두 가지 핵심 기능은 ThreadGroups와 객체 소유권입니다. 두 가지가 모두 없으면 어느 쪽도 유용하지 않습니다. 소유권을 구현하려면 앞에서 논의한 ABI 호환성 파괴가 필요합니다.

이를 염두에 두면 다음과 같은 구현 순서를 생각해 볼 수 있습니다.

  • ThreadGroups
  • 일회성 ABI 호환성 파괴
  • 자유 스레딩 빌드에서 편향 및 지연 참조 카운팅 포팅
  • 단순 소유권 로컬 및 불변 객체만
  • 병렬 할당 및 순환 가비지 수집 지원
  • __freeze__
  • 동기화된 객체
  • 바이트코드 컴파일러 지원을 포함한 보호된 객체 상태
  • TransferBoxChannel
  • sys.monitoring.StopTheWorld
  • 성능 작업

검증

정확성과 성능을 모두 얻기 위해, 이 PEP는 건전하고 최적화 가능한 실행 모델을 제공합니다. JIT 또는 인터프리터의 최적화 맥락에서 이러한 건전성을 검증하기 위해, 인터프리터에서 참조가 스택에 푸시되는 모든 지점에 디버그 빌드의 검증 기능이 추가됩니다.