Repository: numba/numba

Issue:
Unnecessary refcount not removed in 2 levels function call causing significant performance decrease
```python
from timeit import default_timer as timer

import numpy as np

from numba import njit


@njit
def count(arr, i):
    arr[i] += 1


@njit
def append(arr, i):
    count(arr, i)


@njit(_nrt=True)  # disable _nrt would fix it
def foo(arr):
    for i in range(arr.size): 
        append(arr, i)   # calling count() here would fix it


arr = np.random.random(10 ** 7)

for i in range(3):
    ts = timer()
    foo(arr)   # performance is 3x and worse
    te = timer()
    print(te - ts)


foo.inspect_cfg(foo.signatures[0]).display(view=True)
```


Discussion:
This would fix issue #2248 for `ndarray`, i.e., probably the most common use case, (`list` might be more tricky, I guess). 😄

Resolving patch:
diff --git a/numba/runtime/nrtdynmod.py b/numba/runtime/nrtdynmod.py
--- a/numba/runtime/nrtdynmod.py
+++ b/numba/runtime/nrtdynmod.py
@@ -47,6 +47,8 @@ def _define_nrt_incref(module, atomic_incr):
     """
     fn_incref = module.get_or_insert_function(incref_decref_ty,
                                               name="NRT_incref")
+    # Cannot inline this for refcount pruning to work
+    fn_incref.attributes.add('noinline')
     builder = ir.IRBuilder(fn_incref.append_basic_block())
     [ptr] = fn_incref.args
     is_null = builder.icmp_unsigned("==", ptr, cgutils.get_null_value(ptr.type))
@@ -66,6 +68,8 @@ def _define_nrt_decref(module, atomic_decr):
     """
     fn_decref = module.get_or_insert_function(incref_decref_ty,
                                               name="NRT_decref")
+    # Cannot inline this for refcount pruning to work
+    fn_decref.attributes.add('noinline')
     calldtor = module.add_function(ir.FunctionType(ir.VoidType(), [_pointer_type]),
                                    name="NRT_MemInfo_call_dtor")
 
diff --git a/numba/runtime/nrtopt.py b/numba/runtime/nrtopt.py
--- a/numba/runtime/nrtopt.py
+++ b/numba/runtime/nrtopt.py
@@ -9,7 +9,7 @@
 
 _regex_incref = re.compile(r'\s*call void @NRT_incref\((.*)\)')
 _regex_decref = re.compile(r'\s*call void @NRT_decref\((.*)\)')
-_regex_bb = re.compile(r'([-a-zA-Z$._][-a-zA-Z$._0-9]*:)|^define')
+_regex_bb = re.compile(r'([\'"]?[-a-zA-Z$._][-a-zA-Z$._0-9]*[\'"]?:)|^define')
 
 
 def remove_redundant_nrt_refct(ll_module):
@@ -93,14 +93,20 @@ def _examine_refct_op(bb_lines):
     def _prune_redundant_refct_ops(bb_lines):
         incref_map = defaultdict(deque)
         decref_map = defaultdict(deque)
+        to_remove = set()
         for num, incref_var, decref_var in _examine_refct_op(bb_lines):
             assert not (incref_var and decref_var)
             if incref_var:
-                incref_map[incref_var].append(num)
+                if incref_var == 'i8* null':
+                    to_remove.add(num)
+                else:
+                    incref_map[incref_var].append(num)
             elif decref_var:
-                decref_map[decref_var].append(num)
+                if decref_var == 'i8* null':
+                    to_remove.add(num)
+                else:
+                    decref_map[decref_var].append(num)
 
-        to_remove = set()
         for var, decops in decref_map.items():
             incops = incref_map[var]
             ct = min(len(incops), len(decops))
diff --git a/numba/targets/codegen.py b/numba/targets/codegen.py
--- a/numba/targets/codegen.py
+++ b/numba/targets/codegen.py
@@ -121,6 +121,7 @@ def _optimize_final_module(self):
         Internal: optimize this library's final module.
         """
         self._codegen._mpm.run(self._final_module)
+        self._final_module = remove_redundant_nrt_refct(self._final_module)
 
     def _get_module_for_linking(self):
         """


Test patch: