Consider this code:
import numpy as np
import itertools
def get_view(arr):
view = arr.view()
view.flags.writeable = False # this line causes memory to leak?
return view
def main():
for _ in itertools.count():
get_view(np.zeros(1000))
if __name__ == "__main__":
main()
It seems the line setting the view to non-writeable causes a memory leak, although I don't know if it's bounded.
- Why does this happen?
- Is it guaranteed to be bounded? Or is this a numpy bug? Or maybe they are reference counted, but for some reason manually invoking the garbage collector does not collect them?
Here's the same program adorned with tracemalloc logic to print allocations every 100k calls to get_view.
import numpy as np
import tracemalloc
import itertools
import gc
def log_diff(snapshot, prev_snapshot):
diff = snapshot.compare_to(prev_snapshot, "lineno")
reported = 0
for stat in diff:
if "tracemalloc.py" in stat.traceback[0].filename:
continue
if stat.size_diff <= 0:
continue
print(f"#{reported}: {stat}")
reported += 1
print("---")
def get_view(arr):
view = arr.view()
view.flags.writeable = False # this line causes memory to leak?
return view
def main():
tracemalloc.start()
prev_snapshot = None
for i in itertools.count():
get_view(np.zeros(1000))
if i % 100000 == 0:
gc.collect(generation=2)
snapshot = tracemalloc.take_snapshot()
if prev_snapshot is not None:
log_diff(snapshot, prev_snapshot)
prev_snapshot = snapshot
if __name__ == "__main__":
main()
On Python 3.11.6 and numpy 1.26.4 on Linux, the number of allocations we get seems to be nondeterministic, but the largest I've seen it grow is around 250. It grows in the beginning, then much less rapidly later.
If I comment out the line assigning view.flags.writeable, the memory usage does not grow.
#0: /home/sami/bug.py:22: size=3534 B (+3477 B), count=62 (+61), average=57 B
#1: /home/sami/bug.py:29: size=84 B (+28 B), count=2 (+1), average=42 B
---
#0: /home/sami/bug.py:22: size=5871 B (+2337 B), count=103 (+41), average=57 B
#1: /home/sami/bug.py:15: size=72 B (+72 B), count=1 (+1), average=72 B
---
---
#0: /home/sami/bug.py:22: size=6270 B (+399 B), count=110 (+7), average=57 B
---
#0: /home/sami/bug.py:22: size=6327 B (+57 B), count=111 (+1), average=57 B
---
#0: /home/sami/bug.py:22: size=7638 B (+1311 B), count=134 (+23), average=57 B
---
#0: /home/sami/bug.py:22: size=7809 B (+171 B), count=137 (+3), average=57 B
---
---
#0: /home/sami/bug.py:22: size=8436 B (+627 B), count=148 (+11), average=57 B
---
#0: /home/sami/bug.py:22: size=8664 B (+228 B), count=152 (+4), average=57 B
---
#0: /home/sami/bug.py:22: size=8892 B (+228 B), count=156 (+4), average=57 B
---
---
#0: /home/sami/bug.py:22: size=9120 B (+228 B), count=160 (+4), average=57 B
---
---
#0: /home/sami/bug.py:22: size=9177 B (+114 B), count=161 (+2), average=57 B
---
...
I'm not sure if this is a memory leak, but I can give you an equivalent that allocates no memory:
Running this under tracemalloc shows that it does not allocate memory on this line.