Skip to content

Commit 8a346be

Browse files
committed
[GR-52150] Documentation of the C API memory management, inline comments, rename UpdateRefNode.
PullRequest: graalpython/3496
2 parents da9f6d9 + e280841 commit 8a346be

File tree

6 files changed

+170
-32
lines changed

6 files changed

+170
-32
lines changed

docs/contributor/IMPLEMENTATION_DETAILS.md

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ files to get an idea of how rules are set for patches to be applied.
9696

9797
We always run with a GIL, because C extensions in CPython expect to do so and
9898
are usually not written to be reentrant. The reason to always have the GIL
99-
enabled is that when using Python, at least Sulong/LLVM is always available in
100-
the same context and we cannot know if someone may be using that (or another
101-
polyglot language or the Java host interop) to start additional threads that
99+
enabled is that when using Python, another polyglot language or the Java host
100+
interop can be available in the same context, and we cannot know if someone
101+
may be using that to start additional threads that
102102
could call back into Python. This could legitimately happen in C extensions when
103103
the C extension authors use knowledge of how CPython works to do something
104104
GIL-less in a C thread that is fine to do on CPython's data structures, but not
@@ -165,3 +165,127 @@ For embedders, it may be important to be able to interrupt Python threads by
165165
other means. We use the TruffleSafepoint mechanism to mark our threads waiting
166166
to acquire the GIL as blocked for the purpose of safepoints. The Truffle
167167
safepoint action mechanism can thus be used to kill threads waiting on the GIL.
168+
169+
## C Extensions and Memory Management
170+
171+
### High-level
172+
173+
C extensions assume reference counting, but on the managed side we want to leverage
174+
Java tracing GC. This creates a mismatch. The approach is to do both, reference
175+
counting and tracing GC, at the same time.
176+
177+
On the native side we use reference counting. The native code is responsible for doing
178+
the counting, i.e., calling the `Py_IncRef` and `Py_DecRef` API functions. Inside those
179+
functions we add special handling for the point when first reference from the native
180+
code is created and when the last reference from the native code is destroyed.
181+
182+
On the managed side we rely on tracing GC, so managed references are not ref-counted.
183+
For the ref-counting scheme on the native side, we approximate all the managed references
184+
as a single reference, i.e., we increment the refcount when object is referenced from managed
185+
code, and using a `PhantomReference` and reference queue we decrement the refcount when
186+
there are no longer any managed references (but we do not clean the object as long as
187+
`refcount > 0`, because that means that there are still native references to it).
188+
189+
### Details
190+
191+
There are two kinds of Python objects in GraalPy: managed and native.
192+
193+
#### Managed Objects
194+
195+
Managed objects are allocated in the interpreter. If there is no native code involved,
196+
we do not do anything special and let the Java GC handle them. When a managed object
197+
is passed to a native extension code:
198+
199+
* We wrap it in `PythonObjectNativeWrapper`. This is mostly in order to provide different
200+
interop protocol: we do not want to expose `toNative` and `asPointer` on Python objects.
201+
202+
* When NFI calls `toNative`/`asPointer` we:
203+
* Allocate C memory that will represent the object on the native side (including the refcount field)
204+
* Add a mapping of that memory address to the `PythonObjectNativeWrapper` object to a hash map `CApiTransitions.nativeLookup`.
205+
* We initialize the refcount field to a constant `MANAGED_REFCNT` (larger number, because some
206+
extensions like to special case on some small refcount values)
207+
* Create `PythonObjectReference`: a weak reference to the `PythonObjectNativeWrapper`,
208+
when this reference is enqueued (i.e., no managed references exist), we decrement the refcount by
209+
`MANAGED_REFCNT` and if the recount falls back to `0`, we deallocate the native memory of the object,
210+
otherwise we need to wait for the native code to eventually call `Py_DecRef` and make it `0`.
211+
212+
* When extension code wants to create a new reference, it will call `Py_IncRef`.
213+
In the C implementation of `Py_IncRef` we check if a managed object with
214+
`refcount==MANAGED_REFCNT` wants to increment its refcount. In such case, the native code is
215+
creating a first reference to the managed object, we must make sure to keep the object alive
216+
as long as there are some native references. We set a field `PythonObjectReference.strongReference`,
217+
which will keep the `PythonObjectNativeWrapper` alive even when all other managed references die.
218+
219+
* When extension code is done with the object, it will call `Py_DecRef`.
220+
In the C implementation of `Py_DecRef` we check if a managed object with `refcount == MANAGED_REFCNT+1`
221+
wants to decrement its refcount to MANAGED_REFCNT, which means that there are no native references
222+
to that object anymore. In such case we clear the `PythonObjectReference.strongReference` field,
223+
and the memory management is then again left solely to the Java tracing GC.
224+
225+
#### Native Objects
226+
227+
Native objects allocated using `PyObject_GC_New` in the native code are backed by native memory
228+
and may never be passed to managed code (as a return value of extension function or as an argument
229+
to some C API call). If a native object is not made available to managed code, it is just reference
230+
counted as usual, where `Py_DecRef` call that reaches `0` will deallocate the object. If a native
231+
object is passed to managed code:
232+
233+
* We increment the refcount of the native object by `MANAGED_REFCNT`
234+
* We create:
235+
* `PythonAbstractNativeObject` Java object to mirror it on the managed side
236+
* `NativeObjectReference`, a weak reference to the `PythonAbstractNativeObject`.
237+
* Add mapping: native object address => `NativeObjectReference` into hash map `CApiTransitions.nativeLookup`
238+
* Next time we just fetch the existing wrapper and don't do any of this
239+
* When `NativeObjectReference` is enqueued, we decrement the refcount by `MANAGED_REFCNT`
240+
* If the refcount falls to `0`, it means that there are no references to the object even from
241+
native code, and we can destroy it. If it does not fall to `0`, we just wait for the native
242+
code to eventually call `Py_DecRef` that makes it fall to `0`.
243+
244+
#### Weak References
245+
246+
TODO
247+
248+
### Cycle GC
249+
250+
We leverage the CPython's GC module to detect cycles for objects that participate
251+
in the reference counting scheme (native objects or managed objects that got passed
252+
to native code).
253+
See: https://devguide.python.org/internals/garbage-collector/index.html.
254+
255+
There are two issues:
256+
257+
* Objects that are referenced from the managed code have `refcount >= MANAGED_REFCNT` and
258+
until Java GC runs we do not know if they are garbage or not.
259+
* We cannot traverse the managed objects: since we don't do refcounting on the managed
260+
side, we cannot traverse them and decrement refcounts to see if there is a cycle.
261+
262+
The high level solution is that when we see a "dead" cycle going through a managed object
263+
(i.e., cycle not referenced by any native object from the "outside" of the collected set),
264+
we fully replicate the object graphs (and the cycle) on the managed side (refcounts of native objects
265+
in the cycle, which were not referenced from managed yet, will get new `NativeObjectReference`
266+
created and refcount incremented by `MANAGED_REFCNT`). Managed objects already refer
267+
to the `PythonAbstractNativeObject` wrappers of the native objects (e.g., some Python container
268+
with managed storage), but we also make the native wrappers refer to whatever their referents
269+
are on the Java side (we use `tp_traverse` to find their referents).
270+
271+
Then we make the managed objects in the cycle only weakly referenced on the Java side.
272+
One can think about this as pushing the baseline reference count when the
273+
object is eligible for being GC'ed and thus freed. Normally when the object has
274+
`refcount > MANAGED_REFCNT` we keep it alive with a strong reference assuming that
275+
there are some native references to it. In this case, we know that all the native
276+
references to that object are part of potentially dead cycle, and we do not
277+
count them into this limit. Let us call this limit *weak to strong limit*.
278+
279+
After this, if the managed objects are garbage, eventually Java GC will collect them
280+
together with the whole cycle.
281+
282+
If some of the managed objects are not garbage, and they passed back to native code,
283+
the native code can then access and resurrect the whole cycle. W.r.t. the refcounts
284+
integrity this is fine, because we did not alter the refcounts. The native references
285+
between the objects are still factored in their refcounts. What may seem like a problem
286+
is that we pushed the *weak to strong limit* for some objects. Such an object may be
287+
passed to native, get `Py_IncRef`'ed making it strong reference again. Since `Py_DecRef` is
288+
checking the same `MANAGED_REFCNT` limit for all objects, the subsequent `Py_DecRef`
289+
call for this object will not detect that the reference should be made weak again!
290+
However, this is OK, it only prolongs the collection: we will make it weak again in
291+
the next run of the cycle GC on the native side.

graalpython/com.oracle.graal.python.cext/src/gcmodule.c

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ validate_list(PyGC_Head *head, enum flagstates flags)
457457
static int
458458
visit_reachable(PyObject *op, PyGC_Head *reachable);
459459

460-
/* A traversal callback for move_weak_candidates.
460+
/* A traversal callback for move_unreachable.
461461
*
462462
* This function is only used to traverse the referents of an object that was
463463
* moved from 'unreachable' to 'young' because it was made reachable due to a
@@ -729,7 +729,7 @@ visit_reachable(PyObject *op, PyGC_Head *reachable)
729729
// an untracked object.
730730
assert(UNTAG(gc)->_gc_next != 0);
731731

732-
const Py_ssize_t gc_refs_reset = is_managed(gc) ? MANAGED_REFCNT + 1 : 1;
732+
const Py_ssize_t gc_refs_reset = is_managed(gc) ? MANAGED_REFCNT + 1 : 1; // GraalPy change
733733
if (UNTAG(gc)->_gc_next & NEXT_MASK_UNREACHABLE) {
734734
/* This had gc_refs = 0 when move_unreachable got
735735
* to it, but turns out it's reachable after all.
@@ -749,15 +749,18 @@ visit_reachable(PyObject *op, PyGC_Head *reachable)
749749
_PyGCHead_SET_PREV(next, prev);
750750

751751
gc_list_append(gc, reachable);
752-
gc_set_refs(gc, gc_refs_reset);
752+
gc_set_refs(gc, gc_refs_reset); // GraalPy change: 1->gc_refs_reset
753753
}
754-
else if (gc_refs == 0 || (gc_refs == MANAGED_REFCNT && is_managed(gc))) {
754+
else if (gc_refs == 0 ||
755+
// GraalPy change: the additional condition, managed objects with MANAGED_REFCNT
756+
// may be still reachanble from managed, we must not declare them unreachable
757+
(gc_refs == MANAGED_REFCNT && is_managed(gc))) {
755758
/* This is in move_unreachable's 'young' list, but
756759
* the traversal hasn't yet gotten to it. All
757760
* we need to do is tell move_unreachable that it's
758761
* reachable.
759762
*/
760-
gc_set_refs(gc, gc_refs_reset);
763+
gc_set_refs(gc, gc_refs_reset); // GraalPy change: 1->gc_refs_reset
761764
}
762765
/* Else there's nothing to do.
763766
* If gc_refs > 0, it must be in move_unreachable's 'young'
@@ -837,12 +840,12 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable,
837840
"refcount is too small");
838841
// NOTE: visit_reachable may change gc->_gc_next when
839842
// young->_gc_prev == gc. Don't do gc = GC_NEXT(gc) before!
840-
// GraalPy change
841-
// CALL_TRAVERSE(traverse, op, visit_reachable, (void *)young);
842843
if (PyTruffle_PythonGC()) {
844+
// GraalPy change: this branch, else branch is original CPython code
843845
cycle.head = NULL;
844846
cycle.n = 0;
845847
assert (cycle.reachable == weak_candidates );
848+
/* visit_collect_managed_referents is visit_reachable + capture the references into "cycle" */
846849
CALL_TRAVERSE(traverse, op, visit_collect_managed_referents, (void *)&cycle);
847850

848851
/* replicate any native reference to managed objects to Java */
@@ -856,6 +859,9 @@ move_unreachable(PyGC_Head *young, PyGC_Head *unreachable,
856859

857860
if (gc_refcnt == MANAGED_REFCNT && is_managed(gc) &&
858861
Py_REFCNT(op) > MANAGED_REFCNT) {
862+
// The refcount fell to MANAGED_REFCNT, we have a managed object that was
863+
// part of a cycle and is no longer referenced from native space.
864+
859865
// Assertion is enough because if Python GC is disabled, we will
860866
// never track managed objects.
861867
assert (PyTruffle_PythonGC());

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextBuiltins.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandleContext;
132132
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.HandlePointerConverter;
133133
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativePtrToPythonWrapperNode;
134-
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateRefNode;
134+
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateStrongRefNode;
135135
import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.CoerceNativePointerToLongNode;
136136
import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodes.TransformExceptionToNativeNode;
137137
import com.oracle.graal.python.builtins.objects.cext.common.CExtCommonNodesFactory.TransformExceptionToNativeNodeGen;
@@ -1541,7 +1541,7 @@ static Object doNative(Object weakCandidates,
15411541
@Cached CStructAccess.ReadI64Node readI64Node,
15421542
@Cached CStructAccess.WriteLongNode writeLongNode,
15431543
@Cached NativePtrToPythonWrapperNode nativePtrToPythonWrapperNode,
1544-
@Cached UpdateRefNode updateRefNode) {
1544+
@Cached UpdateStrongRefNode updateRefNode) {
15451545
// guaranteed by the guard
15461546
assert PythonContext.get(inliningTarget).isNativeAccessAllowed();
15471547
assert PythonContext.get(inliningTarget).getOption(PythonOptions.PythonGC);
@@ -1571,7 +1571,7 @@ static Object doNative(Object weakCandidates,
15711571
if (GC_LOGGER.isLoggable(Level.FINE)) {
15721572
GC_LOGGER.fine(PythonUtils.formatJString("Breaking reference cycle for %s", abstractObjectNativeWrapper.ref));
15731573
}
1574-
updateRefNode.execute(inliningTarget, abstractObjectNativeWrapper, PythonAbstractObjectNativeWrapper.MANAGED_REFCNT);
1574+
updateRefNode.clearStrongRef(inliningTarget, abstractObjectNativeWrapper);
15751575
}
15761576

15771577
// next = GC_NEXT(gc)

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/cext/PythonCextObjectBuiltins.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonNode;
9292
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeNode;
9393
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.ToPythonWrapperNode;
94-
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateRefNode;
94+
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateStrongRefNode;
9595
import com.oracle.graal.python.builtins.objects.cext.common.GetNextVaArgNode;
9696
import com.oracle.graal.python.builtins.objects.cext.structs.CFields;
9797
import com.oracle.graal.python.builtins.objects.cext.structs.CStructAccess;
@@ -163,7 +163,7 @@ abstract static class PyTruffle_NotifyRefCount extends CApiBinaryBuiltinNode {
163163
@Specialization
164164
static Object doGeneric(PythonAbstractObjectNativeWrapper wrapper, long refCount,
165165
@Bind("this") Node inliningTarget,
166-
@Cached UpdateRefNode updateRefNode) {
166+
@Cached UpdateStrongRefNode updateRefNode) {
167167
assert CApiTransitions.readNativeRefCount(HandlePointerConverter.pointerToStub(wrapper.getNativePointer())) == refCount;
168168
// refcounting on an immortal object should be a NOP
169169
assert refCount != PythonAbstractObjectNativeWrapper.IMMORTAL_REFCNT;
@@ -178,7 +178,7 @@ abstract static class PyTruffle_BulkNotifyRefCount extends CApiBinaryBuiltinNode
178178
@Specialization
179179
static Object doGeneric(Object arrayPointer, int len,
180180
@Bind("this") Node inliningTarget,
181-
@Cached UpdateRefNode updateRefNode,
181+
@Cached UpdateStrongRefNode updateRefNode,
182182
@Cached CStructAccess.ReadPointerNode readPointerNode,
183183
@Cached ToPythonWrapperNode toPythonWrapperNode) {
184184

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/objects/cext/capi/CExtNodes.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.NativeToPythonTransferNode;
107107
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.PythonToNativeNode;
108108
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.ResolveHandleNode;
109-
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateRefNode;
109+
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitions.UpdateStrongRefNode;
110110
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitionsFactory.NativeToPythonNodeGen;
111111
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.CApiTransitionsFactory.PythonToNativeNodeGen;
112112
import com.oracle.graal.python.builtins.objects.cext.capi.transitions.GetNativeWrapperNode;
@@ -1189,7 +1189,7 @@ static void doDecref(Node inliningTarget, Object pointerObj,
11891189
@Cached(inline = false) CApiTransitions.ToPythonWrapperNode toPythonWrapperNode,
11901190
@Cached InlinedBranchProfile isWrapperProfile,
11911191
@Cached InlinedBranchProfile isNativeObject,
1192-
@Cached UpdateRefNode updateRefNode,
1192+
@Cached UpdateStrongRefNode updateRefNode,
11931193
@Cached(inline = false) CStructAccess.ReadI64Node readRefcount,
11941194
@Cached(inline = false) CStructAccess.WriteLongNode writeRefcount,
11951195
@Cached(inline = false) PCallCapiFunction callDealloc) {
@@ -1295,7 +1295,7 @@ public static Object executeUncached(Object pointerObject) {
12951295
@Specialization
12961296
static Object resolveLongCached(Node inliningTarget, long pointer,
12971297
@Exclusive @Cached ResolveHandleNode resolveHandleNode,
1298-
@Exclusive @Cached UpdateRefNode updateRefNode) {
1298+
@Exclusive @Cached UpdateStrongRefNode updateRefNode) {
12991299
Object lookup = CApiTransitions.lookupNative(pointer);
13001300
if (lookup != null) {
13011301
if (lookup instanceof PythonAbstractObjectNativeWrapper objectNativeWrapper) {
@@ -1313,7 +1313,7 @@ static Object resolveLongCached(Node inliningTarget, long pointer,
13131313
static Object resolveGeneric(Node inliningTarget, Object pointerObject,
13141314
@CachedLibrary(limit = "3") InteropLibrary lib,
13151315
@Exclusive @Cached ResolveHandleNode resolveHandleNode,
1316-
@Exclusive @Cached UpdateRefNode updateRefNode) {
1316+
@Exclusive @Cached UpdateStrongRefNode updateRefNode) {
13171317
if (lib.isPointer(pointerObject)) {
13181318
Object lookup;
13191319
long pointer;

0 commit comments

Comments
 (0)