Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BUG: __array_ufunc__ should always be looked up on the type, never the instance #9087

Merged
merged 6 commits into from
May 10, 2017

Conversation

eric-wieser
Copy link
Member

@eric-wieser eric-wieser commented May 10, 2017

This also corrects a broken short-circuit when looking up __array_ufunc__.

Doesn't build on my machine, but msvc is not giving me useful errors.

@eric-wieser eric-wieser force-pushed the fix-ufunc-resolution branch 2 times, most recently from 1836f00 to 260a128 Compare May 10, 2017 12:51
@eric-wieser eric-wieser added this to the 1.13.0 release milestone May 10, 2017
int disables;

array_ufunc = PyObject_GetAttrString(obj, "__array_ufunc__");
disables = (array_ufunc == Py_None);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seemed dumb to look up the attribute again, when we already did given that "The __array_func__ attribute must already be known to exist."

@@ -548,15 +554,13 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method,
goto fail;
}

/* Access the override */
array_ufunc = PyObject_GetAttrString(override_obj,
"__array_ufunc__");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, why look it up a third time? Especially since here we're using a different lookup rule to the previous time

@eric-wieser eric-wieser changed the title MAINT: Distinguish "correct" special method lookups from incorrect ones BUG: __array_ufunc__ should always be looked up on the type, never the instance May 10, 2017
@eric-wieser
Copy link
Member Author

eric-wieser commented May 10, 2017

For some reason the last commit is not showing up in this PR - you'll see it if you click the branch name at the bottom of the page

}
/* Set the self argument, since we have an unbound method */
Py_INCREF(override_obj);
PyTuple_SetItem(override_args, 0, override_obj);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because PyArray_LookupSpecial returns an unbound method. Arguably it should return a bound one, but that's wasteful to construct, and I wasn't able to make it work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyObject_GetAttrString should have done that too, was it buggy before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was buggy because it did PyObject_GetAttrString(obj, ...) not PyObject_GetAttrString(Py_TYPE(obj), ...)

if (obj == Py_None ||
PyBool_Check(obj) ||
/* Basic number types */
PyTypeObject const * const known_types[] = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is somewhat performance relevant for small arrays, a large if will give better code than creating this array each time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the compiler optimize the array since it's const, and full of addresses to static variables?

I could add a static if it helps

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no those variables may come from a dynamic loading so they are not const as far as the compiler can tell.
adding static won't work for the same reason.
You could add a static init variable, but imo just using an if statement is nicer than modifiable global state.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, that's a bit of a pain. I guess the if is not too verbose.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an if is less lines of code and a decent text editor with block and multicursor editing should handle it just as well :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll wait for the tests to pass before pushing that, to see if I missed anything that my local build didn't pick up. Any other comments?

Previously, we would check if the attribute existed on the class, yet use it from the instance.

This also cuts 3 lookups of `__array_ufunc__` down to one.
@eric-wieser
Copy link
Member Author

eric-wieser commented May 10, 2017

Ugh, warning hell - this file is shared between multiple libraries, but each library only uses one function from it, so warnings are produced about the unused ones

+ tee warnings
+ grep -E 'warning\>'
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:108:1: warning: 'PyArray_LookupSpecial' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:108:1: warning: 'PyArray_LookupSpecial' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:108:1: warning: 'PyArray_LookupSpecial' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined but not used [-Wunused-function]
    numpy/core/src/private/get_attr_string.h:129:1: warning: 'PyArray_LookupSpecial_OnInstance' defined 

@juliantaylor
Copy link
Contributor

add NPY_INLINE to the static header functions, that removes the unused warnings as no code is emitted when they are not used (c99 inline semantics we rely on in many places already)


return 0;
/* sentinel to swallow trailing || */
NPY_FALSE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just remove the trailing ||?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do, but it makes it much easier to reorder the above conditions, and add new ones, leaving it this way.

In particular, there's probably some small performance gain to be had by putting the most common types first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, Py_None should be at the top as in the original as it is one of the common arguments (out, axis, ...)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, I'll move that back

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though that is very very minor as GCC just does all comparisons at once anyway and follows it up with some form of jump table.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of not incurring another rebase/wait-for-tests cycle, shall I leave it as is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave it, it is not that important that it needs so much microoptimization.

int nargs = PyTuple_GET_SIZE(args);
npy_intp nin = ufunc->nin;
npy_intp nout = ufunc->nout;
npy_intp nargs = PyTuple_GET_SIZE(args);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changing this will emit warnings in the following format functions, they use %d
for intp you have to use NPY_INTP_FMT or cast them to int again

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I should just add casts to PyTuple_GET_SIZE instead?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just leaving it int is probably fine, I'm not too worried about stuff crashing when you pass more than 32 bit arguments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it seems like either travis does not catch this type of warning, or int == intp on travis

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the warning check is only done on 32 bit where int == intp, I wanted to fix that at some point ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we leave fixing these remaining warnings until that time then? With this change, I get fewer warnings on MSVC, which insists on only showing warnings when an error is elsewhere

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have them fixed. I can push a commit doing so when you are otherwise done.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I think I'm otherwise done here. I've given up on making PyArray_LookupSpecial behave more like _PyObject_LookupSpecial, because it messes with our if (override == ndarray.__array_ufunc__) check in ways that I can't debug (probably comparing bound/unbound methods, or a copy being made of the function object at some level)

@eric-wieser
Copy link
Member Author

Is there any way to make the warning travis build be first? It's maddening having to come back to this every half hour, only to find that most of the way through the test, the warning one (which isn't easy to test locally) has failed

@juliantaylor
Copy link
Contributor

the warning test is after the regular matrix job in the.travis.yaml, I never tried switching the ordering of there. It probably works.

@pv
Copy link
Member

pv commented May 10, 2017 via email

@juliantaylor
Copy link
Contributor

I know, these are ordered from slowest to fastest for scheduling already, but I wonder if the python: and matrix: job can be reordered.
Or we just add a warning check to the python: checks too

}
/* does the class define __array_ufunc__? */
cls_array_ufunc = PyArray_GetAttrString_SuppressException(
(PyObject *)Py_TYPE(obj), "__array_ufunc__");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's worth noting that before and after this patch, __array_ufunc__ would fail on old-style classes, where type(obj) is instancetype

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error is

TypeError: unsupported operand type(s) for +: 'instance' and 'instance'

Which I think is probably good enough

@eric-wieser
Copy link
Member Author

@juliantaylor: Thanks for that commit. Tests all pass.

@juliantaylor juliantaylor merged commit 14ff219 into numpy:master May 10, 2017
@juliantaylor
Copy link
Contributor

thanks

@eric-wieser
Copy link
Member Author

eric-wieser commented May 10, 2017

Does this fix the performance issue you raised in #9085?

@juliantaylor
Copy link
Contributor

yes as you moved the basic check to the type object it doesn't call GetAttr anymore so CheckOverride is down to 4% of the runtime from 50%

/* PyTuple_SET_ITEM steals reference */
PyTuple_SET_ITEM(override_args, 0, (PyObject *)ufunc);
Py_INCREF(Py_None);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, thanks for putting this together. Much better! But why set override_args 0 here? It is just overwritten below anyway (and if you keep this write, wouldn't you have to write to decref None below)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was worried about the behaviour of PyTuple_SetItem with a NULL in its target position.

I do not need to decref None, because unlike SETITEM, SetItem handles the decref

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use SET_ITEM below too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that doesn't free the reference of the object previously in the tuple. To be honest, the reference counting design here was based on the case that I was sure I had reasoned correctly about, rather than picking a strategy that would require me to read the tuple source code again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough! Indeed, while correcting, I played with changing it to SET_ITEM, and realised it became a convoluted pain of now having to decref things oneself.

Py_DECREF(array_ufunc);
/* Call the method */
*result = PyObject_Call(
override_array_ufunc, override_args, normal_kwds);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to DECREF override_array_ufunc after this, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, they all get should have been cleaned up at the bottom. Wanna patch that quick?

@@ -580,6 +584,9 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method,
return 0;

fail:
for (i = 0; i < num_override_args; i++) {
Py_XDECREF(array_ufunc_methods[i]);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, mistake here - this path needs to be run under both failure and success

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is probably the easiest solution...

@mhvk
Copy link
Contributor

mhvk commented May 10, 2017

@eric-wieser, @juliantaylor - this is much nicer than what I had -- I had thought of storing __array_ufunc__ but felt I needed the wrapped method, and it just seemed a bit too many changes from the original __numpy_ufunc__ code.

But also a worry about a memory leak: see inline comment.

@charris
Copy link
Member

charris commented May 10, 2017

I assume a fixup for this is coming?

@eric-wieser
Copy link
Member Author

I don't have time tonight for a fixup, I'm afraid. @mhvk, can you make the fix mentioned in the inline comment?

@mhvk
Copy link
Contributor

mhvk commented May 10, 2017

OK, I'll make a fixup.

@mhvk
Copy link
Contributor

mhvk commented May 10, 2017

See #9092

@mattip
Copy link
Member

mattip commented May 31, 2017

there is a subtle problem with this code. The override_args tuple is used in the function call, but if the call returns NotImplemented, override_args is rebuilt to reflect the next attempt to call, violating the assumption that a tuple will never be modified after use. CPython cannot enforce this, the best it can do is check that refcount == 1 in PyTuple_SetItem. But PyPy does enforces this, and the code fails on PyPy. I have submitted pull request #9195 to push building override_args into the loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants