Skip to content

Commit

Permalink
ENH: Improve array function overhead by using vectorcall
Browse files Browse the repository at this point in the history
This moves dispatching for `__array_function__` into a C-wrapper.  This
helps speed for multiple reasons:
* Avoids one additional dispatching function call to C
* Avoids the use of `*args, **kwargs` which is slower.
* For simple NumPy calls we can stay in the faster "vectorcall" world

This speeds up things generally a little, but can speed things up a lot
when keyword arguments are used on lightweight functions, for example::

    np.can_cast(arr, dtype, casting="same_kind")

is more than twice as fast with this.

There is one alternative in principle to get best speed:  We could inline
the "relevant argument"/dispatcher extraction.  That changes behavior in
an acceptable but larger way (passes default arguments).
Unless the C-entry point seems unwanted, this should be a decent step
in the right direction even if we want to do that eventually, though.

Closes gh-20790
Closes gh-18547  (although not quite sure why)
  • Loading branch information
seberg committed Jan 17, 2023
1 parent 9b6a7b4 commit 60a858a
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 346 deletions.
10 changes: 2 additions & 8 deletions numpy/core/_asarray.py
Expand Up @@ -24,10 +24,6 @@
}


def _require_dispatcher(a, dtype=None, requirements=None, *, like=None):
return (like,)


@set_array_function_like_doc
@set_module('numpy')
def require(a, dtype=None, requirements=None, *, like=None):
Expand Down Expand Up @@ -100,10 +96,10 @@ def require(a, dtype=None, requirements=None, *, like=None):
"""
if like is not None:
return _require_with_like(
like,
a,
dtype=dtype,
requirements=requirements,
like=like,
)

if not requirements:
Expand Down Expand Up @@ -135,6 +131,4 @@ def require(a, dtype=None, requirements=None, *, like=None):
return arr


_require_with_like = array_function_dispatch(
_require_dispatcher
)(require)
_require_with_like = array_function_dispatch()(require)
37 changes: 8 additions & 29 deletions numpy/core/numeric.py
Expand Up @@ -130,10 +130,6 @@ def zeros_like(a, dtype=None, order='K', subok=True, shape=None):
return res


def _ones_dispatcher(shape, dtype=None, order=None, *, like=None):
return(like,)


@set_array_function_like_doc
@set_module('numpy')
def ones(shape, dtype=None, order='C', *, like=None):
Expand Down Expand Up @@ -187,16 +183,13 @@ def ones(shape, dtype=None, order='C', *, like=None):
"""
if like is not None:
return _ones_with_like(shape, dtype=dtype, order=order, like=like)
return _ones_with_like(like, shape, dtype=dtype, order=order)

a = empty(shape, dtype, order)
multiarray.copyto(a, 1, casting='unsafe')
return a


_ones_with_like = array_function_dispatch(
_ones_dispatcher
)(ones)
_ones_with_like = array_function_dispatch()(ones)


def _ones_like_dispatcher(a, dtype=None, order=None, subok=None, shape=None):
Expand Down Expand Up @@ -323,7 +316,7 @@ def full(shape, fill_value, dtype=None, order='C', *, like=None):
"""
if like is not None:
return _full_with_like(shape, fill_value, dtype=dtype, order=order, like=like)
return _full_with_like(like, shape, fill_value, dtype=dtype, order=order)

if dtype is None:
fill_value = asarray(fill_value)
Expand All @@ -333,9 +326,7 @@ def full(shape, fill_value, dtype=None, order='C', *, like=None):
return a


_full_with_like = array_function_dispatch(
_full_dispatcher
)(full)
_full_with_like = array_function_dispatch()(full)


def _full_like_dispatcher(a, fill_value, dtype=None, order=None, subok=None, shape=None):
Expand Down Expand Up @@ -1778,10 +1769,6 @@ def indices(dimensions, dtype=int, sparse=False):
return res


def _fromfunction_dispatcher(function, shape, *, dtype=None, like=None, **kwargs):
return (like,)


@set_array_function_like_doc
@set_module('numpy')
def fromfunction(function, shape, *, dtype=float, like=None, **kwargs):
Expand Down Expand Up @@ -1847,15 +1834,13 @@ def fromfunction(function, shape, *, dtype=float, like=None, **kwargs):
"""
if like is not None:
return _fromfunction_with_like(function, shape, dtype=dtype, like=like, **kwargs)
return _fromfunction_with_like(like, function, shape, dtype=dtype, **kwargs)

args = indices(shape, dtype=dtype)
return function(*args, **kwargs)


_fromfunction_with_like = array_function_dispatch(
_fromfunction_dispatcher
)(fromfunction)
_fromfunction_with_like = array_function_dispatch()(fromfunction)


def _frombuffer(buf, dtype, shape, order):
Expand Down Expand Up @@ -2130,10 +2115,6 @@ def _maketup(descr, val):
return tuple(res)


def _identity_dispatcher(n, dtype=None, *, like=None):
return (like,)


@set_array_function_like_doc
@set_module('numpy')
def identity(n, dtype=None, *, like=None):
Expand Down Expand Up @@ -2168,15 +2149,13 @@ def identity(n, dtype=None, *, like=None):
"""
if like is not None:
return _identity_with_like(n, dtype=dtype, like=like)
return _identity_with_like(like, n, dtype=dtype)

from numpy import eye
return eye(n, dtype=dtype, like=like)


_identity_with_like = array_function_dispatch(
_identity_dispatcher
)(identity)
_identity_with_like = array_function_dispatch()(identity)


def _allclose_dispatcher(a, b, rtol=None, atol=None, equal_nan=None):
Expand Down
94 changes: 37 additions & 57 deletions numpy/core/overrides.py
Expand Up @@ -6,7 +6,7 @@
from .._utils import set_module
from .._utils._inspect import getargspec
from numpy.core._multiarray_umath import (
add_docstring, implement_array_function, _get_implementing_args)
add_docstring, _get_implementing_args, _ArrayFunctionDispatcher)


ARRAY_FUNCTIONS = set()
Expand All @@ -33,40 +33,33 @@ def set_array_function_like_doc(public_api):


add_docstring(
implement_array_function,
_ArrayFunctionDispatcher,
"""
Implement a function with checks for __array_function__ overrides.
Class to wrap functions with checks for __array_function__ overrides.
All arguments are required, and can only be passed by position.
Parameters
----------
dispatcher : function or None
The dispatcher function that returns a single sequence-like object
of all arguments relevant. It must have the same signature (except
the default values) as the actual implementation.
If ``None``, this is a ``like=`` dispatcher and the
``_ArrayFunctionDispatcher`` must be called with ``like`` as the
first (additional and positional) argument.
implementation : function
Function that implements the operation on NumPy array without
overrides when called like ``implementation(*args, **kwargs)``.
public_api : function
Function exposed by NumPy's public API originally called like
``public_api(*args, **kwargs)`` on which arguments are now being
checked.
relevant_args : iterable
Iterable of arguments to check for __array_function__ methods.
args : tuple
Arbitrary positional arguments originally passed into ``public_api``.
kwargs : dict
Arbitrary keyword arguments originally passed into ``public_api``.
overrides when called like.
Returns
-------
Result from calling ``implementation()`` or an ``__array_function__``
method, as appropriate.
Raises
------
TypeError : if no implementation is found.
Attributes
----------
_implementation : function
The original implementation passed in.
""")


# exposed for testing purposes; used internally by implement_array_function
# exposed for testing purposes; used internally by _ArrayFunctionDispatcher
add_docstring(
_get_implementing_args,
"""
Expand Down Expand Up @@ -110,18 +103,22 @@ def verify_matching_signatures(implementation, dispatcher):
'default argument values')


def array_function_dispatch(dispatcher, module=None, verify=True,
def array_function_dispatch(dispatcher=None, module=None, verify=True,
docs_from_dispatcher=False):
"""Decorator for adding dispatch with the __array_function__ protocol.
See NEP-18 for example usage.
Parameters
----------
dispatcher : callable
dispatcher : callable or None
Function that when called like ``dispatcher(*args, **kwargs)`` with
arguments from the NumPy function call returns an iterable of
array-like arguments to check for ``__array_function__``.
If `None`, the first argument is used as the single `like=` argument
and not passed on. A function implementing `like=` must call its
dispatcher with `like` as the first non-keyword argument.
module : str, optional
__module__ attribute to set on new function, e.g., ``module='numpy'``.
By default, module is copied from the decorated function.
Expand Down Expand Up @@ -154,45 +151,28 @@ def decorator(implementation):

def decorator(implementation):
if verify:
verify_matching_signatures(implementation, dispatcher)
if dispatcher is not None:
verify_matching_signatures(implementation, dispatcher)
else:
# Using __code__ directly similar to verify_matching_signature
co = implementation.__code__
last_arg = co.co_argcount + co.co_kwonlyargcount - 1
last_arg = co.co_varnames[last_arg]
if last_arg != "like" or co.co_kwonlyargcount == 0:
raise RuntimeError(
"__array_function__ expects `like=` to be the last "
"argument and a keyword-only argument. "
f"{implementation} does not seem to comply.")

if docs_from_dispatcher:
add_docstring(implementation, dispatcher.__doc__)

@functools.wraps(implementation)
def public_api(*args, **kwargs):
try:
relevant_args = dispatcher(*args, **kwargs)
except TypeError as exc:
# Try to clean up a signature related TypeError. Such an
# error will be something like:
# dispatcher.__name__() got an unexpected keyword argument
#
# So replace the dispatcher name in this case. In principle
# TypeErrors may be raised from _within_ the dispatcher, so
# we check that the traceback contains a string that starts
# with the name. (In principle we could also check the
# traceback length, as it would be deeper.)
msg = exc.args[0]
disp_name = dispatcher.__name__
if not isinstance(msg, str) or not msg.startswith(disp_name):
raise

# Replace with the correct name and re-raise:
new_msg = msg.replace(disp_name, public_api.__name__)
raise TypeError(new_msg) from None

return implement_array_function(
implementation, public_api, relevant_args, args, kwargs)

public_api.__code__ = public_api.__code__.replace(
co_name=implementation.__name__,
co_filename='<__array_function__ internals>')
public_api = _ArrayFunctionDispatcher(dispatcher, implementation)
public_api = functools.wraps(implementation)(public_api)

if module is not None:
public_api.__module__ = module

public_api._implementation = implementation

ARRAY_FUNCTIONS.add(public_api)

return public_api
Expand Down

0 comments on commit 60a858a

Please sign in to comment.