Rawrefcount와 GC

GC 인터페이스

“PyObject”는 최소한 ob_refcnt와 ob_pypy_link라는 두 개의 필드를 가진 원시(raw) 구조체입니다. ob_refcnt는 CPython에서 사용하는 것과 같은 참조 카운터입니다. PyObject 구조체가 살아 있는 PyPy 객체에 연결되어 있으면, 그 객체의 현재 주소가 ob_pypy_link에 저장되고 ob_refcnt는 상수 REFCNT_FROM_PYPY 또는 상수 REFCNT_FROM_PYPY_LIGHT (== REFCNT_FROM_PYPY + SOME_HUGE_VALUE)만큼 증가합니다(후자는 “가벼운 파이널라이저(light finalizer)”를 뜻합니다).

대부분의 PyPy 객체는 cpyext 바깥에 존재하며, 반대로 cpyext 안에서는 많은 PyObject가 PyPy의 나머지 부분에 드러나지 않은 채 존재할 수 있습니다. 그러나 인터페이스에서는 PyPy 객체와 PyObject를 “연결”할 수 있습니다. 연결에는 두 가지 종류가 있습니다:

rawrefcount.create_link_pypy(p, ob)

기존 객체 gcref ‘p’와 새로 할당된 PyObject 구조체 ‘ob’를 연결합니다. ob->ob_refcnt는 REFCNT_FROM_PYPY 또는 REFCNT_FROM_PYPY_LIGHT 중 하나로 초기화해야 합니다. (두 번째 경우는 최적화를 위한 것입니다. GC가 PyPy 객체와 PyObject가 더 이상 참조되지 않는다는 것을 발견하면 PyObject를 그냥 free()하면 됩니다.)

rawrefcount.create_link_pyobj(p, ob)

기존 PyObject 구조체 ‘ob’에서 새로 할당된 W_CPyExtPlaceHolderObject ‘p’로 가는 링크를 만듭니다. 또한 ob->ob_refcnt에 REFCNT_FROM_PYPY를 더해야 합니다. PyObject가 모든 데이터를 담고 있고 PyPy 객체는 프록시에 불과한 경우를 위한 것입니다. W_CPyExtPlaceHolderObject에는 PyObject의 주소를 담는 필드 하나만 있어야 하지만, 이는 GC의 범위를 벗어나는 일입니다.

rawrefcount.from_obj(p)

create_link_pypy()로 만든 객체 ‘p’로부터의 링크가 있으면 해당하는 ‘ob’를 반환합니다. 그렇지 않으면 NULL을 반환합니다.

rawrefcount.to_obj(Class, ob)

ob->ob_pypy_link를 ‘Class’의 인스턴스로 캐스팅하여 반환합니다.

수집 로직

순수하게 C 쪽에만 존재하는 객체는 ob->ob_pypy_link == 0이며, 이러한 객체는 순전히 참조 카운트로만 관리됩니다. 반면에 ob->ob_pypy_link != 0이면 ob->ob_refcnt는 최소한 REFCNT_FROM_PYPY이며, 해당 객체는 “링크”의 일부입니다.

핵심은 ‘p’가 다른 PyPy 객체에서 도달할 수 없고 동시에 ‘ob->ob_refcnt’가 REFCNT_FROM_PYPY 또는 REFCNT_FROM_PYPY_LIGHT인 링크가 소멸하는 링크라는 것입니다. 하지만 PyObject는 여전히 (대개) tp_dealloc이 호출되어야 하고, 이는 즉시 일어날 수 없기 때문에(그리고 이 호출은 이 객체가 가리키는 다른 참조에 접근하거나 객체를 되살리는 등 임의의 일을 할 수 있기 때문에) 상황이 더 복잡합니다.

P를 rawrefcount.create_link_pypy()로 생성한 링크의 목록이라 하고, O를 rawrefcount.create_link_pyobj()로 생성한 링크의 목록이라 합시다. 목록 O에 있는 PyPy 객체는 모두 W_CPyExtPlaceHolderObject입니다. 모든 데이터는 PyObject에 있으며, 외부 참조가 있다면 모두 C 쪽에 PyObject * 필드로 존재합니다.

따라서 컬렉션 중에 P 링크에 대해서는 다음과 같이 처리합니다:

for (p, ob) in P:
    if ob->ob_refcnt != REFCNT_FROM_PYPY
           and ob->ob_refcnt != REFCNT_FROM_PYPY_LIGHT:
        mark 'p' as surviving, as well as all its dependencies

컬렉션이 끝나면 P 링크와 O 링크는 모두 다음과 같이 처리됩니다:

for (p, ob) in P + O:
    if p is not surviving:    # even if 'ob' might be surviving
        unlink p and ob
        if ob->ob_refcnt == REFCNT_FROM_PYPY_LIGHT:
            free(ob)
        elif ob->ob_refcnt > REFCNT_FROM_PYPY_LIGHT:
            ob->ob_refcnt -= REFCNT_FROM_PYPY_LIGHT
        else:
            ob->ob_refcnt -= REFCNT_FROM_PYPY
            if ob->ob_refcnt == 0:
                invoke _Py_Dealloc(ob) later, outside the GC

GC 구현

젊은(young) 객체용과 오래된(old) 객체용으로 P 리스트와 O 리스트를 각각 두 벌씩 두어야 합니다. 네 리스트는 모두 ‘ob’ 객체들을 담는 일반 AddressList로 구현할 수 있습니다.

또한 P 리스트에 있는 모든 링크에 대해 ‘p’를 ‘ob’에 매핑하는 AddressDict가 필요하며, PyPy 객체가 이동하면 이를 갱신해야 합니다.

추가 참고 사항

XXX XXX 나머지는 이상적인 경우이지만, 첫 단계로는 XXX 기존 cpyext를 조정하는 데 필요한 최소한의 수정 사항을 찾아보겠습니다 XXX

CPython에서 <dict>처럼 불투명한(opaque) 객체의 경우, 항상 PyPy 객체를 먼저 생성하고, 필요할 때 빈 PyObject를 만들어 create_link_pypy()/REFCNT_FROM_PYPY_LIGHT로 연결합니다.

<int> 및 <float> 객체의 경우, 대응하는 PyObject에도 “long” 또는 “double” 필드가 들어 있습니다. 이들은 create_link_pypy()로 연결하며, REFCNT_FROM_PYPY_LIGHT도 사용할 수 있습니다. ‘tp_dealloc’을 호출할 필요가 없고, 대신 free()만 호출하면 됩니다.

<type> 객체의 경우 PyPy 쪽과 PyObject 쪽이 모두 필요합니다. 이들은 create_link_pypy()/REFCNT_FROM_PYPY로 만들어집니다.

C 확장 모듈에서 할당된 사용자 정의 PyXxxObject의 경우에는 create_link_pyobj()가 필요합니다.

PyPy에서 온 <str> 또는 <unicode> 객체의 경우, 문자열 크기만큼 미리 할당된 PyObject와 함께 create_link_pypy()/REFCNT_FROM_PYPY_LIGHT를 사용합니다. PyString_AS_STRING()이 호출되면 문자열을 그 영역에 지연 복사합니다.

C 확장 모듈의 <str>, <unicode>, <tuple> 또는 <list> 객체는 먼저 PyObject로만 할당합니다. 이렇게 하면 CPython처럼 C에서 데이터를 변경할 수 있습니다. 이 객체를 PyPy로 내보낼 때 create_link_pyobj()를 사용하여 W_CPyExtPlaceHolderObject를 만들 수 있습니다.

PyPy에서 온 <tuple> 객체가 특수화되어 있지 않다면 PyPy 쪽에서 항목들에 대한 일반 참조를 보유합니다. 그러면 PyTupleObject를 할당하고 그 안에 항목들을 가리키는 빌린(borrowed) PyObject 포인터를 저장할 수 있습니다. 이러한 경우는 create_link_pypy()/REFCNT_FROM_PYPY_LIGHT로 생성됩니다. 특수화되어 있다면 항목들이 PyPy 쪽에서 필요한 시점에(just-in-time) 생성되므로 이 방법이 통하지 않습니다. 이 경우에는 PyTupleObject가 PyObject 항목들에 대한 실제 참조를 보유해야 하며, create_link_pypy()/ REFCNT_FROM_PYPY를 사용합니다. 어느 경우든 PySequence_Fast_ITEMS, PyTuple_ITEMS, PyTuple_GetItem 등에서 직접 반환할 수 있는 PyObject의 C 배열을 갖게 됩니다.

PyPy에서 오는 <list> 객체에는 cpyext 리스트 전략을 사용할 수 있습니다. 이 리스트는 마치 처음부터 C에서 할당된 것처럼 PyListObject로 바뀝니다. 이 특수 전략은 PyListObject에 대한 직접 참조(만)를 보유할 수 있으며, create_link_pyobj()나 create_link_pypy() 중 하나를 사용할 수 있습니다(추후 결정). 그러면 PySequence_Fast_ITEMS도 리스트에 대해 동작하고, PyList_GetItem도 빌린 참조(borrowed reference)를 반환할 수 있는 등의 이점이 있습니다.