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

Python 개선 제안 한국어 번역

PEP 683 – 불멸 객체, 고정 참조 횟수 사용

Author:
Eric Snow <ericsnowcurrently at gmail.com>, Eddie Elizondo <eduardo.elizondorueda at gmail.com>
Discussions-To:
Discourse thread
Status:
Final
Type:
Standards Track
Created:
10-Feb-2022
Python-Version:
3.12
Post-History:
16-Feb-2022, 19-Feb-2022, 28-Feb-2022, 12-Aug-2022
Resolution:
Discourse message

Table of Contents

번역·라이선스 안내

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

Important

This PEP is a historical document. The up-to-date, canonical documentation can now be found at reference count.

×

See PEP 1 for how to propose changes.

PEP 승인 조건

다음 조건을 달아 PEP가 승인되었습니다:

  • Solutions for Accidental De-Immortalization에 제시된 주요 제안(tp_dealloc()에서 불멸 참조 횟수를 재설정)이 적용되었습니다.
  • 이를 적용하지 않은 형식은 CPython 코드에서 불멸화하지 않았습니다.
  • 구현이 완료된 후 최종 벤치마크 결과를 반영하여 PEP를 업데이트했습니다(해당 변경에 가치가 있음을 확인했습니다).

초록

현재 CPython 런타임은 각 객체의 할당된 메모리에 small amount of mutable state를 유지합니다. 이 때문에 본래 불변이어야 하는 객체가 실제로는 변경 가능합니다. 이는 CPU 및 메모리 성능에 큰 부정적 영향을 줄 수 있으며, 특히 Python의 확장성을 높이는 접근 방식에서 그러합니다.

이 제안은 CPython이 내부적으로 해당 런타임 상태가 더 이상 변경되지 않는 객체로 표시하는 기능을 지원하도록 규정합니다. 따라서 이러한 객체의 참조 횟수는 결코 0에 도달하지 않으며, 객체는 절대 정리되지 않습니다(런타임 종료 중처럼 런타임이 정리해도 안전하다고 판단하는 경우는 제외합니다). 이러한 객체를 “불멸”이라고 부릅니다. (일반적으로 내부 객체 중 비교적 적은 수만 불멸 객체가 됩니다.) 여기서 얻는 근본적인 개선점은 이제 객체가 진정으로 불변일 수 있다는 것입니다.

범위

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

“동작 변경 없음”에는 한 가지 예외가 있습니다. 불멸 객체의 참조 횟수 계산 의미가 일부 경우 사용자의 예상과 달라집니다. 이 예외와 해결책은 아래에서 설명합니다.

이 PEP의 대부분은 위의 규정을 충족하는 내부 구현에 초점을 맞춥니다. 그러나 이러한 구현 세부 사항은 엄격하게 규정하려는 것이 아닙니다. 대신 최소한 해당 규정에 필요한 기술적 고려 사항을 설명하는 데 도움이 되도록 포함했습니다. 실제 구현은 아래에 제시된 제약 조건을 충족하는 한 어느 정도 달라질 수 있습니다. 또한 아래에 설명된 특정 구현 세부 사항의 수용 가능성은 명시적으로 지정되지 않는 한 이 PEP의 상태에 좌우되지 않습니다.

예를 들어 다음과 같은 세부 사항이 있습니다:

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

이러한 사항은 CPython에만 국한되지 않을 뿐 아니라 이후 버전에서 변경될 것으로 예상되는 비공개 구현 세부 사항이기도 합니다.

구현 요약

다음은 구현을 개괄적으로 살펴본 내용입니다:

객체의 참조 카운트가 매우 특정한 값(아래에 정의됨)과 일치하면 해당 객체는 불멸 객체로 취급합니다. CPython C-API와 런타임은 불멸 객체의 참조 카운트(또는 기타 런타임 상태)를 수정하지 않습니다. 정적으로 할당된 객체가 아닌 한, 이제 런타임이 종료 과정에서 모든 불멸 객체를 명시적으로 할당 해제할 책임을 집니다. (아래의 Object Cleanup을 참조하십시오.)

참조 카운트 의미 체계의 변경 외에도 고려해야 할 부정적인 영향이 한 가지 더 있을 수 있습니다. 불멸 객체에 대해 “허용 가능한” 성능 저하의 기준은 2%입니다(2022년 언어 정상 회의에서의 합의). 아래에 설명된 접근 방식을 순진하게 구현하면 CPython이 대략 4% 느려집니다. 그러나 알려진 완화책을 적용하면 구현은 ~성능 중립적~입니다.

TODO: 최신 브랜치의 성능 영향을 업데이트하십시오(GCC와 clang 모두).

동기

위에서 언급했듯이 현재 모든 객체는 사실상 변경 가능합니다. 여기에는 str 인스턴스와 같은 “변경 불가능한” 객체도 포함됩니다. 이는 실행 중 객체가 사용되는 동안 모든 객체의 참조 카운트가 빈번하게 수정되기 때문입니다. 이는 None과 같이 흔히 사용되는 여러 전역(내장) 객체에서 특히 중요합니다. 이러한 객체는 Python 코드와 내부적으로 모두 매우 많이 사용됩니다. 그 결과 참조 카운트 변경이 지속적으로 대량 발생합니다.

모든 Python 객체가 사실상 변경 가능하다는 점은 Instagram처럼 확장성을 목표로 하는 프로젝트나 GIL을 인터프리터별로 만들려는 노력 등 Python 커뮤니티의 일부에 구체적인 영향을 미칩니다. 아래에서는 참조 카운트 수정이 이러한 프로젝트에 실제로 부정적인 영향을 미치는 몇 가지 방식을 설명합니다. 진정으로 변경 불가능한 객체라면 이러한 일은 발생하지 않습니다.

CPU 캐시 무효화 줄이기

참조 카운트를 수정할 때마다 해당 CPU 캐시 라인이 무효화됩니다. 여기에는 여러 영향이 있습니다.

우선 쓰기 작업을 다른 캐시 수준과 주 메모리로 전파해야 합니다. 이는 모든 Python 프로그램에 작은 영향을 미칩니다. 불멸 객체는 이와 관련하여 약간의 부담을 덜어 줄 수 있습니다.

또한 멀티코어 애플리케이션은 비용을 치러야 합니다. 서로 다른 코어에서 동시에 실행되는 두 스레드가 동일한 객체(예: None)와 상호 작용하면, 각 incref와 decref가 발생할 때마다 서로의 캐시를 무효화하게 됩니다. 이는 True, 0, str 인스턴스처럼 그 밖에는 변경 불가능한 객체에도 해당합니다. CPython의 GIL은 한 번에 하나의 스레드만 실행되므로 이러한 영향을 줄이는 데 도움이 되지만, 비용을 완전히 제거하지는 못합니다.

데이터 경쟁 방지

멀티코어와 관련하여, 진정한 멀티코어 병렬 처리를 가능하게 하는 인터프리터별 잠금으로 GIL을 만들 것을 고려하고 있습니다. 무엇보다도 현재 GIL은 동일한 객체에 대해 incref 또는 decref를 수행할 수 있는 여러 동시 스레드 간의 경쟁 상태를 방지합니다. 공유 GIL이 없다면 실행 중인 두 인터프리터는 None처럼 그 밖에는 변경 불가능한 객체조차 안전하게 공유할 수 없습니다.

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

대안은 모든 공유 객체가 실제로 변경 불가능하도록 보장하는 것입니다. 수정이 없으므로 경쟁 상태도 발생하지 않습니다. 여기서 제안하는 불멸성이 다른 경우에는 변경 불가능한 객체에 대해 가능하게 하는 것이 바로 이것입니다. 불멸 객체를 사용하면 인터프리터별 GIL 지원이 훨씬 간단해집니다.

Copy-on-Write 회피

일부 애플리케이션에서는 애플리케이션을 원하는 초기 상태로 만든 다음 각 작업자를 위해 프로세스를 fork하는 것이 합리적입니다. 이는 특히 메모리 사용량에서 상당한 성능 향상으로 이어질 수 있습니다. 여러 엔터프라이즈 Python 사용자가(예: Instagram, YouTube) 이를 활용해 왔습니다. 그러나 위의 참조 횟수 의미론은 이점을 크게 줄이며 일부 최적이 아닌 우회책을 초래했습니다.

또한 “fork”가 카피 온 라이트 의미 체계를 사용하는 유일한 운영 체제 메커니즘은 아니라는 점에 유의하십시오. 또 다른 예로는 mmap이 있습니다. 이러한 유틸리티는 “불멸” 객체만 사용하는 경우와 비교하여, 불멸 객체가 관련될 때 카피 온 라이트가 줄어드는 이점을 잠재적으로 얻을 수 있습니다.

근거

제안된 해결책은 매우 명확하여 이 제안의 두 저자가 모두 서로 독립적으로(대체로 동일한 구현과 함께) 같은 결론에 도달했습니다. Pyston 프로젝트는 유사한 접근 방식을 사용합니다. 다른 설계도 고려되었습니다. 지난 몇 년 동안 python-dev에서 여러 가능성이 논의되기도 했습니다.

대안은 다음과 같습니다.

  • 높은 비트를 사용하여 “불멸”을 표시하지만 Py_INCREF()는 변경하지 않습니다.
  • 객체에 명시적인 플래그를 추가합니다.
  • 타입을 통해 구현합니다(tp_dealloc()은 아무 작업도 하지 않습니다).
  • 객체의 타입 객체를 통해 추적합니다.
  • 별도의 테이블로 추적합니다.

위의 방법은 각각 객체를 불멸로 만들지만, 앞서 설명한 참조 횟수 수정으로 인한 성능 저하를 해결하는 방법은 없습니다.

인터프리터별 GIL의 경우 유일하게 현실적인 대안은 모든 전역 객체를 PyInterpreterState로 옮기고, 해당 객체에 액세스하기 위한 하나 이상의 조회 함수를 추가하는 것입니다. 그런 다음 거기에 노출된 많은 객체의 호환성을 유지하기 위해 C-API에 몇 가지 우회 코드를 추가해야 합니다. 불멸 객체를 사용하면 상황이 훨씬, 훨씬 간단해집니다.

영향

이점

특히 위 예시에 설명된 경우는 불멸 객체를 통해 큰 혜택을 얻을 수 있습니다. pre-fork를 사용하는 프로젝트는 자체 우회책을 제거할 수 있습니다. 인터프리터별 GIL 프로젝트에서는 불멸 객체가 기존 정적 타입과 공개 C-API가 노출하는 객체에 대한 해결책을 크게 단순화합니다.

일반적으로 객체에 강력한 불변성 보장을 제공하면 Python 애플리케이션의 확장성이 향상되며, 특히 multi-process deployments에서 그렇습니다. 이는 애플리케이션이 현재 감수해야 하는 것만큼 큰 메모리 사용량 절충 없이 멀티코어 병렬 처리를 활용할 수 있기 때문입니다. 방금 설명한 경우와 위의 Motivation에서 설명한 경우는 이러한 개선을 보여 줍니다.

성능

순진한 구현에서는 a 2% slowdown이 나타납니다(MSVC에서는 3%입니다). 몇 가지 기본 완화책을 적용하면 ~performance-neutral~ 상태로 돌아갈 수 있음을 입증했습니다. 아래의 mitigations 섹션을 참조하십시오.

긍정적인 측면에서 불멸 객체는 with a pre-fork model에서 사용할 때 상당한 메모리를 절약합니다. 또한 불멸 객체는 평가 루프에서 성능을 향상할 수 있는 특수화 기회를 제공합니다.

하위 호환성

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

구체적으로 불멸 객체가 관련된 경우에는 다음과 같습니다.

  • 참조 카운트를 검사하는 코드는 매우, 매우 큰 값을 확인하게 됩니다.
  • 새로운 noop 동작으로 인해 다음과 같은 코드가 중단될 수 있습니다.
    • 참조 카운트가 항상 증가하거나 감소한다고 명시적으로 의존하는 코드(또는 Py_SET_REFCNT()에서 특정 값을 갖는다고 의존하는 코드)
    • 0이나 1이 아닌 특정 참조 카운트 값에 의존하는 코드
    • 추가 정보를 저장하기 위해 참조 카운트를 직접 조작하는 코드
  • 32비트 pre-3.12 Stable ABI 확장 기능에서는 Accidental Immortality로 인해 객체가 누수될 수 있습니다.
  • 이러한 확장 기능은 Accidental De-Immortalizing으로 인해 충돌할 수 있습니다.

다시 말해 이러한 동작 변경은 불멸 객체에만 적용되며, 사용자가 사용할 객체의 대다수에는 적용되지 않습니다. 더구나 사용자는 객체를 불멸 객체로 표시할 수 없으므로, 사용자가 생성한 객체에는 이러한 변경된 동작이 절대 적용되지 않습니다. 전역(내장) 객체에 대해 변경되는 동작에 의존하는 사용자는 이미 곤경에 처해 있습니다. 따라서 전체적인 영향은 작을 것입니다.

또한 참조 누수를 검사하는 코드는 일부 불멸 객체를 기준으로 하드코딩된 작은 값을 검사하지 않는 한 계속 정상적으로 작동해야 합니다. 참조 카운트를 수정하지 않으므로 Pyston에서 발견된 문제는 여기에는 적용되지 않아야 합니다.

추가 논의는 아래의 Public Refcount Details를 참조하십시오.

우발적 불멸화

가정적으로, 불멸 객체가 아닌 객체에 incref가 너무 많이 수행되어 불멸 객체로 간주되는 데 필요한 마법 값에 도달할 수 있습니다. 이는 해당 객체가 다시 0까지 decref되지 않으므로, 우발적으로 누수되어 정리되지 않는다는 의미입니다.

64비트 참조 횟수에서는 이러한 우발적 상황이 발생할 가능성이 매우 낮으므로 걱정할 필요가 없습니다. Py_INCREF()를 긴밀한 루프에서 사용하고 각 반복이 CPU 사이클 1개만 소요되도록 의도적으로 수행하더라도, 2^60 사이클이 필요합니다(불멸 비트가 2^60인 경우). 빠른 5GHz에서도 이는 거의 250,000,000초(2,500일 초과)가 걸립니다!

또한 참조 횟수가 다시 0이 되어 객체가 정리될 때까지는 문제가 되지 않으므로, 문제가 될 가능성이 두 배로 낮다는 점에 유의하십시오. 따라서 마법의 “불멸” 참조 횟수 값에 도달한 객체는 동작 변경이 감지되기 전에 그만큼 여러 번 다시 decref되어야 합니다.

다시 말해, 마법의 참조 횟수에 도달한 다음 되돌리는 현실적인 유일한 방법은 이를 의도적으로 수행하는 것입니다. (물론 Py_SET_REFCNT()를 사용하면 같은 작업을 효율적으로 수행할 수 있지만, 이는 우발적으로 발생할 가능성이 더욱 낮습니다.) 이 시점에서는 이를 이 제안의 우려 사항으로 간주하지 않습니다.

32비트 플랫폼처럼 최대 참조 횟수가 훨씬 작은 빌드에서는 그 결과가 그리 명확하지 않습니다. 마법의 참조 횟수가 2^30이라고 가정하십시오. 위와 동일한 사양을 사용하면 객체를 우발적으로 불멸화하는 데 대략 4초가 걸립니다. 합리적인 조건에서는 객체가 우발적으로 불멸화될 가능성이 여전히 매우 낮습니다. 다음 기준을 충족해야 합니다:

  • 불멸 객체가 아닌 객체를 대상으로 할 것(따라서 사용 빈도가 높은 기본 제공 객체 중 하나가 아닐 것)
  • 확장 모듈이 이에 대응하는 decref 없이 incref할 것(예: 함수 또는 메서드에서 반환할 때)
  • 그동안 다른 코드가 객체를 decref하지 않을 것

훨씬 낮은 빈도로 수행하더라도 32비트에서는 우발적 불멸성에 도달하는 데 오래 걸리지 않습니다. 그러나 그 경우 해당 객체 하나가 사실상 누수되기 전에 동일한 횟수의 (이제 아무 작업도 하지 않는) decref를 다시 수행해야 합니다. 특히 계산이 decref가 전혀 없다고 가정하므로, 이는 가능성이 매우 낮습니다.

게다가 이는 이러한 32비트 확장 모듈이 이미 객체를 2^31 이상 incref하여 참조 횟수를 음수로 바꾸는 방식과 크게 다르지 않습니다. 그것이 실제 문제였다면 우리는 이미 그에 관한 이야기를 들었을 것입니다.

위의 모든 경우를 종합하면, 이 제안에서는 우발적 불멸성을 문제로 간주하지 않습니다.

안정 ABI

이 PEP에서 설명하는 구현 방식은 안정 ABI용으로 컴파일된 확장 모듈과 호환됩니다(Accidental ImmortalityAccidental De-Immortalizing은 제외합니다). 안정 ABI의 특성상 안타깝게도 이러한 확장 모듈은 객체의 ob_refcnt 필드를 직접 수정하는 Py_INCREF(), 기타 버전들을 사용합니다. 이로 인해 불멸 객체의 성능상 이점이 모두 무효화됩니다.

그러나 이러한 상황에서도 불멸 객체가 (대체로) 불멸 상태를 유지하도록 보장합니다. 불멸 객체의 초기 참조 횟수를 객체를 불멸 객체로 식별할 수 있고, 확장 모듈이 참조 횟수를 수정하더라도 계속 그렇게 식별할 수 있는 값으로 설정합니다. (예를 들어 높은 참조 횟수 비트 중 하나를 객체가 불멸임을 나타내는 데 사용한다고 가정하십시오. 다음 비트까지의 중간값처럼 해당 비트와 여전히 일치하는 더 높은 값으로 초기 참조 횟수를 설정합니다. _Py_IMMORTAL_REFCNT를 참조하십시오.) 최악의 경우에도 그러한 상황의 객체는 Motivation 섹션에서 설명한 영향을 받게 됩니다. 그렇더라도 전반적인 영향이 상당할 가능성은 낮습니다.

우발적 불멸성 해제

32비트 빌드의 이전 안정 ABI 확장은 Accidental Immortality를 한 단계 더 발전시킬 수 있습니다.

가정적으로, 이러한 확장은 객체의 참조 횟수를 매직 참조 횟수 값보다 높은 다음 비트의 값까지 incref할 수 있습니다. 예를 들어 매직 값이 2^30이고 초기 불멸 참조 횟수가 따라서 2^30 + 2^29라면, 확장이 2^29번 incref해야 2^31 값에 도달하여 객체가 불멸 상태가 아니게 됩니다. (물론 이렇게 높은 참조 횟수라면 불멸 객체와 관계없이 이미 충돌을 일으킬 가능성이 높습니다.)

더 문제가 되는 경우는 이러한 32비트 안정 ABI 확장이 이미 불멸 상태인 객체를 미친 듯이 decref하는 경우입니다. 위의 예를 계속 적용하면, 매직 불멸 참조 횟수 값 아래로 떨어뜨리려면 2^29번의 비대칭 decref가 필요합니다. 따라서 None과 같은 객체가 유한 수명 상태가 되어 decref의 대상이 될 수 있습니다. 그래도 어떻게든 해당 객체에 대한 decref가 0에 도달할 때까지 계속되지 않는 한 문제는 발생하지 않습니다. None과 같이 정적으로 할당된 불멸 객체의 경우, 확장이 객체를 dealloc하려고 시도하면 프로세스가 충돌합니다. 그 밖의 불멸 객체에서는 dealloc이 정상적으로 수행될 수도 있습니다. 그러나 이전에는 불멸 상태였던 객체가 영원히 존재할 것으로 예상하는 런타임 코드가 있을 수 있습니다. 그러한 코드는 아마 충돌할 것입니다.

다시 말해, 32비트 빌드에서도 이러한 일이 발생할 가능성은 극히 낮습니다. 해당 객체 하나에 대응하는 incref 없이 대략 10억 번의 decref가 필요합니다. 가장 가능성이 높은 시나리오는 다음과 같습니다.

많은 함수와 메서드가 None에 대한 “새로운” 참조를 반환합니다. 3.12 런타임은 불멸 객체가 아닌 객체의 경우와 달리, None을 확장에 전달하기 전에 기본적으로 절대 incref하지 않습니다. 그러나 확장은 작업을 마치면(반환하는 경우를 제외하고) 반드시 decref합니다. 해당 객체 하나에 대해 이러한 교환이 발생할 때마다 충돌에 한 단계씩 가까워집니다.

32비트 환경에서 Python 프로세스의 수명 동안 이러한 교환이 어떤 형태로든(단일 객체에 대해) 10억 번 발생할 가능성이 얼마나 현실적입니까? 이것이 문제라면 어떻게 해결할 수 있습니까?

얼마나 현실적인지에 대해서는 현재 답이 명확하지 않습니다. 그러나 완화책은 충분히 간단하므로 문제가 되지 않을 것이라는 가정하에 안전하게 진행할 수 있습니다.

가능한 해결책은 나중에살펴봅니다.

대체 Python 구현

이 제안은 CPython에만 해당합니다. 그러나 이 제안은 C-API의 동작과 관련이 있으며, 이는 다른 Python 구현에 영향을 줄 수 있습니다. 따라서 위의 하위 호환성에 설명된 변경된 동작의 영향도 여기 적용됩니다(예를 들어 다른 구현이 0이 아닌 특정 참조 횟수 값이나 참조 횟수가 정확히 어떻게 변경되는지에 밀접하게 결합되어 있다면 영향을 받을 수 있습니다).

보안 영향

이 기능이 보안에 미치는 영향은 알려진 바가 없습니다.

유지 관리성

이는 복잡한 기능이 아니므로 유지보수 담당자에게 큰 정신적 부담을 주지 않아야 합니다. 기본 구현은 많은 코드를 건드리지 않으므로 유지보수성에 큰 영향을 미칠 것입니다. 성능 저하를 완화하기 위해 약간의 복잡성이 추가될 수 있습니다. 그러나 이는 초기화 후 모든 객체를 불멸화하고 나중에 런타임 종료 과정에서 명시적으로 할당 해제하는 부분으로 제한되어야 합니다. 이를 위한 코드는 비교적 집중되어 있을 것입니다.

사양

이 접근 방식에는 다음과 같은 근본적인 변경 사항이 포함됩니다.

  • 내부 C-API에 _Py_IMMORTAL_REFCNT (매직 값)을 추가합니다.
  • 매직 참조 횟수와 일치하는 객체에 대해 Py_INCREF()Py_DECREF()를 no-op이 되도록 업데이트합니다.
  • 참조 횟수를 수정하는 다른 모든 API에도 동일하게 적용합니다.
  • 불멸 GC 객체(“컨테이너”)에 대해서는 PyGC_Head를 더 이상 수정하지 않습니다.
  • 런타임 종료 과정에서 모든 불멸 객체가 정리되도록 보장합니다.

그러면 어떤 객체의 참조 횟수든 _Py_IMMORTAL_REFCNT로 설정하면 해당 객체가 불멸 객체가 됩니다.

(여기에서 설명하지 않은 기타 사소한 내부 변경 사항도 있습니다.)

다음 하위 절에서는 가장 중요한 세부 사항을 자세히 살펴봅니다. 먼저 몇 가지 개념적 주제를 다룬 다음, 영향을 받는 특정 API와 같은 보다 구체적인 측면을 살펴봅니다.

공개 참조 횟수 세부 사항

Backward Compatibility 에서는 이 제안의 변경으로 인해 사용자 코드가 손상될 수 있는 방법을 소개했습니다. 사용자가 겪는 오해의 원인은 상당 부분 참조 횟수 관련 API의 이름과 문서에서 해당 API(및 일반적인 참조 횟수 계산)를 설명하는 방식에 있을 가능성이 높습니다.

이름과 문서를 함께 살펴보면 다음 질문에 대한 답을 명확히 확인할 수 있습니다.

  • 사용자는 어떤 동작을 기대합니까?
  • 우리는 어떤 보장을 제공합니까?
  • 사용자가 받은 참조 횟수 값을 어떻게 해석해야 하는지 나타냅니까?
  • 사용자가 객체의 참조 횟수를 특정 값으로 설정하는 사용 사례는 무엇입니까?
  • 사용자가 자신이 생성하지 않은 객체의 참조 횟수를 설정합니까?

이 제안의 일부로서 사용자가 참조 횟수 동작의 어느 부분에 의존할 수 있고 어느 부분이 구현 세부 사항으로 간주되는지 명확히 이해할 수 있도록 해야 합니다. 구체적으로 사용자는 기존의 공개 참조 횟수 관련 API를 사용해야 하며, 의미가 있는 참조 횟수 값은 0과 1뿐입니다. (일부 코드는 객체를 안전하게 수정할 수 있다는 표시로 1에 의존합니다.) 그 밖의 모든 값은 “0 또는 1이 아님”으로 간주됩니다.

이 정보는 documentation 에서 명확히 설명합니다.

사용자에게 기대하도록 하려는 내용을 반영하도록 기존의 참조 횟수 관련 API를 수정해야 한다고 주장할 수도 있습니다. 다음과 같은 형태입니다.

  • Py_INCREF() -> Py_ACQUIRE_REF() (또는 Py_NewRef()만 지원)
  • Py_DECREF() -> Py_RELEASE_REF()
  • Py_REFCNT() -> Py_HAS_REFS()
  • Py_SET_REFCNT() -> Py_RESET_REFS()Py_SET_NO_REFS()

그러나 이러한 변경은 이 제안의 일부가 아닙니다. 이 변경으로부터 혜택을 받을 사용자 기대에 더욱 집중할 수 있음을 보여 주기 위해 여기에 포함합니다.

제약 조건

  • 다른 면에서 불변인 객체가 진정으로 불변이 되도록 보장합니다.
  • 일반적인 Python 사용 사례에서 성능 저하를 최소화합니다.
  • 런타임 종료 시점까지 실제로 유지될 것으로 예상하지 않는 객체를 불멸화할 때 주의합니다.
  • 다른 면에서 불변이 아닌 객체를 불멸화할 때 주의합니다.
  • __del__ 및 weakref가 계속 올바르게 작동해야 합니다.

“진정으로” 불변인 객체와 관련하여 이 PEP는 객체별 런타임 상태(예: 참조 횟수)를 제외한 어떠한 객체의 실질적인 불변성에도 영향을 미치지 않습니다. 따라서 어떤 불멸 객체가 진정으로(또는 실질적으로라도) 불변인지 여부는 이 제안과 별도로 결정해야 합니다. 예를 들어 str 객체는 일반적으로 불변으로 간주되지만, PyUnicodeObject는 일부 데이터를 지연하여 캐시합니다. 이 PEP는 해당 상태가 str 불변성에 영향을 미치는 방식에 영향을 주지 않습니다.

불멸 가변 객체

모든 객체를 불멸 객체로 표시할 수 있습니다. 어떠한 제한이나 검사도 제안하지 않습니다. 그러나 실제로 객체를 불멸 객체로 만드는 것의 가치는 해당 객체의 가변성과 관련되며, 애플리케이션 수명의 충분한 기간 동안 사용될 가능성에 따라 달라집니다. 일부 상황에서는 가변 객체를 불멸 객체로 표시하는 것이 타당할 수 있습니다.

불멸 객체의 많은 사용 사례는 불변성을 중심으로 하므로, 스레드가 잠금 없이 그러한 객체를 안전하고 효율적으로 공유할 수 있습니다. 이러한 이유로 딕셔너리나 리스트와 같은 가변 객체는 결코 공유되지 않습니다(따라서 불멸성도 없습니다). 그러나 일반적으로 가변인 객체가 실제로 수정되지 않을 것이라는 충분한 보장이 있다면 불멸성이 적절할 수 있습니다.

반면 일부 가변 객체는 스레드 간에 결코 공유되지 않습니다(적어도 전역 인터프리터 잠금과 같은 잠금 없이는 공유되지 않습니다). 경우에 따라 그러한 객체 중 일부도 불멸 객체로 만드는 것이 실용적일 수 있습니다. 예를 들어, sys.modules는 해당 인터프리터가 종료될 때까지 해제되지 않을 것으로 예상하는 인터프리터별 딕셔너리입니다(대체되지 않는다고 가정할 경우). 이를 불멸 객체로 만들면 incref/decref 중에 발생하는 추가 오버헤드를 더 이상 부담하지 않게 됩니다.

아래의 mitigations 섹션에서 이 아이디어를 더 자세히 살펴봅니다.

암묵적으로 불멸인 객체

불멸 객체가 일반 객체(유한 수명 객체)에 대한 참조를 보유하면 해당 객체도 실질적으로 불멸입니다. 이는 불멸 객체가 해당 객체를 해제할 때까지 해당 객체의 참조 횟수가 0에 도달할 수 없기 때문입니다.

예:

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

따라서 이러한 보유된 객체는 보유되는 동안 암묵적으로 불멸 객체가 됩니다. 실제로 이는 동작의 변경이 아니므로 실질적인 결과를 초래하지 않아야 합니다. 유일한 차이는 참조를 보유하는 불멸 객체가 정리되지 않는다는 점입니다.

이러한 암묵적으로 불멸인 객체를 어떤 방식으로든 변경하자는 제안은 아닙니다. 불멸 객체가 보유하고 있다는 이유만으로 해당 객체를 명시적으로 불멸 객체로 표시해서는 안 됩니다. 그렇게 해도 아무것도 하지 않는 것보다 나을 점이 없습니다.

객체의 불멸성 해제

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

게다가 명백한 접근 방식은 참조 횟수를 단순히 작은 값으로 설정하는 것입니다. 그러나 그 시점에는 어떤 값이 안전할지 알 방법이 없습니다. 이상적으로는 해당 객체가 불멸 객체로 만들어지지 않았더라면 가졌을 값으로 설정해야 합니다. 그러나 그 값은 이미 오래전에 사라졌을 것입니다. 따라서 관련된 복잡성 때문에, 그렇게 해야 할 타당한 이유가 있더라도 객체의 불멸성을 안전하게 해제할 가능성은 낮아집니다.

_Py_IMMORTAL_REFCNT

내부 상수 두 개를 추가합니다.:

_Py_IMMORTAL_BIT - has the top-most available bit set (e.g. 2^62)
_Py_IMMORTAL_REFCNT - has the two top-most available bits set

실제 최상위 비트는 참조 횟수 비트의 기존 사용 방식, 예를 들어 부호 비트나 일부 GC 사용 방식에 따라 달라집니다. 기존 사용 방식을 검토한 후 가능한 가장 높은 비트를 사용합니다.

불멸 객체의 참조 횟수는 _Py_IMMORTAL_REFCNT로 설정합니다 (즉, 값은 _Py_IMMORTAL_BIT와 그보다 한 단계 높은 비트의 값 사이의 중간이 됩니다). 그러나 객체가 불멸인지 확인할 때는 해당 객체의 참조 횟수와 _Py_IMMORTAL_BIT만 비트 AND 연산하여 비교합니다.

이 차이로 인해 어떤 방식으로든 참조 횟수가 수정되더라도(예: 이전 안정 ABI 확장에 의해) 불멸 객체는 계속 불멸 객체로 간주됩니다.

참조 횟수의 최상위 두 비트는 이미 다른 용도로 예약되어 있다는 점에 유의하십시오. 이것이 최상위에서 세 번째 비트를 사용하는 이유입니다.

구현에서는 부호 비트나 2^31(64비트에서 포화된 참조 횟수에 사용)과 같이 불멸 비트에 다른 값을 사용하는 것도 허용합니다.

영향을 받는 API

이제 불멸 객체를 무시하는 API:

  • (public) Py_INCREF()
  • (public) Py_DECREF()
  • (public) Py_SET_REFCNT()
  • (private) _Py_NewReference()

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

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

(_Py_RefTotal 및 그에 따른 sys.gettotalrefcount()는 영향을 받지 않는다는 점에 유의하십시오.)

TODO: _Py_RefTotal의 상태를 명확히 하십시오.

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

불멸 전역 객체

런타임 전역(내장) 객체는 모두 불멸 객체로 만듭니다. 여기에는 다음이 포함됩니다.

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

완전한 객체를 실제로 불변으로 만들지 여부(예: 인터프리터별 GIL을 위해)는 이 PEP의 범위에 포함되지 않습니다.

객체 정리

런타임 종료 중 모든 불멸 객체를 정리하려면 해당 객체를 추적해야 합니다.

GC 객체(“컨테이너”)의 경우, 모든 불멸화된 컨테이너를 GC의 영구 세대로 이동하여 영구 세대를 활용합니다. 런타임 종료 중에는 먼저 런타임이 이러한 인스턴스를 정상적으로 할당 해제하기 위해 최선을 다하도록 하는 전략을 사용합니다. 이제 대부분의 모듈 할당 해제는 pylifecycle.c:finalize_modules()에서 처리되며, 여기서 남은 모듈을 가능한 한 최선을 다해 정리합니다. 이로 인해 __del__중에 사용할 수 있는 모듈이 달라지지만, 이는 이미 문서에서 명시적으로 정의되지 않은 동작입니다. 선택적으로 사용자 모듈이 표준 라이브러리 모듈보다 먼저 할당 해제되도록 보장하기 위해 위상 정렬을 수행할 수도 있습니다. 마지막으로 남은 항목이 있다면 영구 세대 GC 목록을 통해 찾을 수 있으며, finalize_modules()가 완료된 후 해당 목록을 지울 수 있습니다.

컨테이너가 아닌 객체의 경우 추적 방식은 사례별로 달라집니다. 거의 모든 경우 이러한 객체는 런타임 상태에서 직접 액세스할 수 있습니다. 예를 들어 _PyRuntimeState 또는 PyInterpreterState 필드에 있습니다. 소수의 객체에는 런타임 상태에 추적 메커니즘을 추가해야 할 수도 있습니다.

이러한 정리 작업은 성능에 유의미한 영향을 미치지 않습니다.

성능 회귀 완화

명확히 하기 위해, 순진한 불멸 객체 구현으로 잃게 되는 4% performance성능의 일부를 회복하기 위해 시도할 방법을 몇 가지 제시합니다.

이 절의 내용은 실제로 제안의 일부가 아니라는 점에 유의하십시오.

런타임 초기화가 끝날 때 모든 객체를 불멸로 표시합니다.

순진한 불멸 객체 구현으로 잃게 되는 4% 성능의 일부를 회복하기 위해 Immortal Mutable Objects의 개념을 적용할 수 있습니다. 런타임 초기화가 끝날 때 all모든 객체를 불멸로 표시하여 incref/decref의 추가 비용을 피할 수 있습니다. GIL 없이 스레드 간에 공유할 계획인 객체의 불변성만 고려하면 됩니다.

불필요하게 하드코딩된 참조 카운트 연산 제거

C-API의 일부는 Py_RETURN_NONE과 같이 불멸 객체임을 알고 있는 객체와 특별히 상호 작용합니다. 이러한 함수와 매크로는 모든 참조 카운트 연산을 제거하도록 업데이트할 수 있습니다.

평가 루프에서 불멸 객체에 특화

None과 같은 특정 불멸 객체가 알려진 경우, 평가 루프에서 해당 객체와 관련된 연산을 최적화할 기회가 있습니다. 일반적인 메커니즘은 PEP 659에 설명되어 있습니다. Pyston도 참조하십시오.

기타 가능성

  • 모든 인터닝된 문자열을 불멸로 표시
  • 공유되는 경우 “interned” 딕셔너리를 불멸로 표시하고, 그렇지 않으면 인터닝된 모든 문자열을 공유하십시오.
  • (Larry,MAL) 모듈에 대해 역직렬화된 모든 상수를 불멸로 표시
  • (Larry,MAL) (불변) 불멸 객체를 자체 메모리 페이지에 할당
  • 최하위 32비트를 사용한 포화 참조 카운트

우발적인 불멸성 해제에 대한 해결책

Accidental De-Immortalizing 섹션에서는 불멸 객체의 가능한 부정적 결과를 설명했습니다. 여기에서는 이를 처리할 몇 가지 방안을 살펴봅니다.

문제의 해결 방법을 지시하기보다는 만족스러운 방안을 사용할 수 있음을 보여 주기 위해 여기에서 해결책을 열거한다는 점에 유의하십시오.

다음 사항에도 유의하십시오.

  • 이는 32비트 안정 ABI인 경우에만 중요합니다.
  • 이는 불멸 객체에만 영향을 줍니다.
  • 사용자 정의 불멸 객체는 없으며, 내장 타입만 있습니다.
  • 대부분의 불멸 객체는 정적으로 할당되므로 (따라서 tp_dealloc()이 호출되면 이미 실패해야 하므로)
  • 실제로 이 문제에 직면할 가능성이 있을 정도로 자주 사용되는 불멸 객체는 소수에 불과합니다(예: None).
  • 해결해야 할 주요 문제는 tp_dealloc()에서 발생하는 충돌입니다.

해결책을 위한 한 가지 근본적인 관찰은 어떤 조건이 충족되면 불멸 객체의 참조 카운트를 _Py_IMMORTAL_REFCNT로 재설정할 수 있다는 점입니다.

이 모든 점을 고려할 때 간단하면서도 효과적인 해결책은 tp_dealloc()에서 불멸 객체의 참조 카운트를 재설정하는 것입니다. NoneTypebool에는 이미 트리거될 경우 Py_FatalError()를 호출하는 tp_dealloc()이 있습니다. PyUnicodeObject (unicode_is_singleton()에 따라), PyTupleObject, PyTypeObject와 같이 특정 조건에 따른 다른 타입도 마찬가지입니다. 실제로 동일한 검사는 정적으로 선언된 모든 객체에 중요합니다. 이러한 타입에서는 대신 참조 카운트를 재설정합니다. 나머지 경우에는 해당 검사를 도입합니다. 모든 경우에 tp_dealloc()에서 검사를 수행하는 오버헤드는 무시해도 될 정도로 작아야 합니다.

기타 (실용성이 낮은) 해결책:

  • 불멸 객체의 참조 횟수를 주기적으로 재설정합니다.
  • 사용 빈도가 높은 객체에 대해서만 그렇게 합니다.
  • 안정 ABI 확장이 임포트된 경우에만 그렇게 합니다.
  • 불멸성을 비활성화하는 런타임 플래그를 제공합니다.

(The discussion thread에는 추가 세부 정보가 있습니다.)

최종적으로 어떤 해결책을 선택하든, 필요하다면 나중에 다른 작업을 수행할 수 있습니다.

TODO: 구현된 해결책이 전반적인 ~performance-neutral~ 결과에 영향을 주지 않는다는 내용의 주석을 추가합니다.

문서화

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

그러나 참조 횟수 동작에 대한 공개 보장을 더 명확하게 설명하도록 문서를 업데이트합니다. 구체적으로는 다음을 포함합니다.

  • Py_INCREF() - “객체 o의 참조 횟수를 증가시킵니다.”를 “객체 o에 대한 새 참조를 취했음을 나타냅니다.”로 변경합니다.
  • Py_DECREF() - “객체 o의 참조 횟수를 감소시킵니다.”를 “이전에 취한 객체 o에 대한 참조를 더 이상 사용하지 않음을 나타냅니다.”로 변경합니다.
  • Py_XINCREF(), Py_XDECREF(), Py_NewRef(), Py_XNewRef(), Py_Clear()에도 유사하게 적용합니다.
  • Py_REFCNT() - “참조 횟수 0과 1에는 특정한 의미가 있으며, 나머지 값은 그 값에 관계없이 코드 어딘가에서 객체를 사용하고 있다는 의미일 뿐입니다. 0은 객체가 사용되지 않으며 정리된다는 의미입니다. 1은 코드가 정확히 하나의 참조를 보유한다는 의미입니다.”를 추가합니다.
  • Py_SET_REFCNT() - 1보다 큰 값이 다른 더 큰 값으로 대체될 수 있다는 내용은 Py_REFCNT()를 참조하도록 합니다.

변경으로 인해 사용자가 느낄 수 있는 놀라움을 줄이는 데 도움이 되도록, 다음 항목에 불멸 객체에 관한 메모를 추가할 수도 있습니다:

  • Py_SET_REFCNT() (불멸 객체에 대해서는 아무 작업도 하지 않습니다.)
  • Py_REFCNT() (값이 예상보다 훨씬 클 수 있습니다.)
  • sys.getrefcount() (값이 예상보다 훨씬 클 수 있습니다.)

이러한 주석의 혜택을 받을 수 있는 다른 API는 현재 문서화되어 있지 않습니다. 이 기능은 그 밖에는 사용자에게 투명하므로, Py_INCREF()Py_DECREF()를 포함하여 다른 곳에는 이러한 주석을 추가하지 않습니다.

참조 구현

구현은 GitHub에서 제안되었습니다.

https://github.com/python/cpython/pull/19474

미해결 문제

참고 자료

선행 사례

논의

이 내용은 2021년 12월 python-dev에서 논의되었습니다:

런타임 객체 상태

각 Python 객체에 대해 CPython 런타임이 유지하는 내부 상태는 다음과 같습니다.

ob_refcnt은 모든 객체에 할당되는 메모리의 일부입니다. 그러나 CPython이 Py_TRACE_REFS가 정의된 상태로 빌드된 경우에만 _PyObject_HEAD_EXTRA가 할당됩니다. 객체의 타입에 Py_TPFLAGS_HAVE_GC가 설정된 경우에만 PyGC_Head가 할당됩니다. 일반적으로 이는 컨테이너 타입(예: list)에만 해당합니다. 또한 PyObject.ob_refcnt_PyObject_HEAD_EXTRAPyObject_HEAD의 일부라는 점에 유의하십시오.

순환 가비지 컬렉션을 사용하는 참조 카운팅

가비지 컬렉션은 일부 프로그래밍 언어의 메모리 관리 기능입니다. 이는 객체가 더 이상 사용되지 않으면 정리된다는 의미입니다(예: 메모리가 해제됩니다).

참조 카운팅은 가비지 컬렉션의 한 가지 접근 방식입니다. 언어 런타임은 객체에 유지되는 참조의 수를 추적합니다. 코드가 객체에 대한 참조의 소유권을 취하거나 이를 해제하면 런타임에 알림이 전달되고, 런타임은 그에 따라 참조 카운트를 증가시키거나 감소시킵니다. 참조 카운트가 0에 도달하면 런타임이 객체를 정리합니다.

CPython에서는 C-API의 Py_INCREF()Py_DECREF()를 사용하여 코드가 참조를 명시적으로 취하거나 해제해야 합니다. 이러한 매크로는 객체의 참조 카운트를 직접 수정합니다(가비지 컬렉션 방식을 변경하려는 경우 ABI 호환성 문제가 발생하기 때문에 유감스러운 일입니다). 또한 CPython에서 객체가 정리될 때 객체는 자신이 소유한 모든 참조(및 리소스)도 해제합니다(메모리가 해제되기 전에).

때때로 객체가 참조 순환에 관여할 수 있습니다. 예를 들어 객체 A가 객체 B에 대한 참조를 보유하고 객체 B가 객체 A에 대한 참조를 보유하는 경우입니다. 결과적으로 다른 참조가 보유되지 않더라도 두 객체 모두 절대 정리되지 않습니다(즉, 메모리 누수가 발생합니다). 순환에 관여하는 가장 일반적인 객체는 컨테이너입니다.

CPython에는 참조 순환을 처리하기 위한 전용 메커니즘이 있으며, 이를 “순환 가비지 컬렉터” 또는 흔히 “가비지 컬렉터”나 “GC”라고 부릅니다. 이름 때문에 혼동하지 마십시오. 이는 참조 순환을 끊는 일만 처리합니다.

참조 카운팅과 순환 가비지 컬렉션에 대한 더 자세한 설명은 다음 문서를 참조하십시오.