Repository: numba/numba

Issue:
math.exp is slower than np.exp
I expected them to compile to the same thing, so why is there a difference in speed?
As seen in the script below, both compile in nopython mode, so that is not the problem...

Using the following snippet,

``` python
from timeit import repeat
import math

import numpy as np
from numba import jit, njit, __version__

print "using numba", __version__

def timefunc(func, *args, **kwargs):
    # warm JIT
    func(*args, **kwargs)
    t = min(repeat(lambda: func(*args, **kwargs), number=5, repeat=2))
    print('%s: %d ms' % (func.__name__, t * 1000))

@jit
def loop_npexp1(a):
    length = len(a)
    result = np.empty(length, dtype=np.float64)
    for i in range(length):
        result[i] = np.exp(4.3 * a[i])
    return result

@njit
def core_npexp(result, a):
    for i in range(len(a)):
        result[i] = np.exp(4.3 * a[i])

def loop_npexp2(a):
    result = np.empty(len(a), dtype=np.float64)
    core_npexp(result, a)
    return result

@jit
def loop_mathexp1(a):
    length = len(a)
    result = np.empty(length, dtype=np.float64)
    for i in range(length):
        result[i] = math.exp(4.3 * a[i])
    return result

@njit
def core_mathexp(result, a):
    for i in range(len(a)):
        result[i] = math.exp(4.3 * a[i])

def loop_mathexp2(a):
    result = np.empty(len(a), dtype=np.float64)
    core_mathexp(result, a)
    return result

a = np.random.rand(1e6)

timefunc(loop_npexp1, a)
timefunc(loop_npexp2, a)
timefunc(loop_mathexp1, a)
timefunc(loop_mathexp2, a)
```

I get:

```
using numba 0.14.0
loop_npexp1: 86 ms
loop_npexp2: 92 ms
loop_mathexp1: 120 ms
loop_mathexp2: 109 ms
```



Discussion:
I can't reproduce this on my machine

```
loop_npexp1: 58 ms
loop_npexp2: 56 ms
loop_mathexp1: 57 ms
loop_mathexp2: 53 ms
```

My setup:

```
CPython 2.7.8
IPython 2.2.0

numpy 1.9.0
scipy 0.14.0

compiler   : GCC 4.2.1 (Apple Inc. build 5577)
system     : Darwin
release    : 13.3.0
machine    : x86_64
processor  : i386
CPU cores  : 4
interpreter: 64bit
```

As this seems important now, I am on Windows/64 bits:

```
llvmpy                    0.12.7                   py27_0
numba                     0.14.0               np19py27_0
numpy                     1.9.0                    py27_0
python                    2.7.8                         0
```

FWIW: I get similar results using a Python3 stack

For historical reasons, Numba does in fact emit different LLVM for math.exp() and numpy.exp().  For all Numpy math functions (where available), we use the C functions that Numpy exports.  For the math module, we emit the LLVM intrinsics functions.  On most platforms, both ultimately call the function provided by the platform's math library.  Apparently on Windows, they do not?

OK, we're trying to chase this down here, but haven't figured out how this is possible yet.  We can reproduce the performance difference on Windows, but our read of the Numpy source code is that the `npy_exp()` function we call should pass right through to the standard `exp()`.

@sklam has looked at the calls with a debugger, and it appears that when Numpy calls exp() Visual Studio inserts some kind of jump table that picks a hand-written (at least it looks hand-written) assembly implementation.  When Numba generates code to call the platform exp() directly for the math module, Visual Studio is not involved, and we seem to get some kind of generic implementation from the runtime.

The natural follow up question is whether we should just switch all the Python math module calls to use Numpy.


Resolving patch:
diff --git a/numba/targets/cpu.py b/numba/targets/cpu.py
--- a/numba/targets/cpu.py
+++ b/numba/targets/cpu.py
@@ -53,7 +53,6 @@ def init(self):
         # that LLVM 3.5 will fix this issue.
         eb.mattrs("-avx")
         self.tm = tm = eb.select_target()
-        self.engine = eb.create(tm)
         self.pm = self.build_pass_manager()
         self.native_funcs = utils.UniqueDict()
         self.cmath_provider = {}
@@ -69,6 +68,9 @@ def init(self):
         self.insert_func_defn(operatorimpl.registry.functions)
         self.insert_func_defn(printimpl.registry.functions)
 
+        # Engine creation has sideeffect to the process symbol table
+        self.engine = eb.create(tm)
+
     def get_function_type(self, fndesc):
         """
         Get the implemented Function type for the high-level *fndesc*.


Test patch: