Repository: numba/numba

Issue:
Calling functions with default parameters from interpreter is slow
Calling a compiled function from within the interpreter while omitting its default parameters is ~40 times slower than when the parameter is explicitly provided. Furthermore function calls of compiled functions with `False` or `None` parameters is ~5 times slower in comparison to calling with numeric values, e.g. `0`. See:

``` python
from timeit import timeit
from numba import njit

def func1(c):
    pass

def func2(c=False):
    pass

def test(f1, f2, n=10000):
    for c in (0, False, None):
        print("  c =", c)
        print("  f1(c)", timeit(lambda: f1(c), setup=lambda: f1(c), number=n))
        print("  f2(c)", timeit(lambda: f2(c), setup=lambda: f2(c), number=n))
        print("  f2( )", timeit(lambda: f2(), setup=lambda: f2(), number=n))

print("calling pure python functions:")
test(func1, func2)
print("calling numba compiled functions:")
test(njit(func1), njit(func2))
```

output:

```
calling pure python functions:
  c = 0
  f1(c) 0.0027124029875267297
  f2(c) 0.0028401410090737045
  f2( ) 0.002820795984007418
  c = False
  f1(c) 0.002927093009930104
  f2(c) 0.00296543599688448
  f2( ) 0.002754727000137791
  c = None
  f1(c) 0.0026716850115917623
  f2(c) 0.0029283500043675303
  f2( ) 0.002864026988390833
calling numba compiled functions:
  c = 0
  f1(c) 0.004309520008973777
  f2(c) 0.0040803030133247375
  f2( ) 0.6268751439929474
  c = False
  f1(c) 0.018810751993441954
  f2(c) 0.019296073995064944
  f2( ) 0.608556768973358
  c = None
  f1(c) 0.018237009993754327
  f2(c) 0.017925730004208162
  f2( ) 0.6146204769902397
```



Discussion:
Thanks for the numbers!

When measuring JIT-compiled runtimes, you should account for a first warmup call since that's when the function is compiled by Numba. I'm not saying it will change the numbers a lot, but it will remove one source of uncertainty.

The warmup is (hopefully) done by `timeit(..., setup=lambda: f1(c))` ;). Nonetheless (because I don't know exactly how `timeit` works internally) I also checked beforehand if the numbers were indeed also reproducible without `timeit`.


Resolving patch:
diff --git a/numba/dispatcher.py b/numba/dispatcher.py
--- a/numba/dispatcher.py
+++ b/numba/dispatcher.py
@@ -15,7 +15,7 @@
 from numba.typeconv.rules import default_type_manager
 from numba import sigutils, serialize, typing
 from numba.typing.templates import fold_arguments
-from numba.typing.typeof import typeof, Purpose
+from numba.typing.typeof import Purpose, typeof, typeof_impl
 from numba.bytecode import get_code_object
 from numba.six import create_bound_method, next
 from .caching import NullCache, FunctionCache
@@ -32,6 +32,10 @@ def __init__(self, value):
     def __repr__(self):
         return "omitted arg(%r)" % (self.value,)
 
+    @property
+    def _numba_type_(self):
+        return types.Omitted(self.value)
+
 
 class _FunctionCompiler(object):
 
@@ -150,8 +154,8 @@ def __init__(self, arg_count, py_func, pysig):
         self.__code__ = self.func_code
 
         argnames = tuple(pysig.parameters)
-        defargs = tuple(OmittedArg(val)
-                        for val in (self.py_func.__defaults__ or ()))
+        default_values = self.py_func.__defaults__ or ()
+        defargs = tuple(OmittedArg(val) for val in default_values)
         try:
             lastarg = list(pysig.parameters.values())[-1]
         except IndexError:
@@ -612,4 +616,6 @@ def compile(self, sig):
 
 
 # Initialize typeof machinery
-_dispatcher.typeof_init(dict((str(t), t._code) for t in types.number_domain))
+_dispatcher.typeof_init(
+    OmittedArg,
+    dict((str(t), t._code) for t in types.number_domain))


Test patch:
diff --git a/numba/tests/test_typeof.py b/numba/tests/test_typeof.py
--- a/numba/tests/test_typeof.py
+++ b/numba/tests/test_typeof.py
@@ -15,6 +15,7 @@
 import numba.unittest_support as unittest
 from numba import cffi_support, numpy_support, types
 from numba.special import typeof
+from numba.dispatcher import OmittedArg
 from numba._dispatcher import compute_fingerprint
 
 from .support import TestCase, tag
@@ -301,6 +302,17 @@ def test_custom(self):
         ty = typeof(Custom())
         self.assertEqual(ty, types.UniTuple(types.boolean, 42))
 
+    def test_omitted_args(self):
+        ty0 = typeof(OmittedArg(0.0))
+        ty1 = typeof(OmittedArg(1))
+        ty2 = typeof(OmittedArg(1.0))
+        ty3 = typeof(OmittedArg(1.0))
+        self.assertEqual(ty0, types.Omitted(0.0))
+        self.assertEqual(ty1, types.Omitted(1))
+        self.assertEqual(ty2, types.Omitted(1.0))
+        self.assertEqual(len({ty0, ty1, ty2}), 3)
+        self.assertEqual(ty3, ty2)
+
 
 class DistinctChecker(object):
 
@@ -529,6 +541,20 @@ def test_sets(self):
         with self.assertRaises(NotImplementedError):
             compute_fingerprint(frozenset([2, 3]))
 
+    def test_omitted_args(self):
+        distinct = DistinctChecker()
+
+        v0 = OmittedArg(0.0)
+        v1 = OmittedArg(1.0)
+        v2 = OmittedArg(1)
+
+        s = compute_fingerprint(v0)
+        self.assertEqual(compute_fingerprint(v1), s)
+        distinct.add(s)
+        distinct.add(compute_fingerprint(v2))
+        distinct.add(compute_fingerprint(0.0))
+        distinct.add(compute_fingerprint(1))
+
     def test_complicated_type(self):
         # Generating a large fingerprint
         t = None