Commit 92147baf authored by scoder's avatar scoder Committed by GitHub

Validate and fix temp releasing (GH-3708)

* Fix a temp leak in the type init code.
* Fix temp leaks in fused types initialisation code.
* Correctly release the buffer index temps allocated for index calculations.
* Make tests fails hard if a temp variable is not released at the end of a generated function.
* Fix temp leak in switch statement code.
* Make end-to-end tests fail on refnanny output.
* Fix result temp leak in PyTypeTestNode.
* Fix result temp leak in external type/function import code and enable the refnanny check for them.
* Fix temp leak when try-return-finally is used in generators.
* Make it explicit when an allocated temp is not meant to be reused.
* Fix temp leak when assigning to the real/imag attributes of complex numbers.
* Fix temp leak when assigning to a memoryview slice.
* Clean up "num_threads" result temp in parallel section, not only in prange loop.
* Fix temp leak in Pythran buffer setitem code.
* Simplify NumPyMethodCallNode since it does not need the Python function anymore. Previously, it generated code that needlessly looked up the Python function without actually using it.
* Fix temp leak when deleting C++ objects.
* Add a test that actually reusing temps when deleting C++ objects works correctly.
parent d831d71a
...@@ -721,6 +721,7 @@ class FunctionState(object): ...@@ -721,6 +721,7 @@ class FunctionState(object):
self.temps_allocated = [] # of (name, type, manage_ref, static) self.temps_allocated = [] # of (name, type, manage_ref, static)
self.temps_free = {} # (type, manage_ref) -> list of free vars with same type/managed status self.temps_free = {} # (type, manage_ref) -> list of free vars with same type/managed status
self.temps_used_type = {} # name -> (type, manage_ref) self.temps_used_type = {} # name -> (type, manage_ref)
self.zombie_temps = set() # temps that must not be reused after release
self.temp_counter = 0 self.temp_counter = 0
self.closure_temps = None self.closure_temps = None
...@@ -740,14 +741,14 @@ class FunctionState(object): ...@@ -740,14 +741,14 @@ class FunctionState(object):
def validate_exit(self): def validate_exit(self):
# validate that all allocated temps have been freed # validate that all allocated temps have been freed
if self.temps_allocated: if self.temps_allocated:
leftovers = set(self.all_managed_temps()).difference(self.all_free_managed_temps()) leftovers = self.temps_in_use()
if leftovers: if leftovers:
msg = "Temps left over at end of '%s': %s" % ( msg = "TEMPGUARD: Temps left over at end of '%s': %s" % (self.scope.name, ', '.join([
self.scope.name, '%s [%s]' % (name, ctype)
', '.join(map(str, sorted(leftovers, key=operator.itemgetter(0)))), for name, ctype, is_pytemp in sorted(leftovers)]),
) )
print(msg) #print(msg)
#raise RuntimeError(msg) raise RuntimeError(msg)
# labels # labels
...@@ -818,7 +819,7 @@ class FunctionState(object): ...@@ -818,7 +819,7 @@ class FunctionState(object):
# temp handling # temp handling
def allocate_temp(self, type, manage_ref, static=False): def allocate_temp(self, type, manage_ref, static=False, reusable=True):
""" """
Allocates a temporary (which may create a new one or get a previously Allocates a temporary (which may create a new one or get a previously
allocated and released one of the same type). Type is simply registered allocated and released one of the same type). Type is simply registered
...@@ -837,6 +838,8 @@ class FunctionState(object): ...@@ -837,6 +838,8 @@ class FunctionState(object):
This is only used when allocating backing store for a module-level This is only used when allocating backing store for a module-level
C array literals. C array literals.
if reusable=False, the temp will not be reused after release.
A C string referring to the variable is returned. A C string referring to the variable is returned.
""" """
if type.is_cv_qualified and not type.is_reference: if type.is_cv_qualified and not type.is_reference:
...@@ -852,7 +855,7 @@ class FunctionState(object): ...@@ -852,7 +855,7 @@ class FunctionState(object):
manage_ref = False manage_ref = False
freelist = self.temps_free.get((type, manage_ref)) freelist = self.temps_free.get((type, manage_ref))
if freelist is not None and freelist[0]: if reusable and freelist is not None and freelist[0]:
result = freelist[0].pop() result = freelist[0].pop()
freelist[1].remove(result) freelist[1].remove(result)
else: else:
...@@ -861,9 +864,11 @@ class FunctionState(object): ...@@ -861,9 +864,11 @@ class FunctionState(object):
result = "%s%d" % (Naming.codewriter_temp_prefix, self.temp_counter) result = "%s%d" % (Naming.codewriter_temp_prefix, self.temp_counter)
if result not in self.names_taken: break if result not in self.names_taken: break
self.temps_allocated.append((result, type, manage_ref, static)) self.temps_allocated.append((result, type, manage_ref, static))
if not reusable:
self.zombie_temps.add(result)
self.temps_used_type[result] = (type, manage_ref) self.temps_used_type[result] = (type, manage_ref)
if DebugFlags.debug_temp_code_comments: if DebugFlags.debug_temp_code_comments:
self.owner.putln("/* %s allocated (%s) */" % (result, type)) self.owner.putln("/* %s allocated (%s)%s */" % (result, type, "" if reusable else " - zombie"))
if self.collect_temps_stack: if self.collect_temps_stack:
self.collect_temps_stack[-1].add((result, type)) self.collect_temps_stack[-1].add((result, type))
...@@ -882,10 +887,12 @@ class FunctionState(object): ...@@ -882,10 +887,12 @@ class FunctionState(object):
self.temps_free[(type, manage_ref)] = freelist self.temps_free[(type, manage_ref)] = freelist
if name in freelist[1]: if name in freelist[1]:
raise RuntimeError("Temp %s freed twice!" % name) raise RuntimeError("Temp %s freed twice!" % name)
if name not in self.zombie_temps:
freelist[0].append(name) freelist[0].append(name)
freelist[1].add(name) freelist[1].add(name)
if DebugFlags.debug_temp_code_comments: if DebugFlags.debug_temp_code_comments:
self.owner.putln("/* %s released */" % name) self.owner.putln("/* %s released %s*/" % (
name, " - zombie" if name in self.zombie_temps else ""))
def temps_in_use(self): def temps_in_use(self):
"""Return a list of (cname,type,manage_ref) tuples of temp names and their type """Return a list of (cname,type,manage_ref) tuples of temp names and their type
......
...@@ -4396,6 +4396,7 @@ class BufferIndexNode(_IndexingBaseNode): ...@@ -4396,6 +4396,7 @@ class BufferIndexNode(_IndexingBaseNode):
pythran_indexing_code(self.indices), pythran_indexing_code(self.indices),
op, op,
rhs.pythran_result())) rhs.pythran_result()))
code.funcstate.release_temp(obj)
return return
# Used from generate_assignment_code and InPlaceAssignmentNode # Used from generate_assignment_code and InPlaceAssignmentNode
...@@ -4436,11 +4437,11 @@ class BufferIndexNode(_IndexingBaseNode): ...@@ -4436,11 +4437,11 @@ class BufferIndexNode(_IndexingBaseNode):
code.putln("%s = (PyObject *) *%s;" % (self.result(), self.buffer_ptr_code)) code.putln("%s = (PyObject *) *%s;" % (self.result(), self.buffer_ptr_code))
code.putln("__Pyx_INCREF((PyObject*)%s);" % self.result()) code.putln("__Pyx_INCREF((PyObject*)%s);" % self.result())
def free_temps(self, code): def free_subexpr_temps(self, code):
for temp in self.index_temps: for temp in self.index_temps:
code.funcstate.release_temp(temp) code.funcstate.release_temp(temp)
self.index_temps = () self.index_temps = ()
super(BufferIndexNode, self).free_temps(code) super(BufferIndexNode, self).free_subexpr_temps(code)
class MemoryViewIndexNode(BufferIndexNode): class MemoryViewIndexNode(BufferIndexNode):
...@@ -4717,6 +4718,7 @@ class MemoryCopyNode(ExprNode): ...@@ -4717,6 +4718,7 @@ class MemoryCopyNode(ExprNode):
self.dst.generate_evaluation_code(code) self.dst.generate_evaluation_code(code)
self._generate_assignment_code(rhs, code) self._generate_assignment_code(rhs, code)
self.dst.generate_disposal_code(code) self.dst.generate_disposal_code(code)
self.dst.free_temps(code)
rhs.generate_disposal_code(code) rhs.generate_disposal_code(code)
rhs.free_temps(code) rhs.free_temps(code)
...@@ -5641,7 +5643,7 @@ class SimpleCallNode(CallNode): ...@@ -5641,7 +5643,7 @@ class SimpleCallNode(CallNode):
env.add_include_file(pythran_get_func_include_file(function)) env.add_include_file(pythran_get_func_include_file(function))
return NumPyMethodCallNode.from_node( return NumPyMethodCallNode.from_node(
self, self,
function=function, function_cname=pythran_functor(function),
arg_tuple=self.arg_tuple, arg_tuple=self.arg_tuple,
type=PythranExpr(pythran_func_type(function, self.arg_tuple.args)), type=PythranExpr(pythran_func_type(function, self.arg_tuple.args)),
) )
...@@ -6047,13 +6049,13 @@ class SimpleCallNode(CallNode): ...@@ -6047,13 +6049,13 @@ class SimpleCallNode(CallNode):
code.funcstate.release_temp(self.opt_arg_struct) code.funcstate.release_temp(self.opt_arg_struct)
class NumPyMethodCallNode(SimpleCallNode): class NumPyMethodCallNode(ExprNode):
# Pythran call to a NumPy function or method. # Pythran call to a NumPy function or method.
# #
# function ExprNode the function/method to call # function_cname string the function/method to call
# arg_tuple TupleNode the arguments as an args tuple # arg_tuple TupleNode the arguments as an args tuple
subexprs = ['function', 'arg_tuple'] subexprs = ['arg_tuple']
is_temp = True is_temp = True
may_return_none = True may_return_none = True
...@@ -6061,7 +6063,6 @@ class NumPyMethodCallNode(SimpleCallNode): ...@@ -6061,7 +6063,6 @@ class NumPyMethodCallNode(SimpleCallNode):
code.mark_pos(self.pos) code.mark_pos(self.pos)
self.allocate_temp_result(code) self.allocate_temp_result(code)
self.function.generate_evaluation_code(code)
assert self.arg_tuple.mult_factor is None assert self.arg_tuple.mult_factor is None
args = self.arg_tuple.args args = self.arg_tuple.args
for arg in args: for arg in args:
...@@ -6072,7 +6073,7 @@ class NumPyMethodCallNode(SimpleCallNode): ...@@ -6072,7 +6073,7 @@ class NumPyMethodCallNode(SimpleCallNode):
code.putln("new (&%s) decltype(%s){%s{}(%s)};" % ( code.putln("new (&%s) decltype(%s){%s{}(%s)};" % (
self.result(), self.result(),
self.result(), self.result(),
pythran_functor(self.function), self.function_cname,
", ".join(a.pythran_result() for a in args))) ", ".join(a.pythran_result() for a in args)))
...@@ -7287,6 +7288,8 @@ class AttributeNode(ExprNode): ...@@ -7287,6 +7288,8 @@ class AttributeNode(ExprNode):
self.member.upper(), self.member.upper(),
self.obj.result_as(self.obj.type), self.obj.result_as(self.obj.type),
rhs.result_as(self.ctype()))) rhs.result_as(self.ctype())))
rhs.generate_disposal_code(code)
rhs.free_temps(code)
else: else:
select_code = self.result() select_code = self.result()
if self.type.is_pyobject and self.use_managed_ref: if self.type.is_pyobject and self.use_managed_ref:
...@@ -8140,20 +8143,18 @@ class ListNode(SequenceNode): ...@@ -8140,20 +8143,18 @@ class ListNode(SequenceNode):
return t return t
def allocate_temp_result(self, code): def allocate_temp_result(self, code):
if self.type.is_array and self.in_module_scope: if self.type.is_array:
if self.in_module_scope:
self.temp_code = code.funcstate.allocate_temp( self.temp_code = code.funcstate.allocate_temp(
self.type, manage_ref=False, static=True) self.type, manage_ref=False, static=True, reusable=False)
else: else:
SequenceNode.allocate_temp_result(self, code)
def release_temp_result(self, env):
if self.type.is_array:
# To be valid C++, we must allocate the memory on the stack # To be valid C++, we must allocate the memory on the stack
# manually and be sure not to reuse it for something else. # manually and be sure not to reuse it for something else.
# Yes, this means that we leak a temp array variable. # Yes, this means that we leak a temp array variable.
pass self.temp_code = code.funcstate.allocate_temp(
self.type, manage_ref=False, reusable=False)
else: else:
SequenceNode.release_temp_result(self, env) SequenceNode.allocate_temp_result(self, code)
def calculate_constant_result(self): def calculate_constant_result(self):
if self.mult_factor: if self.mult_factor:
...@@ -13104,9 +13105,18 @@ class PyTypeTestNode(CoercionNode): ...@@ -13104,9 +13105,18 @@ class PyTypeTestNode(CoercionNode):
def generate_post_assignment_code(self, code): def generate_post_assignment_code(self, code):
self.arg.generate_post_assignment_code(code) self.arg.generate_post_assignment_code(code)
def allocate_temp_result(self, code):
pass
def release_temp_result(self, code):
pass
def free_temps(self, code): def free_temps(self, code):
self.arg.free_temps(code) self.arg.free_temps(code)
def free_subexpr_temps(self, code):
self.arg.free_subexpr_temps(code)
class NoneCheckNode(CoercionNode): class NoneCheckNode(CoercionNode):
# This node is used to check that a Python object is not None and # This node is used to check that a Python object is not None and
......
...@@ -924,17 +924,23 @@ class FusedCFuncDefNode(StatListNode): ...@@ -924,17 +924,23 @@ class FusedCFuncDefNode(StatListNode):
(self.resulting_fused_function.result(), (self.resulting_fused_function.result(),
self.__signatures__.result())) self.__signatures__.result()))
self.__signatures__.generate_giveref(code) self.__signatures__.generate_giveref(code)
self.__signatures__.generate_post_assignment_code(code)
self.__signatures__.free_temps(code)
self.fused_func_assignment.generate_execution_code(code) self.fused_func_assignment.generate_execution_code(code)
# Dispose of results # Dispose of results
self.resulting_fused_function.generate_disposal_code(code) self.resulting_fused_function.generate_disposal_code(code)
self.resulting_fused_function.free_temps(code)
self.defaults_tuple.generate_disposal_code(code) self.defaults_tuple.generate_disposal_code(code)
self.defaults_tuple.free_temps(code)
self.code_object.generate_disposal_code(code) self.code_object.generate_disposal_code(code)
self.code_object.free_temps(code)
for default in self.defaults: for default in self.defaults:
if default is not None: if default is not None:
default.generate_disposal_code(code) default.generate_disposal_code(code)
default.free_temps(code)
def annotate(self, code): def annotate(self, code):
for stat in self.stats: for stat in self.stats:
......
...@@ -3413,6 +3413,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode): ...@@ -3413,6 +3413,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
module.qualified_name, module.qualified_name,
temp, temp,
code.error_goto(self.pos))) code.error_goto(self.pos)))
code.put_gotref(temp, py_object_type)
for entry in entries: for entry in entries:
if env is module: if env is module:
cname = entry.cname cname = entry.cname
...@@ -3423,7 +3424,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode): ...@@ -3423,7 +3424,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
'if (__Pyx_ImportVoidPtr(%s, "%s", (void **)&%s, "%s") < 0) %s' % ( 'if (__Pyx_ImportVoidPtr(%s, "%s", (void **)&%s, "%s") < 0) %s' % (
temp, entry.name, cname, signature, temp, entry.name, cname, signature,
code.error_goto(self.pos))) code.error_goto(self.pos)))
code.putln("Py_DECREF(%s); %s = 0;" % (temp, temp)) code.put_decref_clear(temp, py_object_type)
code.funcstate.release_temp(temp)
def generate_c_function_import_code_for_module(self, module, env, code): def generate_c_function_import_code_for_module(self, module, env, code):
# Generate import code for all exported C functions in a cimported module. # Generate import code for all exported C functions in a cimported module.
...@@ -3441,6 +3443,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode): ...@@ -3441,6 +3443,7 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
module.qualified_name, module.qualified_name,
temp, temp,
code.error_goto(self.pos))) code.error_goto(self.pos)))
code.put_gotref(temp, py_object_type)
for entry in entries: for entry in entries:
code.putln( code.putln(
'if (__Pyx_ImportFunction(%s, %s, (void (**)(void))&%s, "%s") < 0) %s' % ( 'if (__Pyx_ImportFunction(%s, %s, (void (**)(void))&%s, "%s") < 0) %s' % (
...@@ -3449,7 +3452,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode): ...@@ -3449,7 +3452,8 @@ class ModuleNode(Nodes.Node, Nodes.BlockNode):
entry.cname, entry.cname,
entry.type.signature_string(), entry.type.signature_string(),
code.error_goto(self.pos))) code.error_goto(self.pos)))
code.putln("Py_DECREF(%s); %s = 0;" % (temp, temp)) code.put_decref_clear(temp, py_object_type)
code.funcstate.release_temp(temp)
def generate_type_init_code(self, env, code): def generate_type_init_code(self, env, code):
# Generate type import code for extern extension types # Generate type import code for extern extension types
......
...@@ -5182,9 +5182,9 @@ class CClassDefNode(ClassDefNode): ...@@ -5182,9 +5182,9 @@ class CClassDefNode(ClassDefNode):
return return
if entry.visibility != 'extern': if entry.visibility != 'extern':
code.putln("#if CYTHON_COMPILING_IN_LIMITED_API") code.putln("#if CYTHON_COMPILING_IN_LIMITED_API")
tuple_temp = code.funcstate.allocate_temp(py_object_type, manage_ref=True)
base_type = scope.parent_type.base_type base_type = scope.parent_type.base_type
if base_type: if base_type:
tuple_temp = code.funcstate.allocate_temp(py_object_type, manage_ref=True)
code.putln( code.putln(
"%s = PyTuple_Pack(1, (PyObject *)%s); %s" % ( "%s = PyTuple_Pack(1, (PyObject *)%s); %s" % (
tuple_temp, tuple_temp,
...@@ -6238,6 +6238,7 @@ class DelStatNode(StatNode): ...@@ -6238,6 +6238,7 @@ class DelStatNode(StatNode):
arg.generate_evaluation_code(code) arg.generate_evaluation_code(code)
code.putln("delete %s;" % arg.result()) code.putln("delete %s;" % arg.result())
arg.generate_disposal_code(code) arg.generate_disposal_code(code)
arg.free_temps(code)
# else error reported earlier # else error reported earlier
def annotate(self, code): def annotate(self, code):
...@@ -6758,6 +6759,8 @@ class SwitchStatNode(StatNode): ...@@ -6758,6 +6759,8 @@ class SwitchStatNode(StatNode):
# generate the switch statement, so shouldn't be bothered). # generate the switch statement, so shouldn't be bothered).
code.putln("default: break;") code.putln("default: break;")
code.putln("}") code.putln("}")
self.test.generate_disposal_code(code)
self.test.free_temps(code)
def generate_function_definitions(self, env, code): def generate_function_definitions(self, env, code):
self.test.generate_function_definitions(env, code) self.test.generate_function_definitions(env, code)
...@@ -8044,9 +8047,10 @@ class TryFinallyStatNode(StatNode): ...@@ -8044,9 +8047,10 @@ class TryFinallyStatNode(StatNode):
if self.func_return_type.is_pyobject: if self.func_return_type.is_pyobject:
code.putln("%s = 0;" % ret_temp) code.putln("%s = 0;" % ret_temp)
code.funcstate.release_temp(ret_temp) code.funcstate.release_temp(ret_temp)
ret_temp = None
if self.in_generator: if self.in_generator:
self.put_error_uncatcher(code, exc_vars) self.put_error_uncatcher(code, exc_vars)
for cname in exc_vars:
code.funcstate.release_temp(cname)
if not self.finally_clause.is_terminator: if not self.finally_clause.is_terminator:
code.put_goto(old_label) code.put_goto(old_label)
...@@ -9177,6 +9181,11 @@ class ParallelStatNode(StatNode, ParallelNode): ...@@ -9177,6 +9181,11 @@ class ParallelStatNode(StatNode, ParallelNode):
self.begin_of_parallel_control_block_point = None self.begin_of_parallel_control_block_point = None
self.begin_of_parallel_control_block_point_after_decls = None self.begin_of_parallel_control_block_point_after_decls = None
if self.num_threads is not None:
# FIXME: is it the right place? should not normally produce code.
self.num_threads.generate_disposal_code(code)
self.num_threads.free_temps(code)
# Firstly, always prefer errors over returning, continue or break # Firstly, always prefer errors over returning, continue or break
if self.error_label_used: if self.error_label_used:
c.putln("const char *%s = NULL; int %s = 0, %s = 0;" % self.parallel_pos_info) c.putln("const char *%s = NULL; int %s = 0, %s = 0;" % self.parallel_pos_info)
...@@ -9557,7 +9566,7 @@ class ParallelRangeNode(ParallelStatNode): ...@@ -9557,7 +9566,7 @@ class ParallelRangeNode(ParallelStatNode):
# And finally, release our privates and write back any closure # And finally, release our privates and write back any closure
# variables # variables
for temp in start_stop_step + (self.chunksize, self.num_threads): for temp in start_stop_step + (self.chunksize,):
if temp is not None: if temp is not None:
temp.generate_disposal_code(code) temp.generate_disposal_code(code)
temp.free_temps(code) temp.free_temps(code)
......
...@@ -1777,6 +1777,8 @@ class EndToEndTest(unittest.TestCase): ...@@ -1777,6 +1777,8 @@ class EndToEndTest(unittest.TestCase):
cmd.append(command) cmd.append(command)
out.append(_out) out.append(_out)
err.append(_err) err.append(_err)
if res == 0 and b'REFNANNY: ' in _out:
res = -1
if res != 0: if res != 0:
for c, o, e in zip(cmd, out, err): for c, o, e in zip(cmd, out, err):
sys.stderr.write("%s\n%s\n%s\n\n" % ( sys.stderr.write("%s\n%s\n%s\n\n" % (
......
...@@ -212,6 +212,7 @@ cdef vector[vector_int_ptr] create_to_delete() except *: ...@@ -212,6 +212,7 @@ cdef vector[vector_int_ptr] create_to_delete() except *:
cdef int f(int x): cdef int f(int x):
return x return x
def test_nested_del(): def test_nested_del():
""" """
>>> test_nested_del() >>> test_nested_del()
...@@ -220,3 +221,15 @@ def test_nested_del(): ...@@ -220,3 +221,15 @@ def test_nested_del():
v.push_back(new vector[int]()) v.push_back(new vector[int]())
del v[0] del v[0]
del create_to_delete()[f(f(0))] del create_to_delete()[f(f(0))]
def test_nested_del_repeat():
"""
>>> test_nested_del_repeat()
"""
cdef vector[vector_int_ptr] v
v.push_back(new vector[int]())
del v[0]
del create_to_delete()[f(f(0))]
del create_to_delete()[f(f(0))]
del create_to_delete()[f(f(0))]
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment