Repository: numba/numba

Issue:
Performance differences between @jit and @vectorize on arrays
The following functions:

```
def py_rel_diff(a, b):
    return 2 * (a - b) / (a + b)

rel_diff = vectorize(nopython=True)(py_rel_diff)
rel_diff_jit = jit(nopython=True)(py_rel_diff)
```

give different performance results on simple input arrays (1d). This issue will try to find out why.



Discussion:
First, we must disable the "safe" alloc in NRT (i.e. the memset() call that fills the allocated array with a magic debug value).

The second observation is that removing the division can make the `@jit` version faster: this is because type inference infers a different result type. For example np.add(int32, int32) returns int32, while the "+" operator, as inferred by Numba, returns int64 on the same inputs. This makes a large difference when LLVM is able to vectorize the loop.

The third observation is that Python "/" (as used in the `@vectorize` code) checks for zero division, which can prevent vectorization. Numpy's np.divide (or np.true_divide) doesn't.

The fourth observation is that some small change in the loop generation for `@vectorize` can allow LLVM to vectorize better. I don't know yet if the change is correct in all cases.


Resolving patch:
diff --git a/numba/npyufunc/array_exprs.py b/numba/npyufunc/array_exprs.py
--- a/numba/npyufunc/array_exprs.py
+++ b/numba/npyufunc/array_exprs.py
@@ -6,7 +6,7 @@
 
 from numpy import ufunc
 
-from .. import ir, types, rewrites, six
+from .. import compiler, ir, types, rewrites, six
 from ..typing import npydecl
 from .dufunc import DUFunc
 
@@ -336,7 +336,11 @@ def _lower_array_expr(lowerer, expr):
             inner_sig_args.append(argty)
     inner_sig = outer_sig.return_type.dtype(*inner_sig_args)
 
-    cres = context.compile_only_no_cache(builder, impl, inner_sig)
+    # Follow the Numpy error model.  Note this also allows e.g. vectorizing
+    # division (issue #1223).
+    flags = compiler.Flags()
+    flags.set('error_model', 'numpy')
+    cres = context.compile_only_no_cache(builder, impl, inner_sig, flags=flags)
 
     # Create kernel subclass calling our native function
     from ..targets import npyimpl
diff --git a/numba/targets/base.py b/numba/targets/base.py
--- a/numba/targets/base.py
+++ b/numba/targets/base.py
@@ -709,7 +709,7 @@ def get_dummy_value(self):
     def get_dummy_type(self):
         return GENERIC_POINTER
 
-    def compile_only_no_cache(self, builder, impl, sig, locals={}):
+    def compile_only_no_cache(self, builder, impl, sig, locals={}, flags=None):
         """Invoke the compiler to compile a function to be used inside a
         nopython function, but without generating code to call that
         function.
@@ -719,7 +719,8 @@ def compile_only_no_cache(self, builder, impl, sig, locals={}):
 
         codegen = self.codegen()
         library = codegen.create_library(impl.__name__)
-        flags = compiler.Flags()
+        if flags is None:
+            flags = compiler.Flags()
         flags.set('no_compile')
         flags.set('no_cpython_wrapper')
         cres = compiler.compile_internal(self.typing_context, self,


Test patch:
diff --git a/numba/tests/test_array_exprs.py b/numba/tests/test_array_exprs.py
--- a/numba/tests/test_array_exprs.py
+++ b/numba/tests/test_array_exprs.py
@@ -44,6 +44,10 @@ def call_stuff(a0, a1):
 def are_roots_imaginary(As, Bs, Cs):
     return (Bs ** 2 - 4 * As * Cs) < 0
 
+def div_add(As, Bs, Cs):
+    return As / Bs + Cs
+
+
 # From issue #1264
 def distance_matrix(vectors):
     n_vectors = vectors.shape[0]
@@ -262,6 +266,7 @@ def test_cmp_op(self):
 
 
 class TestRewriteIssues(MemoryLeakMixin, unittest.TestCase):
+
     def test_issue_1184(self):
         from numba import jit
         import numpy as np
@@ -321,5 +326,22 @@ def foo(a, b):
         np.testing.assert_allclose(got, expect)
 
 
+class TestSemantics(MemoryLeakMixin, unittest.TestCase):
+
+    def test_division_by_zero(self):
+        # Array expressions should follow the Numpy error model
+        # i.e. 1./0. returns +inf instead of raising ZeroDivisionError
+        pyfunc = div_add
+        cfunc = njit(pyfunc)
+
+        a = np.float64([0.0, 1.0, float('inf')])
+        b = np.float64([0.0, 0.0, 1.0])
+        c = np.ones_like(a)
+
+        expect = pyfunc(a, b, c)
+        got = cfunc(a, b, c)
+        np.testing.assert_array_equal(expect, got)
+
+
 if __name__ == "__main__":
     unittest.main()