numpy / numpy/numpy

Ufunc calls on scalars are very slow

Open
#11,232 8 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

15 - Discussion component: numpy._core
Dominant language
Python
Stars
32.8k
Forks
12.8k
Avg merge
1d 7h
Merged PRs (30d)
197

Description

It is well known that ufunc calls on scalars are rather slow, but it is probably good to have a summary of why it is so slow, for which it is useful to go along the ufunc_generic_call path. I got only partway, but one possible solution might be for the scalars to already get overridden in in CheckOverride, i.e., to treat them as if they had their own __array_ufunc__ (with priority even below that of ndarray; an actual __array_ufunc__ calling math is slightly slower than our present state).

  1. PyUFunc_CheckOverride: for non-arrays (thus including scalars), this checks whether the scalar has __array_ufunc__. Easy to avoid if our whole API is available - needs #10915.
  2. PyUFunc_GenericFunction: to be done (will edit).
  3. make_full_arg_tuple: EDIT now fast (with #11231).
  4. _find_array_wrap -> _find_array_method: skips arrays and scalars, so should be reasonably fast (though a subclass check for Generic is done before type checks on python objects in PyArray_IsAnyScalar (in ndarrayobject.h).

Simple timings

Single-input ufunc, comparing with math

a = 1.
a64 = np.float64(1.)
as64 = np.array(1., dtype=np.float64)
%timeit math.sin(a)  # and a64, as64
# 76, 76, 87  ns for a, a64, as64
%timeit np.sin(a)
# 600, 930, 450 ns for a, a64, as64

Somewhat more random, for addition

%timeit np.add(1., 1)
# 1000000 loops, best of 3: 970 ns per loop
%timeit 1. + 1
# 100000000 loops, best of 3: 8.73 ns per loop
# slightly fairer
%timeit operator.add(1., 1)
# 10000000 loops, best of 3: 80.4 ns per loop
# Oddly, again, scalars are much slower than array scalars
a64 = np.float64(1.)
%timeit np.add(a64, a64)
# 1000000 loops, best of 3: 1.35 µs per loop
as64 = np.array(1., dtype=np.float64)
%timeit np.add(as64, as64)
# 1000000 loops, best of 3: 468 ns per loop

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Trace the ufunc_generic_call path, beginning with PyUFunc_CheckOverride and PyUFunc_GenericFunction, and review the notes about make_full_arg_tuple and _find_array_method. Compare the scalar timings in the issue with array and math operations; done requires an agreed approach that reduces scalar-call overhead and demonstrates the improvement with comparable timings.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.