Skip to content

Commit eb377ee

Browse files
Improve homogeneous containers allocation scheme. (pyccel#2007)
The goal of this PR is to resolve pyccel#1979 and to systematically address memory allocation for STC containers. This PR aims to use one of the following to address allocation of STC containers: - use STC `init` functions to allocate containers initialized with literal lists, sets, or dictionaries. - use `reserve` on STC containers when the capacity is known at compile time and the container will be populated with append/add methods. - use `resize` on STC containers when elements of the container will be accessed through their indices in the code. Care is taken to drop containers before they are reassigned or to reuse them whenever possible. --------- Co-authored-by: Emily Bourne <[email protected]>
1 parent 9d047d6 commit eb377ee

File tree

7 files changed

+132
-5
lines changed

7 files changed

+132
-5
lines changed

.github/actions/valgrind_run/action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,10 @@ runs:
99
valgrind --leak-check=full --error-exitcode=1 ./leaks_check
1010
shell: bash
1111
working-directory: ./tests/ndarrays
12+
13+
- name: Test with valgrind for memory leaks in stc_containers
14+
run: |
15+
pyccel --language=c --flags="-g -O0" leaks_check.py
16+
valgrind --leak-check=full --error-exitcode=1 ./leaks_check
17+
shell: bash
18+
working-directory: ./tests/stc_containers

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ All notable changes to this project will be documented in this file.
6868
- #1913 : Fix function calls to renamed functions.
6969
- #1930 : Preserve ordering of import targets.
7070
- #1892 : Fix implementation of list function when an iterable is passed as parameter.
71+
- #1979 : Fix memory leaks in C due to homogeneous container redefinition.
7172
- #1972 : Simplified `printf` statement for Literal String.
7273
- #2026 : Fix missing loop in slice assignment.
7374

ci_tools/check_pylint_commands.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
re.compile('tests/semantic/scripts/expressions.py'):['unused-variable'],
2222
re.compile('tests/semantic/scripts/calls.py'):['unused-variable'],
2323
re.compile('tests/pyccel/project_class_imports/.*'):['relative-beyond-top-level'], # ignore Codacy bad pylint call
24-
re.compile('tests/errors/syntax_errors/import_star.py'):['wildcard-import']
24+
re.compile('tests/errors/syntax_errors/import_star.py'):['wildcard-import'],
25+
re.compile('tests/stc_containers/leaks_check.py'):['unused-variable'],
2526
}
2627

2728
def run_pylint(file, flag, messages):

pyccel/ast/core.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
from .variable import DottedName, IndexedElement
3636
from .variable import Variable, AnnotatedPyccelSymbol
37+
from .datatypes import HomogeneousSetType, HomogeneousListType, DictType
3738

3839
errors = Errors()
3940
pyccel_stage = PyccelStage()
@@ -398,16 +399,25 @@ class Allocate(PyccelAstNode):
398399
In C this provides the size which will be passed to malloc. In Fortran
399400
this provides the source argument of the allocate function.
400401
402+
alloc_type : str {'init'|'reserve'|'resize'}, optional
403+
Specifies the memory allocation strategy for containers with dynamic memory management.
404+
This parameter is relevant for any container type where memory allocation patterns
405+
need to be specified based on usage.
406+
407+
- 'init' refers to direct allocation with predefined data (e.g., `x = [1, 2, 4]`).
408+
- 'reserve' refers to cases where the container will be appended to.
409+
- 'resize' referes to cases where the container is populated via indexed elements.
410+
401411
Notes
402412
-----
403413
An object of this class is immutable, although it contains a reference to a
404414
mutable Variable object.
405415
"""
406-
__slots__ = ('_variable', '_shape', '_order', '_status', '_like')
416+
__slots__ = ('_variable', '_shape', '_order', '_status', '_like', '_alloc_type')
407417
_attribute_nodes = ('_variable', '_like')
408418

409419
# ...
410-
def __init__(self, variable, *, shape, status, like = None):
420+
def __init__(self, variable, *, shape, status, like = None, alloc_type = None):
411421

412422
if not isinstance(variable, (Variable, PointerCast)):
413423
raise TypeError(f"Can only allocate a 'Variable' object, got {type(variable)} instead")
@@ -429,11 +439,15 @@ def __init__(self, variable, *, shape, status, like = None):
429439
if status not in ('allocated', 'unallocated', 'unknown'):
430440
raise ValueError(f"Value of 'status' not allowed: '{status}'")
431441

442+
assert alloc_type is None or isinstance(variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType))
443+
assert alloc_type is None or alloc_type in ('init', 'reserve', 'resize')
444+
432445
self._variable = variable
433446
self._shape = shape
434447
self._order = variable.order
435448
self._status = status
436449
self._like = like
450+
self._alloc_type = alloc_type
437451
super().__init__()
438452
# ...
439453

@@ -485,6 +499,18 @@ def like(self):
485499
"""
486500
return self._like
487501

502+
@property
503+
def alloc_type(self):
504+
"""
505+
Determines the allocation type for homogeneous containers.
506+
507+
Returns a string that indicates the allocation type used for memory allocation.
508+
The value is either 'init' for containers initialized with predefined data,
509+
'reserve' for containers populated through appending, and 'resize' for containers
510+
populated through indexed element assignment.
511+
"""
512+
return self._alloc_type
513+
488514
def __str__(self):
489515
return f'Allocate({self.variable}, shape={self.shape}, order={self.order}, status={self.status})'
490516

pyccel/codegen/printing/ccode.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,9 @@ def _print_Declare(self, expr):
14261426
assert init == ''
14271427
preface = ''
14281428
init = ' = {.shape = NULL}'
1429+
elif isinstance(var.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
1430+
preface = ''
1431+
init = ' = {0}'
14291432
else:
14301433
preface = ''
14311434

@@ -1688,7 +1691,18 @@ def _print_Allocate(self, expr):
16881691
free_code = ''
16891692
variable = expr.variable
16901693
if isinstance(variable.class_type, (HomogeneousListType, HomogeneousSetType, DictType)):
1691-
return ''
1694+
if expr.status in ('allocated', 'unknown'):
1695+
free_code = f'{self._print(Deallocate(variable))}\n'
1696+
if expr.shape[0] is None:
1697+
return free_code
1698+
size = self._print(expr.shape[0])
1699+
variable_address = self._print(ObjectAddress(expr.variable))
1700+
container_type = self.get_c_type(expr.variable.class_type)
1701+
if expr.alloc_type == 'reserve':
1702+
return free_code + f'{container_type}_reserve({variable_address}, {size});\n'
1703+
elif expr.alloc_type == 'resize':
1704+
return f'{container_type}_resize({variable_address}, {size}, {0});\n'
1705+
return free_code
16921706
if isinstance(variable.class_type, (NumpyNDArrayType, HomogeneousTupleType)):
16931707
#free the array if its already allocated and checking if its not null if the status is unknown
16941708
if (expr.status == 'unknown'):

pyccel/parser/semantic.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1570,6 +1570,14 @@ def _assign_lhs_variable(self, lhs, d_var, rhs, new_expressions, is_augassign =
15701570
shape=a.alloc_shape, status=status))
15711571
args = new_args
15721572
new_args = []
1573+
elif isinstance(lhs.class_type, (HomogeneousListType, HomogeneousSetType,DictType)):
1574+
if isinstance(rhs, (PythonList, PythonDict, PythonSet, FunctionCall)):
1575+
alloc_type = 'init'
1576+
elif isinstance(rhs, IndexedElement) or rhs.get_attribute_nodes(IndexedElement):
1577+
alloc_type = 'resize'
1578+
else:
1579+
alloc_type = 'reserve'
1580+
new_expressions.append(Allocate(lhs, shape=lhs.alloc_shape, status=status, alloc_type=alloc_type))
15731581
else:
15741582
new_expressions.append(Allocate(lhs, shape=lhs.alloc_shape, status=status))
15751583
# ...
@@ -1738,6 +1746,14 @@ def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_auga
17381746
self.current_ast_node.col_offset))
17391747

17401748
else:
1749+
alloc_type = None
1750+
if isinstance(var.class_type, (HomogeneousListType, HomogeneousSetType,DictType)):
1751+
if isinstance(rhs, (PythonList, PythonDict, PythonSet, FunctionCall)):
1752+
alloc_type = 'init'
1753+
elif isinstance(rhs, IndexedElement) or rhs.get_attribute_nodes(IndexedElement):
1754+
alloc_type = 'resize'
1755+
else:
1756+
alloc_type = 'reserve'
17411757
if previous_allocations:
17421758
var.set_changeable_shape()
17431759
last_allocation = previous_allocations[-1]
@@ -1762,7 +1778,7 @@ def _ensure_inferred_type_matches_existing(self, class_type, d_var, var, is_auga
17621778
else:
17631779
status = 'unallocated'
17641780

1765-
new_expressions.append(Allocate(var, shape=d_var['shape'], status=status))
1781+
new_expressions.append(Allocate(var, shape=d_var['shape'], status=status, alloc_type=alloc_type))
17661782

17671783
if status == 'unallocated':
17681784
self._allocs[-1].add(var)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
2+
# pylint: disable=missing-function-docstring, missing-module-docstring, unused-variable
3+
def create_list():
4+
a = [1,2,3]
5+
6+
def create_set():
7+
a = {1,2,3}
8+
9+
def create_dict():
10+
a = {1:1,2:2,3:3}
11+
12+
def list_reassign():
13+
a = [1,2,3]
14+
a = [1,2,3,4]
15+
a = [2 * i for i in range(15)]
16+
a = [1,2]
17+
18+
def set_reassign():
19+
a = {1,2,3}
20+
a = {1,2}
21+
a = {1,2,3,4}
22+
a = {1,2,3,4,5}
23+
24+
def dict_reassign():
25+
a = {1:1,2:2,3:3}
26+
a = {1:1,2:2,3:3,4:4}
27+
a = {1:1,2:2}
28+
a = {1:1,2:2,3:3,4:4,5:5}
29+
30+
def conditional_list(b1: bool):
31+
if (b1):
32+
a = [1,2,3]
33+
a = [1,2,3,4]
34+
35+
def conditional_set(b1: bool):
36+
if (b1):
37+
a = {1,2,3}
38+
a = {1,2,3,4}
39+
40+
def conditional_dict(b1: bool):
41+
if (b1):
42+
a = {1:1,2:2,3:3}
43+
a = {1:1,2:2,3:3,4:4,5:5}
44+
45+
def slice_assign():
46+
a = [1,2,3,4]
47+
b = a[1:-1]
48+
49+
if __name__ == '__main__':
50+
create_list()
51+
create_set()
52+
create_dict()
53+
list_reassign()
54+
set_reassign()
55+
dict_reassign()
56+
conditional_list(True)
57+
conditional_set(True)
58+
conditional_dict(True)
59+
conditional_list(False)
60+
conditional_set(False)
61+
conditional_dict(False)
62+
slice_assign()

0 commit comments

Comments
 (0)