[Final] PEP 683 - Immortal Objects, Using a Fixed Refcount

원문 링크: PEP 683 - Immortal Objects, Using a Fixed Refcount

상태: Final 유형: Standards Track 작성일: 10-Feb-2022

PEP 683 – 고정된 참조 카운트(Refcount)를 사용하는 불멸(Immortal) 객체

목표

이 PEP는 CPython 내부적으로 특정 객체들을 “불멸(Immortal)”로 지정하여 참조 카운트(refcount) 조작으로 인한 오버헤드를 줄이고, 멀티코어 및 멀티프로세스 환경에서의 성능과 확장성을 개선하는 것을 목표로 합니다.

개요 (Abstract)

현재 CPython 런타임은 각 객체에 할당된 메모리에 적은 양의 가변 상태를 유지하고 있습니다. 이로 인해 불변(immutable) 객체로 간주되는 것들도 실제로는 가변적입니다. 이러한 특성은 특히 Python의 확장성을 높이려는 접근 방식에서 CPU 및 메모리 성능에 큰 부정적인 영향을 미칠 수 있습니다.

이 제안은 CPython이 내부적으로 객체를 “불멸”로 표시하여 런타임 상태가 더 이상 변경되지 않도록 지원할 것을 요구합니다. 결과적으로, 이러한 객체의 refcount는 절대로 0에 도달하지 않으므로 객체는 정리되지 않습니다 (런타임 종료와 같이 안전하다고 판단될 때 제외). 이러한 객체를 “불멸 객체(immortal objects)”라고 부릅니다. 이 개선의 핵심은 이제 객체가 진정으로 불변할 수 있다는 점입니다.

적용 범위 (Scope)

객체 불멸성(object immortality)은 내부 전용 기능이므로, 이 제안은 공개 API 또는 동작에 대한 변경을 포함하지 않습니다 (한 가지 예외 제외). 불멸 객체로 표시하거나 불멸 객체인지 확인하는 등의 작업을 위해 일부 비공개(하지만 공개적으로 접근 가능한) API를 추가할 수 있습니다. 이 기능을 사용자에게 노출하려는 모든 노력은 별도로 제안되어야 합니다.

“동작 변경 없음” 규칙에는 한 가지 예외가 있습니다. 불멸 객체의 refcount 의미는 일부 경우에 사용자 기대와 다를 수 있습니다. 이 예외와 해결책은 아래에서 논의됩니다.

이 PEP의 대부분은 위 요구 사항을 충족하는 내부 구현에 초점을 맞추고 있습니다. 그러나 이러한 구현 세부 사항이 엄격하게 지시적인 것은 아닙니다. 대신, 최소한 요구 사항에 필요한 기술적 고려 사항을 설명하는 데 도움이 되도록 포함되어 있습니다. 실제 구현은 아래에 설명된 제약 조건을 충족하는 한 다소 다를 수 있습니다. 또한, 아래에 설명된 특정 구현 세부 사항의 수용 여부는 명시적으로 지정되지 않는 한 이 PEP의 상태에 의존하지 않습니다.

예를 들어, 다음의 특정 세부 사항들은 CPython 고유의 비공개 구현 세부 사항이며 이후 버전에서 변경될 수 있습니다.

  • 무엇을 불멸로 표시하는지
  • 무엇을 불멸로 인식하는지
  • 기능적으로 불멸인 객체 중 어떤 하위 집합이 불멸로 표시되는지
  • 불멸 객체에 대해 어떤 메모리 관리 활동이 건너뛰거나 수정되는지

구현 요약 (Implementation Summary)

고수준에서의 구현은 다음과 같습니다:

  • 객체의 refcount가 매우 특정한 값(_Py_IMMORTAL_REFCNT로 정의됨)과 일치하면 해당 객체는 불멸로 처리됩니다.
  • CPython C-API 및 런타임은 불멸 객체의 refcount (또는 기타 런타임 상태)를 수정하지 않습니다.
  • 런타임은 이제 정적으로 할당된 경우를 제외하고 종료 과정(finalization) 중에 모든 불멸 객체를 명시적으로 해제할 책임이 있습니다.

refcount 의미의 변경 외에 고려해야 할 다른 잠재적인 부정적인 영향은 없습니다. 불멸 객체에 대한 “허용 가능한” 성능 저하의 임계값은 2%입니다 (2022년 언어 서밋 합의). 아래 설명된 접근 방식의 순진한 구현은 CPython을 약 4% 느리게 만들지만, 알려진 완화책을 적용하면 성능은 ~성능 중립적(performance-neutral)~으로 돌아옵니다.

동기 (Motivation)

현재 모든 객체는 사실상 가변적이며, 이는 str 인스턴스와 같은 “불변” 객체도 포함합니다. 그 이유는 모든 객체의 refcount가 실행 중에 객체가 사용될 때마다 자주 수정되기 때문입니다. 이는 None과 같이 일반적으로 사용되는 많은 전역(builtin) 객체에 특히 중요합니다. 이러한 객체는 Python 코드와 내부적으로 많이 사용되며, 이는 지속적으로 많은 양의 refcount 변경으로 이어집니다.

모든 Python 객체의 유효한 가변성은 Instagram과 같은 확장성을 목표로 하는 프로젝트나 GIL(Global Interpreter Lock)을 인터프리터별로(per-interpreter) 만들려는 노력과 같은 Python 커뮤니티의 일부에 구체적인 영향을 미칩니다. refcount 수정이 이러한 프로젝트에 실제 부정적인 영향을 미치는 몇 가지 방법을 아래에서 설명합니다. 진정으로 불변인 객체에는 이러한 문제가 발생하지 않습니다.

CPU 캐시 무효화 감소 (Reducing CPU Cache Invalidation)

refcount를 수정할 때마다 해당 CPU 캐시 라인이 무효화됩니다. 이는 여러 가지 영향을 미칩니다:

  • 쓰기 작업은 다른 캐시 수준과 주 메모리로 전파되어야 합니다. 이는 모든 Python 프로그램에 작은 영향을 미칩니다. 불멸 객체는 이러한 면에서 약간의 부담을 덜어줄 것입니다.
  • 멀티코어 애플리케이션은 추가적인 비용을 지불합니다. 두 스레드가 동일한 객체(None 등)와 상호 작용하는 경우, increfdecref가 발생할 때마다 서로의 캐시를 무효화하게 됩니다. 이는 True, 0, str 인스턴스와 같이 불변 객체로 간주되는 경우에도 마찬가지입니다. CPython의 GIL은 한 번에 하나의 스레드만 실행되므로 이러한 효과를 줄이는 데 도움이 되지만, 페널티를 완전히 제거하지는 못합니다.

데이터 경쟁 회피 (Avoiding Data Races)

멀티코어와 관련하여, GIL을 인터프리터별 잠금(per-interpreter lock)으로 만들어 진정한 멀티코어 병렬 처리를 가능하게 하는 것을 고려하고 있습니다. 현재 GIL은 동일한 객체를 incref 또는 decref할 수 있는 여러 동시 스레드 간의 경쟁을 방지합니다. 공유 GIL이 없으면 두 개의 실행 중인 인터프리터는 None과 같이 불변 객체라도 안전하게 객체를 공유할 수 없습니다.

이는 인터프리터별 GIL을 가지려면 각 인터프리터가 모든 객체의 자체 복사본을 가져야 함을 의미합니다. 여기에는 싱글톤(singletons) 및 정적 타입(static types)도 포함됩니다. 이를 위한 실행 가능한 전략이 있지만 상당한 추가 노력과 복잡성이 필요합니다.

대안은 모든 공유 객체가 진정으로 불변임을 보장하는 것입니다. 수정이 없으므로 경쟁도 없을 것입니다. 이는 이 제안에서 제안하는 불멸성이 다른 불변 객체에 대해 가능하게 할 것입니다. 불멸 객체를 사용하면 인터프리터별 GIL 지원이 훨씬 간단해집니다.

Copy-on-Write 회피 (Avoiding Copy-on-Write)

일부 애플리케이션의 경우, 원하는 초기 상태로 애플리케이션을 설정한 다음 각 워커(worker)에 대해 프로세스를 fork하는 것이 합리적입니다. 이는 특히 메모리 사용량 측면에서 큰 성능 향상을 가져올 수 있습니다. Instagram, YouTube와 같은 여러 기업 Python 사용자들이 이를 활용했습니다. 그러나 위에서 언급한 refcount 의미는 이러한 이점을 크게 줄여 일부 최적화되지 않은 해결책으로 이어졌습니다.

또한 “fork”가 copy-on-write 의미를 사용하는 유일한 운영 체제 메커니즘이 아니라는 점에 유의하십시오. mmap도 또 다른 예입니다. 이러한 유틸리티는 “가변” 객체만 사용하는 경우보다 불멸 객체가 관련될 때 copy-on-write 횟수가 줄어들어 잠재적으로 이점을 얻을 수 있습니다.

근거 (Rationale)

제안된 해결책은 이 제안의 두 저자가 독립적으로 동일한 결론 (그리고 거의 동일한 구현)에 도달했을 정도로 명백합니다. Pyston 프로젝트도 유사한 접근 방식을 사용합니다. 다른 설계들도 고려되었습니다. 지난 몇 년 동안 python-dev에서도 여러 가능성이 논의되었습니다.

대안으로는 다음이 있습니다:

  • “불멸”을 표시하기 위해 상위 비트를 사용하지만 Py_INCREF()를 변경하지 않음
  • 객체에 명시적 플래그를 추가
  • 타입(tp_dealloc()이 no-op인 경우)을 통해 구현
  • 객체의 타입 객체를 통해 추적
  • 별도의 테이블로 추적

위의 각 방법은 객체를 불멸로 만들지만, 위에서 설명한 refcount 수정으로 인한 성능 페널티를 해결하지는 못합니다.

인터프리터별 GIL의 경우, 유일한 현실적인 대안은 모든 전역 객체를 PyInterpreterState로 이동하고, 해당 객체에 접근하기 위해 하나 이상의 조회 함수를 추가하는 것입니다. 그러면 노출된 많은 객체에 대한 호환성을 유지하기 위해 C-API에 몇 가지 해킹을 추가해야 할 것입니다. 불멸 객체를 사용하면 훨씬 더 간단해집니다.

영향 (Impact)

이점 (Benefits)

가장 주목할 만한 점은 위 예시에서 설명된 경우들이 불멸 객체로부터 큰 이점을 얻을 수 있다는 것입니다. pre-fork를 사용하는 프로젝트는 해결책을 중단할 수 있습니다. 인터프리터별 GIL 프로젝트의 경우, 불멸 객체는 기존 정적 타입(static types)과 공개 C-API에 의해 노출되는 객체에 대한 해결책을 크게 단순화합니다.

일반적으로 객체에 대한 강력한 불변성 보장은 Python 애플리케이션이 더 잘 확장될 수 있도록 합니다. 특히 다중 프로세스 배포에서 그렇습니다. 이는 현재와 같은 상당한 메모리 사용량의 트레이드오프 없이 멀티코어 병렬 처리를 활용할 수 있기 때문입니다. 방금 설명한 사례와 동기(Motivation)에서 설명된 사례는 이러한 개선 사항을 반영합니다.

성능 (Performance)

순진한 구현은 2%의 속도 저하를 보입니다 (MSVC에서는 3%). 몇 가지 기본적인 완화책을 적용하면 ~성능 중립적(performance-neutral)~으로 돌아오는 것을 입증했습니다.

긍정적인 측면에서, 불멸 객체는 pre-fork 모델과 함께 사용할 때 상당한 양의 메모리를 절약합니다. 또한, 불멸 객체는 eval loop에서 최적화 기회를 제공하여 성능을 향상시킬 수 있습니다.

하위 호환성 (Backward Compatibility)

이상적으로는 이 내부 전용 기능이 완전히 호환되어야 합니다. 그러나 일부 경우 refcount 의미의 변경을 포함합니다. 불멸 객체만 영향을 받지만, 여기에는 None, True, False와 같이 많이 사용되는 객체도 포함됩니다.

특히, 불멸 객체가 관련된 경우:

  • refcount를 검사하는 코드는 매우 큰 값을 보게 될 것입니다.
  • 새로운 noop 동작은 다음 코드를 손상시킬 수 있습니다:
    • refcount가 항상 증가하거나 감소할 것이라고 (또는 Py_SET_REFCNT()에서 특정 값을 가질 것이라고) 특정하게 의존하는 코드
    • 0 또는 1이 아닌 특정 refcount 값에 의존하는 코드
    • refcount를 직접 조작하여 추가 정보를 저장하는 코드
  • 32비트 pre-3.12 Stable ABI 확장에서는 Accidental Immortality로 인해 객체가 누수될 수 있습니다.
  • 이러한 확장은 Accidental De-Immortalizing으로 인해 충돌할 수 있습니다.

이러한 동작 변경은 불멸 객체에만 적용되며, 사용자가 사용하는 대다수의 객체에는 적용되지 않습니다. 또한, 사용자는 객체를 불멸로 표시할 수 없으므로 사용자 생성 객체는 이러한 변경된 동작을 갖지 않습니다. 전역(builtin) 객체의 변경된 동작에 의존하는 사용자는 이미 문제가 발생할 수 있습니다. 따라서 전체적인 영향은 작을 것입니다.

refleaks를 확인하는 코드는 하드코딩된 작은 값을 일부 불멸 객체와 비교하여 확인하지 않는 한 계속 잘 작동해야 합니다. Pyston에서 발견된 문제는 refcount를 수정하지 않으므로 여기에는 적용되지 않습니다.

우발적 불멸화 (Accidental Immortality)

가정적으로, 불멸이 아닌 객체가 너무 많이 incref되어 불멸로 간주되는 “마법의 값”에 도달할 수 있습니다. 이는 refcount가 다시 0으로 돌아오지 않아 우발적으로 누수될 수 있음을 의미합니다 (절대 정리되지 않음).

64비트 refcount의 경우, 이러한 우발적인 시나리오는 걱정할 필요가 없을 정도로 희박합니다. 32비트 플랫폼과 같이 최대 refcount가 훨씬 작은 빌드에서는 결과가 그렇게 명확하지 않습니다. 그러나 increfdecref가 균형을 이루지 않는 한, 그리고 해당 객체가 tp_dealloc()에 의해 호출될 때까지 refcount가 0에 도달할 기회를 얻지 못하는 한, 여전히 문제가 될 가능성은 매우 낮습니다.

Stable ABI

이 PEP에 설명된 구현 접근 방식은 stable ABI로 컴파일된 확장 기능과 호환됩니다 (Accidental ImmortalityAccidental De-Immortalizing은 예외). stable ABI의 특성상, 이러한 확장은 객체의 ob_refcnt 필드를 직접 수정하는 Py_INCREF() 등을 사용합니다. 이는 불멸 객체의 모든 성능 이점을 무효화합니다.

그러나, 우리는 이러한 상황에서도 불멸 객체가 (대부분) 불멸 상태를 유지하도록 보장합니다. 불멸 객체의 초기 refcount를 객체를 불멸로 식별할 수 있는 값으로 설정하고, 확장에 의해 refcount가 수정되더라도 계속해서 그렇게 식별되도록 합니다.

우발적 불멸 해제 (Accidental De-Immortalizing)

Accidental De-Immortalizing은 32비트 빌드의 이전 stable ABI 확장 기능이 이미 불멸인 객체를 decref하여 “가변” 상태로 만들 수 있는 경우를 나타냅니다. 예를 들어, None과 같은 객체가 decref되어 refcount가 마법의 불멸 refcount 값 아래로 떨어지는 경우입니다.

이러한 일이 발생할 가능성은 32비트 빌드에서도 매우 낮습니다. 해당 객체에 대해 상응하는 incref 없이 약 10억 번의 decref가 필요합니다. 하지만 문제가 발생한다면, tp_dealloc()에서 불멸 객체의 refcount_Py_IMMORTAL_REFCNT로 재설정하는 간단하지만 효과적인 해결책을 적용할 수 있습니다.

기타 Python 구현 (Alternate Python Implementations)

이 제안은 CPython에만 해당됩니다. 그러나 C-API의 동작과 관련이 있으므로 다른 Python 구현에 영향을 미칠 수 있습니다. 결과적으로 위에서 “하위 호환성”에서 설명된 변경된 동작의 영향이 여기에도 적용됩니다 (예: 다른 구현이 0이 아닌 특정 refcount 값에 밀접하게 연결되어 있거나 refcount가 정확히 어떻게 변경되는지에 의존하는 경우 영향을 받을 수 있습니다).

보안 영향 (Security Implications)

이 기능은 알려진 보안 영향을 미치지 않습니다.

유지 보수성 (Maintainability)

이것은 복잡한 기능이 아니므로 유지 관리자에게 많은 정신적 오버헤드를 유발하지 않을 것입니다. 기본적인 구현은 많은 코드를 건드리지 않으므로 유지 보수성에 큰 영향을 미치지 않을 것입니다. 성능 페널티 완화로 인해 약간의 추가 복잡성이 있을 수 있지만, 이는 런타임 초기화 후 모든 객체를 불멸로 만들고 나중에 런타임 종료 시 명시적으로 해제하는 경우로 제한될 것입니다. 이 코드는 비교적 집중적일 것입니다.

명세 (Specification)

이 접근 방식은 다음과 같은 근본적인 변경 사항을 포함합니다:

  • 내부 C-API에 _Py_IMMORTAL_REFCNT (마법의 값) 추가
  • 마법 refcount와 일치하는 객체에 대해 Py_INCREF()Py_DECREF()no-op으로 업데이트
  • refcount를 수정하는 다른 모든 API에 대해서도 동일하게 적용
  • 불멸 GC 객체 ("컨테이너")에 대해 PyGC_Head 수정을 중지
  • 런타임 종료 중에 모든 불멸 객체가 정리되도록 보장

모든 객체의 refcount_Py_IMMORTAL_REFCNT로 설정하면 불멸 객체가 됩니다.

공개 Refcount 세부 정보 (Public Refcount Details)

“하위 호환성”에서 우리는 이 제안의 변경으로 인해 사용자 코드가 손상될 수 있는 가능한 방법을 소개했습니다. 사용자에 의한 오해는 refcount 관련 API의 이름과 문서가 해당 API(및 refcount 일반)를 설명하는 방식에 크게 기인할 수 있습니다.

이 제안의 일환으로, 사용자가 refcount 동작의 어떤 부분에 의존할 수 있고 어떤 부분이 구현 세부 사항으로 간주되는지 명확하게 이해하도록 해야 합니다. 특히, 사용자는 기존의 공개 refcount 관련 API를 사용해야 하며, 의미 있는 refcount 값은 0과 1뿐입니다. 다른 모든 값은 “0 또는 1이 아님“으로 간주됩니다.

이 정보는 문서에 명확히 설명될 것입니다.

제약 조건 (Constraints)

  • 불변 객체로 간주되는 것들을 진정으로 불변하게 만들도록 보장합니다.
  • 일반적인 Python 사용 사례에 대한 성능 페널티를 최소화합니다.
  • 런타임 종료까지 지속될 것으로 예상하지 않는 객체를 불멸로 만들 때 주의합니다.
  • 불변 객체로 간주되지 않는 객체를 불멸로 만들 때 주의합니다.
  • __del__weakrefs는 계속 제대로 작동해야 합니다.

불멸 가변 객체 (Immortal Mutable Objects)

모든 객체는 불멸로 표시될 수 있습니다. 우리는 어떠한 제한이나 검사도 제안하지 않습니다. 그러나 실제로 객체를 불멸로 만드는 가치는 해당 객체의 가변성과 애플리케이션 수명 중 충분한 부분 동안 사용될 가능성에 달려 있습니다. 가변 객체를 불멸로 표시하는 것은 일부 상황에서 합리적일 수 있습니다.

불멸 객체의 많은 사용 사례는 불변성에 중점을 두어 스레드가 잠금 없이 안전하고 효율적으로 객체를 공유할 수 있도록 합니다. 이러한 이유로 dict 또는 list와 같은 가변 객체는 공유되지 않을 것입니다 (따라서 불멸성도 없음). 그러나 일반적으로 가변 객체가 실제로 수정되지 않는다는 충분한 보장이 있다면 불멸성이 적절할 수 있습니다.

암묵적 불멸 객체 (Implicitly Immortal Objects)

불멸 객체가 일반 (가변) 객체에 대한 참조를 유지하는 경우, 해당 참조된 객체는 사실상 불멸이 됩니다. 이는 불멸 객체가 해당 객체를 해제하기 전까지는 refcount가 0에 도달할 수 없기 때문입니다.

예시:

  • dictlist와 같은 컨테이너
  • PyTypeObjecttp_subclassestp_weaklist와 같이 내부적으로 참조를 유지하는 객체
  • 객체의 타입 (ob_type에 유지됨)

이러한 참조된 객체는 참조되는 동안 암묵적으로 불멸입니다. 실제로 이는 동작 변경이 아니므로 실제 결과는 없을 것입니다. 유일한 차이점은 (참조를 유지하는) 불멸 객체가 절대로 정리되지 않는다는 것입니다.

우리는 이러한 암묵적 불멸 객체를 어떤 식으로든 변경할 것을 제안하지 않습니다. 불멸 객체에 의해 참조된다는 이유만으로 명시적으로 불멸로 표시되어서는 안 됩니다. 그렇게 하는 것은 아무것도 하지 않는 것보다 이점이 없습니다.

객체 불멸 해제 (Un-Immortalizing Objects)

이 제안은 불멸 객체를 “정상” 상태로 되돌리는 메커니즘을 포함하지 않습니다. 현재 그러한 기능이 필요하지 않습니다.

가장 확실한 접근 방식은 refcount를 작은 값으로 설정하는 것이지만, 어떤 값이 안전한지 알 수 없습니다. 이상적으로는 불멸이 아니었을 때의 값으로 설정해야 하지만, 그 값은 이미 오래전에 손실되었을 것입니다. 따라서 관련된 복잡성으로 인해 좋은 이유가 있더라도 객체를 안전하게 불멸 해제하는 것이 어려워집니다.

_Py_IMMORTAL_REFCNT

두 개의 내부 상수가 추가됩니다:

  • _Py_IMMORTAL_BIT: 가장 높은 사용 가능한 비트가 설정된 값 (예: 2^62)
  • _Py_IMMORTAL_REFCNT: 가장 높은 두 개의 사용 가능한 비트가 설정된 값

불멸 객체의 refcount_Py_IMMORTAL_REFCNT로 설정됩니다. 하지만 객체가 불멸인지 확인하려면 refcount_Py_IMMORTAL_BIT와 비트 AND 연산하여 비교합니다. 이 차이는 refcount가 어떻게든 수정되더라도 (예: 이전 stable ABI 확장에 의해) 불멸 객체가 여전히 불멸로 간주됨을 의미합니다.

영향 받는 API (Affected API)

이제 불멸 객체를 무시할 API는 다음과 같습니다:

  • (공개) Py_INCREF()
  • (공개) Py_DECREF()
  • (공개) Py_SET_REFCNT()
  • (비공개) _Py_NewReference()

refcount를 노출하는 API (변경되지 않지만 이제 큰 값을 반환할 수 있음):

  • (공개) Py_REFCNT()
  • (공개) sys.getrefcount()

또한, 불멸 객체는 GC에 참여하지 않습니다.

불멸 전역 객체 (Immortal Global Objects)

모든 런타임 전역(runtime-global) (내장 builtin) 객체는 불멸로 만들어집니다. 여기에는 다음이 포함됩니다:

  • 싱글톤 (None, True, False, Ellipsis, NotImplemented)
  • 모든 정적 타입 (PyLong_Type, PyExc_Exception 등)
  • _PyRuntimeState.global_objects의 모든 정적 객체 (예: 식별자, 작은 정수)

전체 객체를 실제로 불변하게 만드는 문제 (예: per-interpreter GIL의 경우)는 이 PEP의 범위를 벗어납니다.

객체 정리 (Object Cleanup)

런타임 종료(runtime finalization) 중에 모든 불멸 객체를 정리하려면 해당 객체들을 추적해야 합니다.

GC 객체("컨테이너")의 경우, 모든 불멸 컨테이너를 GC의 permanent generation으로 푸시하여 GC의 permanent generation을 활용합니다. 런타임 종료 시, 런타임이 이러한 인스턴스들을 정상적으로 해제하기 위해 최선을 다하는 전략을 취할 것입니다.

컨테이너가 아닌 객체의 경우, 추적 방식은 상황에 따라 달라집니다. 거의 모든 경우에 이러한 객체는 런타임 상태에서 직접 접근 가능합니다 (예: _PyRuntimeState 또는 PyInterpreterState 필드). 소수의 객체에 대한 추적 메커니즘을 런타임 상태에 추가해야 할 수도 있습니다.

어떤 정리도 성능에 큰 영향을 미치지 않을 것입니다.

성능 저하 완화 (Performance Regression Mitigations)

이 섹션은 제안의 실제 일부는 아니지만, 순진한 구현에서 발생하는 4%의 성능 손실을 회복하기 위한 몇 가지 방법을 설명합니다.

  • 런타임 초기화 종료 시 모든 객체를 불멸로 표시: Immortal Mutable Objects에서 얻은 개념을 사용하여 incref/decref의 추가 비용을 피할 수 있습니다.
  • 불필요한 하드코딩된 refcount 작업 제거: Py_RETURN_NONE과 같이 불멸 객체와 상호 작용하는 C-API의 일부는 refcount 작업을 제거하도록 업데이트할 수 있습니다.
  • eval loop에서 불멸 객체에 대한 특수화: 특정 알려진 불멸 객체(None 등)와 관련된 eval loop의 작업을 최적화할 기회가 있습니다.
  • 기타 가능성: 모든 interned string을 불멸로 표시, interned 딕셔너리를 불멸로 표시, 모듈에 대해 마셜링 해제된 모든 상수를 불멸로 표시, (불변) 불멸 객체를 자체 메모리 페이지에 할당, 32비트 최하위 비트를 사용하여 포화된 refcount 사용 등이 있습니다.

우발적 불멸 해제에 대한 해결책 (Solutions for Accidental De-Immortalization)

Accidental De-Immortalizing 섹션에서 불멸 객체의 잠재적인 부정적인 결과를 설명했습니다. 여기에서는 이를 다루기 위한 몇 가지 옵션을 살펴봅니다.

해결책은 문제를 어떻게 해결할지 지시하기보다는 만족스러운 옵션이 있음을 보여주기 위해 열거됩니다.

핵심 관찰 사항은 특정 조건이 충족될 때 불멸 객체의 refcount_Py_IMMORTAL_REFCNT로 재설정할 수 있다는 것입니다.

이를 염두에 두고, 간단하면서도 효과적인 해결책은 tp_dealloc()에서 불멸 객체의 refcount를 재설정하는 것입니다. NoneTypebool은 이미 Py_FatalError()를 호출하는 tp_dealloc()을 가지고 있습니다. 다른 타입들도 특정 조건에 따라 동일하게 적용됩니다. 이러한 타입의 경우 refcount를 재설정하고, 나머지 경우에는 검사를 도입합니다. 모든 경우에 tp_dealloc()의 검사 오버헤드는 중요하지 않을 것입니다.

다른 (덜 실용적인) 해결책:

  • 불멸 객체의 refcount를 주기적으로 재설정
  • 사용 빈도가 높은 객체에 대해서만 수행
  • stable-ABI 확장이 가져온 경우에만 수행
  • 불멸성을 비활성화하는 런타임 플래그 제공

문서화 (Documentation)

불멸 객체의 동작 및 API는 내부 구현 세부 사항이므로 문서에 추가되지 않습니다.

그러나 refcount 동작에 대한 공개 보장을 더 명확히 하기 위해 문서를 업데이트할 것입니다. 특히 다음을 포함합니다:

  • Py_INCREF(): “객체 o의 참조 카운트를 증가시킵니다.”를 “객체 o에 대한 새 참조를 가져왔음을 나타냅니다.”로 변경.
  • Py_DECREF(): “객체 o의 참조 카운트를 감소시킵니다.”를 “이전에 가져온 객체 o에 대한 참조를 더 이상 사용하지 않음을 나타냅니다.”로 변경.
  • Py_XINCREF(), Py_XDECREF(), Py_NewRef(), Py_XNewRef(), Py_Clear()에 대해서도 유사하게 적용.
  • Py_REFCNT(): “refcount 0과 1은 특정 의미를 가지며, 다른 모든 값은 값에 관계없이 어딘가에서 객체를 사용하고 있음을 의미합니다. 0은 객체가 사용되지 않고 정리될 것임을 의미합니다. 1은 코드가 정확히 하나의 참조를 가지고 있음을 의미합니다.”를 추가.
  • Py_SET_REFCNT(): 1을 초과하는 값이 다른 값으로 대체될 수 있는 방법에 대해 Py_REFCNT()를 참조하도록 변경.

또한, 사용자가 변경 사항에 놀라지 않도록 다음 내용에 불멸 객체에 대한 참고 사항을 추가할 수 있습니다:

  • Py_SET_REFCNT() (불멸 객체에 대한 no-op)
  • Py_REFCNT() (값이 놀랍도록 클 수 있음)
  • sys.getrefcount() (값이 놀랍도록 클 수 있음)

이러한 참고 사항으로 이점을 얻을 수 있는 다른 API는 현재 문서화되어 있지 않습니다. 이 기능은 사용자에게는 투명하므로 다른 곳 ( Py_INCREF()Py_DECREF() 포함)에는 이러한 참고 사항을 추가하지 않을 것입니다.

참조 구현 (Reference Implementation)

구현은 GitHub에 제안되어 있습니다: https://github.com/python/cpython/pull/19474

⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments