Skip to content

Commit 4162265

Browse files
committed
[Python] Set memory policy to "strict"
The ROOT Python interfaces have many memory leaks, which is a major pain point for people using it for long-running scripts in batch jobs. One source of memory leaks was indentified to be the "heuristic memory policy" of cppyy. This means that cppyy assumes that every non-const pointer member function argument was interpreted as the object taking ownership if the argument. For examle, take the non-owning RooLinkedList container. It has a `RooLinkedList::Add(RooAbsArg *arg)` method. ROOT wrongly assumes that this means the RooLinkedList takes ownership of arg, and it drops the ROOT overship. Nobody feels responsible for deleting the object anymore, and there is a memory leak or `arg`. That particular leak was reported in this forum post: https://root-forum.cern.ch/t/memory-leak-in-fits/56249 Function parameters of type `T *` are very common in ROOT, and only rarely do they imply ownership transfer. So changing the memory policy to "strict" would surely fix also many other memory leaks that are not reported so far. In fact, upstream cppyy doesn't even enable this heuristic memory policy anymore by default! So moving ROOT also to the strict memory policy closes the gap between ROOT and cppyy. The potential drawback of this change are crashes in usercode if memory is not properly managed. But these problems should either be fixed by: * the user * dedicated pythonizations for these methods to manage shared ownership via Python reference counters (i.e., setting the parameter as an attribute of the object that the member function was called on) This follows up on PR root-project#4294, in particular it reverts 3a12063. Note that owning **TCollection**, **TSeqCollection**, and **TList** instances are Pythonized to preserve the old behavior of dropping Python ownership of added elements, as that was the case where the memory heuristic was correct.
1 parent 2734b50 commit 4162265

3 files changed

Lines changed: 141 additions & 7 deletions

File tree

README/ReleaseNotes/v640/index.md

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The following people have contributed to this new version:
4545
## Removals
4646

4747
* The `TH1K` class was removed. `TMath::KNNDensity` can be used in its stead.
48-
* The `TObject` equality operator pythonization (`TObject.__eq__`) that was deprecated in ROOT 6.38 and scheduled for removal in ROOT 6.40 is removed.
48+
* The `TObject` equality operator Pythonization (`TObject.__eq__`) that was deprecated in ROOT 6.38 and scheduled for removal in ROOT 6.40 is removed.
4949
* Comparing C++ `nullptr` objects with `None` in Python now raises a `TypeError`, as announced in the ROOT 6.38 release notes. Use truth-value checks like `if not x` or `x is None` instead.
5050
* The `TGLIncludes.h` and `TGLWSIncludes.h` that were deprecated in ROOT 6.38 and scheduled for removal are gone now. Please include your required headers like `<GL/gl.h>` or `<GL/glu.h>` directly.
5151
* The GLEW headers (`GL/eglew.h`, `GL/glew.h`, `GL/glxew.h`, and `GL/wglew.h`) that were installed when building ROOT with `builtin_glew=ON` are no longer installed. This is done because ROOT is moving away from GLEW for loading OpenGL extensions.
@@ -236,6 +236,113 @@ The new implementation is statistically consistent and recommended.
236236

237237
- ROOT dropped support for Python 3.9, meaning ROOT now requires at least Python 3.10.
238238

239+
240+
### Change in memory ownership heuristics
241+
242+
In previous ROOT versions, if a C++ member function took a non-`const` raw pointer, e.g.
243+
```C++
244+
MyClass::add(T* obj)
245+
```
246+
then calling this method from Python on an instance
247+
```Python
248+
my_instance.add(obj)
249+
```
250+
would assume that ownership of `obj` is transferred to `my_instance`.
251+
252+
In practice, many such functions do not actually take ownership.
253+
As a result, this heuristic caused several memory leaks.
254+
255+
Starting with ROOT 6.40, the ROOT Python interfaces no longer assumes ownership transfer for non-`const` raw pointer arguments.
256+
257+
#### What does this mean for you?
258+
259+
Because Python no longer automatically relinquishes ownership, some code that previously relied on the old heuristic may now expose:
260+
261+
* **Dangling references** on the C++ side
262+
* **Double deletes**
263+
264+
These issues must now be fixed by managing object lifetimes explicitly.
265+
266+
#### Dangling references
267+
268+
A dangling reference occurs when C++ holds a pointer or reference to an object that has already been deleted on the Python side.
269+
270+
*Example*
271+
```Python
272+
obj = ROOT.MyClass()
273+
my_instance.foo(obj)
274+
del obj # C++ may still hold a pointer: dangling reference
275+
```
276+
277+
When the Python object is deleted, the memory associated with the C++ object
278+
is also freed. But the C++ side may want to delete the object as well, for
279+
instance if the destructor of the class of `my_instance` also calls `delete obj`.
280+
281+
*Possible remedies*
282+
283+
1. **Keep the Python object alive**
284+
285+
Assign the object to a Python variable that lives at least as long as the C++ reference.
286+
287+
*Example:* Python lifeline
288+
```Python
289+
obj = ROOT.MyClass()
290+
my_instance.foo(obj)
291+
# Setting `obj` as an attribute of `my_instance` makes sure that it's not
292+
# garbage collected before `my_instance` is deleted:
293+
my_instance._owned_obj = obj
294+
del obj # Reference counter for `obj` doesn't hit zero here
295+
```
296+
Setting the lifeline reference could also be done in a user-side Pythonization of `MyClass::foo`.
297+
298+
3. **Transfer ownership explicitly to C++**
299+
* Drop the ownership on the Python side:
300+
```Python
301+
ROOT.SetOwnership(obj, False)
302+
```
303+
* Ensure that the C++ side will take ownership instead, e.g. by explicitly calling `delete` in the class destructor or using smart pointers like `std::unique_ptr`.
304+
305+
3. **Rely on Pythonizations that imply ownership transfer**
306+
307+
If the object is stored in a non-owning collection such as a default-constructed `TCollection` (e.g. `TList`), you can make the collection owning before adding any elements:
308+
```Python
309+
coll.SetOwner(True)
310+
```
311+
This will imply ownership transfer to the C++ side when adding elements with `TCollection::Add()`.
312+
313+
#### Note on **TCollection** Pythonization
314+
315+
`TCollection`-derived classes are Pythonized such that when an object is added to an owning collection via `TCollection::Add()`, Python ownership is automatically dropped.
316+
317+
If you identify other cases where such a Pythonization would be beneficial, please report them via a GitHub issue. Users can also implement custom Pythonizations outside ROOT if needed.
318+
319+
#### Double deletes
320+
321+
A double delete indicates that C++ already owns the object, but Python still attempts to delete it.
322+
323+
In this case, you do not need to ensure C++ ownership, as it already exists.
324+
Instead, ensure that Python does not delete the object.
325+
326+
*Possible remedies*
327+
328+
1. Drop Python ownership explicitly:
329+
```Python
330+
ROOT.SetOwnership(obj, False)
331+
```
332+
333+
2. Pythonize the relevant member function to automatically drop ownership on the Python side (similar to the `TCollection` Pythonization described above).
334+
335+
#### Temporary compatibility option
336+
337+
You can temporarily restore the old heuristic by calling:
338+
```Python
339+
ROOT.SetHeuristicMemoryPolicy(True)
340+
```
341+
342+
after importing ROOT.
343+
344+
This option is intended **for debugging only and will be removed in ROOT 6.44**.
345+
239346
### UHI
240347
#### Backwards incompatible changes
241348
- `TH1.values()` now returns a **read-only** NumPy array by default. Previously it returned a writable array that allowed modifying histogram contents implicitly.

bindings/pyroot/pythonizations/python/ROOT/_facade.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,18 @@ def __init__(self, module, is_ipython):
8383
self.__class__.__getattr__ = self._getattr
8484
self.__class__.__setattr__ = self._setattr
8585

86+
def SetHeuristicMemoryPolicy(self, enabled):
87+
import textwrap
88+
import warnings
89+
90+
msg = """ROOT.SetHeuristicMemoryPolicy() is deprecated and will be removed in ROOT 6.44.
91+
Since ROOT 6.40, the heuristic memory policy is disabled by default, and with
92+
ROOT 6.44 it won't be possible to re-enable it with ROOT.SetHeuristicMemoryPolicy(),
93+
which was only meant to be used during a transition period and will be removed.
94+
"""
95+
warnings.warn(textwrap.dedent(msg), FutureWarning, stacklevel=0)
96+
return self._cppyy._backend.SetHeuristicMemoryPolicy(enabled)
97+
8698
def AddressOf(self, obj):
8799
# Return an indexable buffer of length 1, whose only element
88100
# is the address of the object.
@@ -172,7 +184,6 @@ def _finalSetup(self):
172184
"bind_object",
173185
"as_cobject",
174186
"addressof",
175-
"SetHeuristicMemoryPolicy",
176187
"SetImplicitSmartPointerConversion",
177188
"SetOwnership",
178189
]
@@ -197,11 +208,6 @@ def _finalSetup(self):
197208
if not self.gROOT.IsBatch() and self.PyConfig.StartGUIThread:
198209
self.app.init_graphics(self._cppyy.gbl.gEnv, self._cppyy.gbl.gSystem)
199210

200-
# Set memory policy to kUseHeuristics.
201-
# This restores the default in PyROOT which was changed
202-
# by new Cppyy
203-
self.SetHeuristicMemoryPolicy(True)
204-
205211
# The automatic conversion of ordinary obejcts to smart pointers is
206212
# disabled for ROOT because it can cause trouble with overload
207213
# resolution. If a function has overloads for both ordinary objects and

bindings/pyroot/pythonizations/test/root_module.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ def test_import_nested_submodules(self):
105105
if root_module_has("RooFit.Evaluator"):
106106
from ROOT.RooFit import Evaluator
107107

108+
def test_deprecated_features(self):
109+
"""
110+
Verify that deprecated features are removed in the release where we
111+
announced that they will be removed.
112+
113+
This serves as a reminder to us to delete the deprecated code after
114+
increasing the version number.
115+
116+
Once a deprecated feature was deleted, we can remove the corresponding
117+
check in this test.
118+
"""
119+
import ROOT
120+
121+
version = tuple(int(n) for n in ROOT.__version__.split("."))
122+
123+
if version >= (6, 44, 0):
124+
# Verify that SetHeuristicMemoryPolicy is not available
125+
with self.assertRaises(AttributeError):
126+
ROOT.SetHeuristicMemoryPolicy(True)
127+
ROOT.SetHeuristicMemoryPolicy(False)
128+
108129

109130
if __name__ == "__main__":
110131
unittest.main()

0 commit comments

Comments
 (0)