Skip to content

Commit f74e0db

Browse files
Merge pull request #546 from 27rabbitlt/add_disable_cache_parameter
FEAT: add `disable_cache` parameter in `evaluate` function.
2 parents 30624a2 + c657605 commit f74e0db

File tree

2 files changed

+42
-0
lines changed

2 files changed

+42
-0
lines changed

numexpr/necompiler.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,7 @@ def evaluate(ex: str,
920920
casting: str = 'same_kind',
921921
sanitize: Optional[bool] = None,
922922
_frame_depth: int = 3,
923+
disable_cache: bool = False,
923924
**kwargs) -> numpy.ndarray:
924925
r"""
925926
Evaluate a simple array expression element-wise using the virtual machine.
@@ -978,10 +979,41 @@ def evaluate(ex: str,
978979
The calling frame depth. Unless you are a NumExpr developer you should
979980
not set this value.
980981
982+
disable_cache: bool
983+
If set to be `True`, disables the uses of internal expression cache.
984+
985+
By default, NumExpr caches compiled expressions and associated metadata
986+
(via the internal `_numexpr_last`, `_numexpr_cache`, and `_names_cache`
987+
structures). This allows repeated evaluations of the same expression
988+
to skip recompilation, improving performance in workloads where the same
989+
expression is executed multiple times.
990+
991+
However, caching retains references to input and output arrays in order
992+
to support re-evaluation. As a result, this can increase their reference
993+
counts and may prevent them from being garbage-collected immediately.
994+
In situations where precise control over object lifetimes or memory
995+
management is required, set `disable_cache=True` to avoid this behavior.
996+
997+
Default is `False`.
998+
981999
"""
9821000
# We could avoid code duplication if we called validate and then re_evaluate
9831001
# here, but we have difficulties with the `sys.getframe(2)` call in
9841002
# `getArguments`
1003+
1004+
# If dissable_cache set to be True, we evaluate the expression here
1005+
# Otherwise we validate and then re_evaluate
1006+
if disable_cache:
1007+
context = getContext(kwargs)
1008+
names, ex_uses_vml = getExprNames(ex, context, sanitize=sanitize)
1009+
arguments = getArguments(names, local_dict, global_dict, _frame_depth=_frame_depth - 1)
1010+
signature = [(name, getType(arg)) for (name, arg) in
1011+
zip(names, arguments)]
1012+
compiled_ex = NumExpr(ex, signature, sanitize=sanitize, **context)
1013+
kwargs = {'out': out, 'order': order, 'casting': casting,
1014+
'ex_uses_vml': ex_uses_vml}
1015+
return compiled_ex(*arguments, **kwargs)
1016+
9851017
e = validate(ex, local_dict=local_dict, global_dict=global_dict,
9861018
out=out, order=order, casting=casting,
9871019
_frame_depth=_frame_depth, sanitize=sanitize, **kwargs)

numexpr/tests/test_numexpr.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,16 @@ def _test_refcount(self):
337337
evaluate('1')
338338
assert sys.getrefcount(a) == 2
339339

340+
# Test if `disable_cache` works correctly with refcount, see issue #521
341+
# Comment out as modern Python optimizes handling refcounts.
342+
@unittest.skipIf(hasattr(sys, "pypy_version_info"),
343+
"PyPy does not have sys.getrefcount()")
344+
def _test_refcount_disable_cache(self):
345+
a = array([1])
346+
b = array([1])
347+
evaluate('a', out=b, disable_cache=True)
348+
assert sys.getrefcount(b) == 2
349+
340350
@pytest.mark.thread_unsafe
341351
def test_locals_clears_globals(self):
342352
# Check for issue #313, whereby clearing f_locals also clear f_globals

0 commit comments

Comments
 (0)