Numba appears to ignore type signatures

Consider a simple function

@numba.njit("i4(i4, i4)")
def add(a, b): return a+b

compiling, and inspecting types:

add (int32, int32)
--------------------------------------------------------------------------------
# --- LINE 1 --- 

@njit(["i4(i4, i4)"])

# --- LINE 2 --- 

def add(a, b):

    # --- LINE 3 --- 
    # label 0
    #   a = arg(0, name=a)  :: int32
    #   b = arg(1, name=b)  :: int32
    #   $6binary_add.2 = a + b  :: int64
    #   del b
    #   del a
    #   $8return_value.3 = cast(value=$6binary_add.2)  :: int32
    #   del $6binary_add.2
    #   return $8return_value.3

    return  a + b

Appears to show the right behaviour. But when tested with:

>>> x = np.int32(1)
>>> res = add(x, x)
>>> print(numba.typeof(res))
int64

My usecase is dependent on the signatures working, why doesn’t this seem to work? This code was tested on an Intel-i7 CPU running Ubuntu, with Numba 0.53.0, and Numpy version 1.21.1, installed via Anaconda.

My initial thought was that Python could be casting it upwards. However,

>>> res = np.int32(0)
>>> res += add(x, x)
>>> print(numba.typeof(res))
int64

also gives the same result

hi @skailasa , I’m not 100% sure but I don’t think numba is doing something wrong. It’s the way of querying the type that leads to confusion.
when you assigned res += add(x, x) the plus operation was not performed by numba. That operation was performed in pure python. Pure python does not have an int32 type, so Numba, when moving your int32 to python, returned a python integer. then python performed the sum. You can try the following type(np.int32(0)+0) and you’ll get numpy.int64. That’s why you get that result. Numba does not return np.int32, it returns int (built-in one).
As long as you stay within a jitted function your int32 will remain int32. If you need to keep them after going back to python, I think you’ll have to wrap in a call to np.int32. Maybe there’s another way but I don’t know it.

Hi there,

I see what you’re saying, and I suspected something like this was happening.

I think it’s worth highlighting for future readers that if you use a Numpy container, which obviously does support types like int32 etc, then you will get the behaviour you would expect.

i.e.

>>> x = np.array([1], np.int32)
>>> print(add(x, x))
array([2], dtype=int32)

bearing in mind that the function has to be decorated correctly to support arrays.

I think that this behaviour is unintuitive though, and not documented clearly anywhere as far as I can tell, and it could be extremely relevant in a lot of applications.

p.s.
I’d like to add that I’m averse to casting, as it adds overhead to my application, which may also be the case for a lot of other sci-comp folks.

if the casting happens in a hot portion of the code, and you are worried about overhead, it would be better that you don’t cross from jitted code into python at all. The process of returning a result from a jit compiled function back to python requires boxing the result from its native representation into a python object. I haven’t measured it, but I’m guessing that has as much overhead as the casting from int to np.int32.

If you put all the hot loops inside the jitted functions, and then return a value that needs to be cast once or twice during the whole execution, then it won’t be a problem. If you are casting many times, is because you are also boxing many times, and that’s an overhead that you should look into.

Regarding behaviour and documentation, PRs are always welcome. If you have spotted a good place where you would have expected this to be explained, then please contribute a few sentences in a PR.

Luk