Skip to content

Commit d4a5ed6

Browse files
authored
Fix a memory leak in the Operator base class (#1632)
* Fix a memory leak in the Operator base class References to operator *class* definitions were cached in the argument cache of `_dispatch_call_args` function, hence GC couldn't collect them. Normally that's not an issue, as class definitions don't consume much memory. However, a typical pattern of defining adjoint operators in ODL is using nested classes with references back to their encapsulating *objects*. Depending on the Operator, it may cause considerable memory leaks. For example, `RayTransform` objects hold references to space and geometry objects, preventing them from being reaped by GC and leading to considerable memory leaks. The fix I've implemented is moving away from an argument cache in favor of caching the returned values on the class itself. This ensures no class object references are held permanently in the cache_arguments() decorator.
1 parent 139e796 commit d4a5ed6

File tree

1 file changed

+15
-16
lines changed

1 file changed

+15
-16
lines changed

odl/operator/operator.py

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
from odl.set import Field, LinearSpace, Set
1919
from odl.set.space import LinearSpaceElement
20-
from odl.util import cache_arguments
2120

2221
__all__ = (
2322
'Operator',
@@ -122,7 +121,6 @@ def _function_signature(func):
122121
return '{}({})'.format(func.__name__, argstr)
123122

124123

125-
@cache_arguments
126124
def _dispatch_call_args(cls=None, bound_call=None, unbound_call=None,
127125
attr='_call'):
128126
"""Check the arguments of ``_call()`` or similar for conformity.
@@ -420,20 +418,21 @@ class described in the following.
420418

421419
def __new__(cls, *args, **kwargs):
422420
"""Create a new instance."""
423-
call_has_out, call_out_optional, _ = _dispatch_call_args(cls)
424-
cls._call_has_out = call_has_out
425-
cls._call_out_optional = call_out_optional
426-
if not call_has_out:
427-
# Out-of-place _call
428-
cls._call_in_place = _default_call_in_place
429-
cls._call_out_of_place = cls._call
430-
elif call_out_optional:
431-
# Dual-use _call
432-
cls._call_in_place = cls._call_out_of_place = cls._call
433-
else:
434-
# In-place-only _call
435-
cls._call_in_place = cls._call
436-
cls._call_out_of_place = _default_call_out_of_place
421+
if '_call_out_of_place' not in cls.__dict__:
422+
call_has_out, call_out_optional, _ = _dispatch_call_args(cls)
423+
cls._call_has_out = call_has_out
424+
cls._call_out_optional = call_out_optional
425+
if not call_has_out:
426+
# Out-of-place _call
427+
cls._call_in_place = _default_call_in_place
428+
cls._call_out_of_place = cls._call
429+
elif call_out_optional:
430+
# Dual-use _call
431+
cls._call_in_place = cls._call_out_of_place = cls._call
432+
else:
433+
# In-place-only _call
434+
cls._call_in_place = cls._call
435+
cls._call_out_of_place = _default_call_out_of_place
437436

438437
return object.__new__(cls)
439438

0 commit comments

Comments
 (0)