Repository: numba/numba

Issue:
Memory leak when using enumerate in a jit function
<!--

Thanks for opening an issue! To help the Numba team handle your information
efficiently, please first ensure that there is no other issue present that
already describes the issue you have
(search at https://github.com/numba/numba/issues?&q=is%3Aissue).

For more general "how do I do X?" type questions, please speak to us in real
time on https://gitter.im/numba/numba or post to the Numba mailing list
https://groups.google.com/a/continuum.io/forum/#!forum/numba-users.

-->

## Reporting a bug

<!--

Before submitting a bug report please ensure that you can check off these boxes:

-->

- [X] I am using the latest released version of Numba (most recent is visible in
 the change log (https://github.com/numba/numba/blob/master/CHANGE_LOG).
- [X] I have included below a minimal working reproducer (if you are unsure how
 to write one see http://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports).

<!--

Please include details of the bug here, including, if applicable, what you
expected to happen!

-->

There seem to be a memory leak caused by the use of `enumerate` in a `jit` function.

The following script (requires the `psutil` package.) prints the memory usage at each iteration and it's clearly increasing.

```python
import psutil
from numba import (jit, jitclass, uint32)

@jitclass([
    ('attr', uint32),
])
class JitClass:
    def __init__(self):
        pass


@jit  # same with njit
def f():
    cs = [JitClass() for i in range(1000)]

    # leak
    for i, c in enumerate(cs):
        c.attr = 1234

    # No enumerate, no leak
    # for c in cs:
    #     c.attr = 1234

    
p = psutil.Process()
for _ in range(10000):
    f()
    # leak proportional to the size of cs
    print(f"memory used by program: {p.memory_info().rss / 1e6} MB")
```

This seems to be coupled with the list comprehension because the following doesn't leak:

```
@jit
def f():
    c = JitClass()
    for i, j in enumerate(range(10)):
        c.attr = 1234
```

Not sure if this is related but using list comprehensions has some weird side effect (I also opened #3472)


Discussion:
Update:

Here is a leaking log (shortened):

<details>

```
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.402816 MB
memory used by program: 90.66496 MB
memory used by program: 90.66496 MB
memory used by program: 90.66496 MB
memory used by program: 90.66496 MB
memory used by program: 90.66496 MB
memory used by program: 90.935296 MB
memory used by program: 90.935296 MB
memory used by program: 90.935296 MB
memory used by program: 90.935296 MB
memory used by program: 91.205632 MB
memory used by program: 91.205632 MB
memory used by program: 91.205632 MB
memory used by program: 91.205632 MB
memory used by program: 91.475968 MB
memory used by program: 91.475968 MB
memory used by program: 91.475968 MB
memory used by program: 91.475968 MB
memory used by program: 91.746304 MB
memory used by program: 91.746304 MB
memory used by program: 91.746304 MB
memory used by program: 91.746304 MB
memory used by program: 92.01664 MB
memory used by program: 92.01664 MB
memory used by program: 92.01664 MB
memory used by program: 92.01664 MB
memory used by program: 92.01664 MB
memory used by program: 92.286976 MB
memory used by program: 92.286976 MB
memory used by program: 92.286976 MB
memory used by program: 92.286976 MB
memory used by program: 92.557312 MB
memory used by program: 92.557312 MB
memory used by program: 92.557312 MB
memory used by program: 92.557312 MB
memory used by program: 92.827648 MB
memory used by program: 92.827648 MB
memory used by program: 92.827648 MB
memory used by program: 92.827648 MB
memory used by program: 93.097984 MB
memory used by program: 93.097984 MB
memory used by program: 93.097984 MB
memory used by program: 93.097984 MB
memory used by program: 93.36832 MB
memory used by program: 93.36832 MB
memory used by program: 93.36832 MB
memory used by program: 93.36832 MB
memory used by program: 93.36832 MB
memory used by program: 93.638656 MB
memory used by program: 93.638656 MB
memory used by program: 93.638656 MB
memory used by program: 93.638656 MB
memory used by program: 93.908992 MB
memory used by program: 93.908992 MB
memory used by program: 93.908992 MB
memory used by program: 93.908992 MB
memory used by program: 94.179328 MB
memory used by program: 94.179328 MB
memory used by program: 94.179328 MB
memory used by program: 94.179328 MB
memory used by program: 94.449664 MB
memory used by program: 94.449664 MB
memory used by program: 94.449664 MB
memory used by program: 94.449664 MB
memory used by program: 94.72 MB
memory used by program: 94.72 MB
memory used by program: 94.72 MB
memory used by program: 94.72 MB
memory used by program: 94.72 MB
memory used by program: 94.990336 MB
memory used by program: 94.990336 MB
memory used by program: 94.990336 MB
memory used by program: 94.990336 MB
memory used by program: 95.260672 MB
memory used by program: 95.260672 MB
memory used by program: 95.260672 MB
memory used by program: 95.260672 MB
memory used by program: 95.531008 MB
memory used by program: 95.531008 MB
memory used by program: 95.531008 MB
memory used by program: 95.531008 MB
memory used by program: 95.801344 MB
memory used by program: 95.801344 MB
memory used by program: 95.801344 MB
memory used by program: 95.801344 MB
memory used by program: 96.07168 MB
memory used by program: 96.07168 MB
memory used by program: 96.07168 MB
memory used by program: 96.07168 MB
memory used by program: 96.07168 MB
memory used by program: 96.342016 MB
memory used by program: 96.342016 MB
memory used by program: 96.342016 MB
memory used by program: 96.342016 MB
memory used by program: 96.612352 MB
memory used by program: 96.612352 MB
memory used by program: 96.612352 MB
memory used by program: 96.612352 MB
memory used by program: 96.882688 MB
memory used by program: 96.882688 MB
memory used by program: 96.882688 MB
memory used by program: 96.882688 MB
memory used by program: 97.153024 MB
memory used by program: 97.153024 MB
memory used by program: 97.153024 MB
memory used by program: 97.153024 MB
memory used by program: 97.42336 MB
memory used by program: 97.42336 MB
memory used by program: 97.42336 MB
memory used by program: 97.42336 MB
memory used by program: 97.42336 MB
memory used by program: 97.693696 MB
memory used by program: 97.693696 MB
memory used by program: 97.693696 MB
memory used by program: 97.693696 MB
memory used by program: 97.964032 MB
memory used by program: 97.964032 MB
memory used by program: 97.964032 MB
memory used by program: 97.964032 MB
memory used by program: 98.234368 MB
memory used by program: 98.234368 MB
memory used by program: 98.234368 MB
memory used by program: 98.234368 MB
memory used by program: 98.504704 MB
memory used by program: 98.504704 MB
memory used by program: 98.504704 MB
memory used by program: 98.504704 MB
memory used by program: 98.77504 MB
memory used by program: 98.77504 MB
memory used by program: 98.77504 MB
memory used by program: 98.77504 MB
memory used by program: 98.77504 MB
memory used by program: 99.045376 MB
memory used by program: 99.045376 MB
memory used by program: 99.045376 MB

...
...
...

keeps going up
```
</details>

And here is a non-leaking log (shortened)

<details>

```
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
...
...
...
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
memory used by program: 89.841664 MB
```
</details>
Thanks for the report. The Numba runtime (NRT) is leaking.
```python
import psutil
from numba import (jit, jitclass, uint32)
import numba

rtsys = numba.runtime.rtsys

@jitclass([
    ('attr', uint32),
])
class JitClass:
    def __init__(self):
        pass


@jit  # same with njit
def f():
    cs = [JitClass() for i in range(1000)]

    # leak
    for i, c in enumerate(cs):
        c.attr = 1234

    #No enumerate, no leak
    #for c in cs:
        #c.attr = 1234

p = psutil.Process()
for _ in range(3):
    print('-' * 80)
    print(rtsys.get_allocation_stats())
    f()
    print(rtsys.get_allocation_stats())
    # leak proportional to the size of cs
    print(f"memory used by program: {p.memory_info().rss / 1e6} MB")
```
the leaking code gives:
```
--------------------------------------------------------------------------------
nrt_mstats(alloc=0, free=0, mi_alloc=0, mi_free=0)
nrt_mstats(alloc=1002, free=2, mi_alloc=1001, mi_free=1)
memory used by program: 77.238272 MB
--------------------------------------------------------------------------------
nrt_mstats(alloc=1002, free=2, mi_alloc=1001, mi_free=1)
nrt_mstats(alloc=2004, free=4, mi_alloc=2002, mi_free=2)
memory used by program: 77.238272 MB
--------------------------------------------------------------------------------
nrt_mstats(alloc=2004, free=4, mi_alloc=2002, mi_free=2)
nrt_mstats(alloc=3006, free=6, mi_alloc=3003, mi_free=3)
memory used by program: 77.238272 MB
```
whereas the non-leaking gives:
```
--------------------------------------------------------------------------------
nrt_mstats(alloc=0, free=0, mi_alloc=0, mi_free=0)
nrt_mstats(alloc=1002, free=1002, mi_alloc=1001, mi_free=1001)
memory used by program: 76.439552 MB
--------------------------------------------------------------------------------
nrt_mstats(alloc=1002, free=1002, mi_alloc=1001, mi_free=1001)
nrt_mstats(alloc=2004, free=2004, mi_alloc=2002, mi_free=2002)
memory used by program: 76.439552 MB
--------------------------------------------------------------------------------
nrt_mstats(alloc=2004, free=2004, mi_alloc=2002, mi_free=2002)
nrt_mstats(alloc=3006, free=3006, mi_alloc=3003, mi_free=3003)
memory used by program: 76.439552 MB
```

Just noting that you don't need `@jitclass` to trigger this. For example, the test below shows that numba leaks 700MB per call because I pass it an array of that size packed within a tuple:

```
@nb.njit(nogil=True)
def numba_fn(outs):
    for out in outs:
        out[1337, 1377] = 1

    # does not leak
    #for i in range(len(outs)):
    #    out[i][1337, 1377] = 1

def test_numba():
    T = 10000
    C = 10000
    O = 1

    def _call():
        outs = tuple(np.full((T, C), np.nan, dtype='f8') for _ in range(O))
        numba_fn(outs)

    import psutil
    import gc

    def _measure():
        gc.collect()
        return psutil.Process().memory_info().rss / (1024*1024)

    first_usage = _measure()
    prev_usage = first_usage
    for _ in range(20):
        _call()

        last_usage = _measure()
        print(last_usage - prev_usage)
        prev_usage = last_usage

    assert (prev_usage - first_usage) < 10
```
If you look at the LLVM in e.g. the `O=2` case it's pretty clear what is going on. In the `switch.end` block below, we `NRT_incref` on `%extracted.meminfo.16`, then branch to the `%B6.endif` block. This block then `NRT_incref` on `%.92.sroa.0.1`, which will be exactly `%extracted.meminfo.16`. This double `incref` is not matched by a double `decref` so we leak.

I don't see any obvious issue in the corresponding IR but I might be missing something:

```
label 0:
    outs = arg(0, name=outs)                 ['outs']
    jump 2                                   []
label 2:
    $2.2 = getiter(value=outs)               ['$2.2', 'outs']
    del outs                                 []
    $phi6.1 = $2.2                           ['$2.2', '$phi6.1']
    del $2.2                                 []
    jump 6                                   []
label 6:
    $6.2 = iternext(value=$phi6.1)           ['$6.2', '$phi6.1']
    $6.3 = pair_first(value=$6.2)            ['$6.2', '$6.3']
    $6.4 = pair_second(value=$6.2)           ['$6.2', '$6.4']
    del $6.2                                 []
    $phi8.1 = $6.3                           ['$6.3', '$phi8.1']
    $phi20.1 = $6.3                          ['$6.3', '$phi20.1']
    del $phi20.1                             []
    del $6.3                                 []
    $phi20.2 = $phi6.1                       ['$phi20.2', '$phi6.1']
    del $phi20.2                             []
    branch $6.4, 8, 20                       ['$6.4']
label 8:
    del $6.4                                 []
    out = $phi8.1                            ['$phi8.1', 'out']
    del $phi8.1                              []
    $const8.2 = const(int, 1)                ['$const8.2']
    $const8.4 = const(tuple, (1337, 1377))   ['$const8.4']
    out[(1337, 1377)] = $const8.2            ['$const8.2', '$const8.4', 'out']
    del out                                  []
    del $const8.4                            []
    del $const8.2                            []
    jump 6                                   []
label 20:
    del $phi8.1                              []
    del $phi6.1                              []
    del $6.4                                 []
    jump 22                                  []
label 22:
    $const22.1 = const(NoneType, None)       ['$const22.1']
    $22.2 = cast(value=$const22.1)           ['$22.2', '$const22.1']
    del $const22.1                           []
    return $22.2                             ['$22.2']
```

```
define i32 @"_ZN8__main__12numba_fn$244E8UniTupleI5ArrayIdLi2E1C7mutable7alignedELi2EE"(i8** noalias nocapture %retptr, { i8*, i32 }** noalias nocapture %excinfo, i8* noalias nocapture readnone %env, i8* %arg.outs.0.0, i8* %arg.outs.0.1, i64 %arg.outs.0.2, i64 %arg.outs.0.3, double* %arg.outs.0.4, i64 %arg.outs.0.5.0, i64 %arg.outs.0.5.1, i64 %arg.outs.0.6.0, i64 %arg.outs.0.6.1, i8* %arg.outs.1.0, i8* %arg.outs.1.1, i64 %arg.outs.1.2, i64 %arg.outs.1.3, double* %arg.outs.1.4, i64 %arg.outs.1.5.0, i64 %arg.outs.1.5.1, i64 %arg.outs.1.6.0, i64 %arg.outs.1.6.1) local_unnamed_addr {
entry:
  %.48 = alloca i64, align 8
  tail call void @NRT_incref(i8* %arg.outs.0.0)
  tail call void @NRT_incref(i8* %arg.outs.1.0)
  store i64 0, i64* %.48, align 8
  %0 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } undef, i8* %arg.outs.1.0, 0
  %1 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %0, i8* %arg.outs.1.1, 1
  %2 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %1, i64 %arg.outs.1.2, 2
  %3 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %2, i64 %arg.outs.1.3, 3
  %4 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %3, double* %arg.outs.1.4, 4
  %5 = insertvalue [2 x i64] undef, i64 %arg.outs.1.5.0, 0
  %.98.unpack161.unpack172181 = insertvalue [2 x i64] %5, i64 %arg.outs.1.5.1, 1
  %6 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %4, [2 x i64] %.98.unpack161.unpack172181, 5
  %7 = insertvalue [2 x i64] undef, i64 %arg.outs.1.6.0, 0
  %.98.unpack161.unpack174178 = insertvalue [2 x i64] %7, i64 %arg.outs.1.6.1, 1
  %.98.unpack161175 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %6, [2 x i64] %.98.unpack161.unpack174178, 6
  %8 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } undef, i8* %arg.outs.0.0, 0
  %9 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %8, i8* %arg.outs.0.1, 1
  %10 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %9, i64 %arg.outs.0.2, 2
  %11 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %10, i64 %arg.outs.0.3, 3
  %12 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %11, double* %arg.outs.0.4, 4
  %13 = insertvalue [2 x i64] undef, i64 %arg.outs.0.5.0, 0
  %.98.unpack.unpack191200 = insertvalue [2 x i64] %13, i64 %arg.outs.0.5.1, 1
  %14 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %12, [2 x i64] %.98.unpack.unpack191200, 5
  %15 = insertvalue [2 x i64] undef, i64 %arg.outs.0.6.0, 0
  %.98.unpack.unpack193197 = insertvalue [2 x i64] %15, i64 %arg.outs.0.6.1, 1
  %.98.unpack194 = insertvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %14, [2 x i64] %.98.unpack.unpack193197, 6
  br label %B6

B6:                                               ; preds = %B8, %entry
  %.101 = phi i64 [ 0, %entry ], [ %.101.pre, %B8 ]
  %.92.sroa.18.0 = phi i64 [ 0, %entry ], [ %.92.sroa.18.1, %B8 ]
  %.92.sroa.12.0 = phi double* [ null, %entry ], [ %.92.sroa.12.1, %B8 ]
  %.92.sroa.0.0 = phi i8* [ null, %entry ], [ %.92.sroa.0.1, %B8 ]
  %.102 = icmp slt i64 %.101, 2
  br i1 %.102, label %B6.if, label %B6.endif

B8:                                               ; preds = %B6.endif
  %.249 = mul i64 %.92.sroa.18.1, 1337
  %.252 = add i64 %.249, 1377
  %.253 = getelementptr double, double* %.92.sroa.12.1, i64 %.252
  store double 1.000000e+00, double* %.253, align 8
  tail call void @NRT_decref(i8* %.92.sroa.0.1)
  %.101.pre = load i64, i64* %.48, align 8
  br label %B6

B20:                                              ; preds = %B6.endif
  tail call void @NRT_decref(i8* %.92.sroa.0.1)
  tail call void @NRT_decref(i8* %arg.outs.0.0)
  tail call void @NRT_decref(i8* %arg.outs.1.0)
  store i8* null, i8** %retptr, align 8
  ret i32 0

B6.if:                                            ; preds = %B6
  switch i64 %.101, label %switch.else [
    i64 0, label %switch.end
    i64 1, label %switch.1
  ]

B6.endif:                                         ; preds = %switch.end, %B6
  %.92.sroa.18.1 = phi i64 [ %.109.elt253.elt259, %switch.end ], [ %.92.sroa.18.0, %B6 ]
  %.92.sroa.12.1 = phi double* [ %.109.elt251, %switch.end ], [ %.92.sroa.12.0, %B6 ]
  %.92.sroa.0.1 = phi i8* [ %extracted.meminfo.16, %switch.end ], [ %.92.sroa.0.0, %B6 ]
  %16 = icmp slt i64 %.101, 2
  tail call void @NRT_incref(i8* %.92.sroa.0.1)
  br i1 %16, label %B8, label %B20

switch.else:                                      ; preds = %B6.if
  store { i8*, i32 }* @.const.picklebuf.216662378568, { i8*, i32 }** %excinfo, align 8
  ret i32 1

switch.end:                                       ; preds = %switch.1, %B6.if
  %.109 = phi { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } [ %.98.unpack161175, %switch.1 ], [ %.98.unpack194, %B6.if ]
  %extracted.meminfo.16 = extractvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %.109, 0
  tail call void @NRT_incref(i8* %extracted.meminfo.16)
  %.109.elt251 = extractvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %.109, 4
  %.109.elt253 = extractvalue { i8*, i8*, i64, i64, double*, [2 x i64], [2 x i64] } %.109, 5
  %.109.elt253.elt259 = extractvalue [2 x i64] %.109.elt253, 1
  %.117 = add i64 %.101, 1
  store i64 %.117, i64* %.48, align 8
  br label %B6.endif

switch.1:                                         ; preds = %B6.if
  br label %switch.end
}
```
This makes me wonder if this is an error in the refcount pruning pass, which tries to remove unnecessary/redundant incref/decref pairs, as they significantly reduce performance.
Good thought, but disabling the call to `remove_unnecessary_nrt_usage` in `lowering.py` doesn't seem to help. I see some similar code in `targets/cpu.py`, but by adding a `raise ValueError()` to `remove_refct_pairs` it seems that it's not actually called in my situation (and I can't see any callsite for this function anyway? Not sure what's up with that.).
I think I see the problem. The issue is that `iternext_impl` uses `impl_ret_borrowed` on the pair returned by `iternext`, which bumps the ref count of the value being yielded (https://github.com/numba/numba/blob/master/numba/targets/imputils.py#L312). However, the value stored in that pair has actually already had its ref count bumped by `iternext_unituple` because `getitem_unituple` returns a ref-bumped object (https://github.com/numba/numba/blob/master/numba/targets/tupleobj.py#L191).

My understanding of the code is that the argument to `yield_` is intended to be ref-counted, which implies that `iternext_impl` is at fault and `iternext_unituple` is doing the right thing. If I change `iternext_impl` to use `impl_ret_new_ref` instead then the leak goes away. Didn't run the test suite though, so it's entirely possible that this breaks something else.
Excellent hunting!  If you want to check this on more platforms, feel free to open a work-in-progress PR, which will trigger CI to run the full test suite on Win/Mac/Linux.

Resolving patch:
diff --git a/numba/cgutils.py b/numba/cgutils.py
--- a/numba/cgutils.py
+++ b/numba/cgutils.py
@@ -262,7 +262,7 @@ def __init__(self, context, builder, value=None, ref=None, cast_ref=False):
         self._context = context
         self._builder = builder
         if ref is None:
-            self._value = alloca_once(builder, self._type)
+            self._value = alloca_once(builder, self._type, zfill=True)
             if value is not None:
                 assert not is_pointer(value.type)
                 assert value.type == self._type, (value.type, self._type)
@@ -369,9 +369,13 @@ def alloca_once(builder, ty, size=None, name='', zfill=False):
         size = ir.Constant(intp_t, size)
     with builder.goto_entry_block():
         ptr = builder.alloca(ty, size=size, name=name)
+        # Zero-fill at the init-site
         if zfill:
             builder.store(ty(None), ptr)
-        return ptr
+    # Also zero-fill at the use-site
+    if zfill:
+        builder.store(ty(None), ptr)
+    return ptr
 
 
 def alloca_once_value(builder, value, name=''):
diff --git a/numba/targets/iterators.py b/numba/targets/iterators.py
--- a/numba/targets/iterators.py
+++ b/numba/targets/iterators.py
@@ -61,6 +61,10 @@ def iternext_enumerate(context, builder, sig, args, result):
 
     with builder.if_then(is_valid):
         srcval = srcres.yielded_value()
+        # As a iternext_impl function, this will incref the yieled value.
+        # We need to release the new reference from call_iternext.
+        if context.enable_nrt:
+            context.nrt.decref(builder, enumty.yield_type[1], srcval)
         result.yield_(context.make_tuple(builder, enumty.yield_type,
                                          [count, srcval]))
 
diff --git a/numba/targets/tupleobj.py b/numba/targets/tupleobj.py
--- a/numba/targets/tupleobj.py
+++ b/numba/targets/tupleobj.py
@@ -188,6 +188,10 @@ def iternext_unituple(context, builder, sig, args, result):
                                        types.intp)
         getitem_out = getitem_unituple(context, builder, getitem_sig,
                                        [tup, idx])
+        # As a iternext_impl function, this will incref the yieled value.
+        # We need to release the new reference from getitem_unituple.
+        if context.enable_nrt:
+            context.nrt.decref(builder, tupiterty.container.dtype, getitem_out)
         result.yield_(getitem_out)
         nidx = builder.add(idx, context.get_constant(types.intp, 1))
         builder.store(nidx, iterval.index)


Test patch:
diff --git a/numba/tests/test_iteration.py b/numba/tests/test_iteration.py
--- a/numba/tests/test_iteration.py
+++ b/numba/tests/test_iteration.py
@@ -5,7 +5,7 @@
 import numba.unittest_support as unittest
 from numba.compiler import compile_isolated, Flags
 from numba import numpy_support, types
-from .support import TestCase, tag
+from .support import TestCase, MemoryLeakMixin, tag
 
 enable_pyobj_flags = Flags()
 enable_pyobj_flags.set("enable_pyobject")
@@ -53,6 +53,15 @@ def nested_enumerate_usecase():
         res = res * 2
     return res
 
+
+def enumerate_array_usecase():
+    res = 0
+    arrays = (np.ones(4), np.ones(5))
+    for i, v in enumerate(arrays):
+        res += v.sum()
+    return res
+
+
 def scalar_iter_usecase(iterable):
     res = 0.0
     for x in iterable:
@@ -75,7 +84,7 @@ def record_iter_mutate_usecase(iterable):
                          ])
 
 
-class IterationTest(TestCase):
+class IterationTest(MemoryLeakMixin, TestCase):
 
     def run_nullary_func(self, pyfunc, flags):
         cr = compile_isolated(pyfunc, (), flags=flags)
@@ -120,6 +129,14 @@ def test_nested_enumerate(self, flags=force_pyobj_flags):
     def test_nested_enumerate_npm(self):
         self.test_nested_enumerate(flags=no_pyobj_flags)
 
+    def test_enumerate_refct(self):
+        # Test issue 3473
+        pyfunc = enumerate_array_usecase
+        cr = compile_isolated(pyfunc, ())
+        cfunc = cr.entry_point
+        expected = pyfunc()
+        self.assertPreciseEqual(cfunc(), expected)
+
     def run_array_1d(self, item_type, arg, flags):
         # Iteration over a 1d numpy array
         pyfunc = scalar_iter_usecase
@@ -193,6 +210,23 @@ def bar(x, y):
         got = cres.entry_point(x, y)
         self.assertEqual(expect, got)
 
+    def test_tuple_of_arrays_iter(self):
+        # We used to leak a reference to each element of the tuple
+        def bar(arrs):
+            total = 0
+            for arr in arrs:
+                total += arr[0]
+
+            return total
+
+        x = y = np.arange(3, dtype=np.int32)
+        aryty = types.Array(types.int32, 1, 'C')
+        cres = compile_isolated(bar, (types.containers.UniTuple(aryty, 2),))
+
+        expect = bar((x, y))
+        got = cres.entry_point((x, y))
+        self.assertEqual(expect, got)
+
 
 if __name__ == '__main__':
     unittest.main()