Why does setting flags on an NDArray view result in allocations? Are they guaranteed to be bounded?

33 Views Asked by At

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.

  1. Why does this happen?
  2. 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
---
...
1

There are 1 best solutions below

0
Nick ODell On

I'm not sure if this is a memory leak, but I can give you an equivalent that allocates no memory:

view.setflags(write=False)

Running this under tracemalloc shows that it does not allocate memory on this line.