ENH: avoid allocations in getmaskarray - #8910
eric-wieser wants to merge 5 commits into
Conversation
|
|
||
| # duplicate it using zero strides | ||
| # like np.lib.stride_tricks.as_strided, but faster | ||
| return np.ndarray( |
There was a problem hiding this comment.
This is faster than np.lib.stride_tricks.as_strided and np.broadcast_to, surprisingly.
It's still pretty darn slow though, when all we really want to do is directly write to the shape and stride attributes.
Could np.zeros acquire a readonly kwarg, to do this C-side?
There was a problem hiding this comment.
not using keyword arguments for ndarray() should be significantly faster.
There was a problem hiding this comment.
You're right, it is. Using buff = b'\0' instead of np.ma.nomask is also faster.
With both optimizations in place, this breaks even at shape (2000,).
For small n, this line by itself is worse by a factor of 2 (600ns)
For n = 100000, this is better by a factor of 8.
np.zeros(..., bool) really is alarmingly fast
There was a problem hiding this comment.
want to update the PR?
|
interesting idea, readonly one element arrays from ones/zeros/full concerning this, the boolean bitwise functions do currently not have scalar specializations so this could actually harm performance. But that can be fixed. |
Not sure I understand how this is relevant - how do ufuncs behave on zero strides? Do they not still iterate over the full array? "readonly one element arrays" isn't quite the right description - they're multi-element backed by a single element's-worth of buffer There's a more interesting idea here which would be to maintain 0 strides, so that an addition like: would result in This could be implemented as an extra flag field on the arrays, |
|
bitwise_and/or use SSE2 code for contiguous arrays but not if one of the operands is a scalar. the arrays still would be readonly so generalizing it to ufuncs doesn't sound that useful. |
This is what is confusing me. Why are you talking about one of the operands being a scalar? As far as I can tell, this patch does not introduce this. Feel like I'm missing something here |
|
In the ufunc loops a scalar is just a memory block with stride zero, like the arrays you are creating here. what actually happens if you do return a scalar in |
Can you point me to the bit of code that special cases zero strides then?
I'm assuming you mean an array with Of course, this comes down to what "right" is defined as, but this would definitely be incompatible with the current behaviour, and it becomes harder to implement |
|
it are just the loops in |
|
Oh right, I see what you're getting at now - scalars are promoted to arrays with strides = (0,0,...) by the ufunc machinery, so doing that promotion by hand beforehand is equivalent. That looks like a pretty straightforward patch, and probably would offer some performance improvement. Although we could do even better if the Also, could this be taken further to work for any case of Regarding my remark about collapsing strides - I was envisaging something like this: |
|
do you have a benchmark for this? |
|
@juliantaylor: Not one that shows an improvement. I suspect there would be one with your patch included though. Do you want me to add one anyway? The actual goal here was to make calling |
* Avoid named arguments to ndarray * Avoid getting a buffer from bool_ when we can construct that buffer directly
|
Ok, updated with some micro-optimizations.
However, the timings become absolutely terrible when you do boolean operations on the |
mhvk
left a comment
There was a problem hiding this comment.
Overall, I quite like it. Especially if you follow my suggestion of making _viewmaskarray the routine that does the actual work, I would go ahead and replace getmaskarray with _viewmaskarray in other places where this is the logical choice.
| If None, use a MaskType instance. Otherwise, use a new datatype with | ||
| the same fields as `dtype`, converted to boolean types. | ||
| readonly : bool | ||
| If True, return a read-only array that uses less memory. |
There was a problem hiding this comment.
Maybe try to be a bit more explicit about what is actually done. E.g., "return a readonly array scalar that is broadcasted over the shape and thus uses less memory"
As noted above, I'd suggest calling this writable (which here should default to True for backward compatibility)
|
|
||
|
|
||
| def getmaskarray(arr): | ||
| def getmaskarray(arr, nomask_as_readonly=False): |
There was a problem hiding this comment.
My own sense would be not to add an argument here, which is used by _viewmaskarray, but rather have this call _viewmaskarray with a new argument; that way the rest of the code is not slowed down by another function call.
I'd also suggest to make the argument writable=True, ie., it states what kind of view is required.
There was a problem hiding this comment.
Flipping the call stack around sounds sensible. Do you still think there should be an argument here to expose the behaviour publicly?
I'd also suggest to make the argument
writable=True
I went for readonly because that's consistent with np.lib.stride_tricks._broadcast_to, but I guess you're right, making the name match the flags makes more sense.
There was a problem hiding this comment.
Done in make_mask_none.
The real meaning in this function though is require_to_be_writeable, which I thought was better spelt allow_readonly.
There was a problem hiding this comment.
allow_readonly definitely describes the intent, so 👍 on that.
| readonly=nomask_as_readonly) | ||
| return mask | ||
|
|
||
| def _viewmaskarray(arr): |
There was a problem hiding this comment.
So, in my view this would become
def _viewmaskarray(arr, writable=False):
mask = getmask(arr)
if mask is nomask:
make_mask_none(np.shape(arr), dtype=getattr(arr, 'dtype', None), writable=writable)
There was a problem hiding this comment.
The problem I have with that is that _viewmaskarray(arr, writable=False).writable == False does not always hold, which might be a little surprising.
There was a problem hiding this comment.
I had actually not thought so much about the flags, but more that writable=True indicated the array had to be writable. Maybe we can be even more direct and make it broadcast_if_possible=True?
There was a problem hiding this comment.
That seems to expose unnecessary implementation details. The important thing is whether the consumer is ok with mask.writeable == False. Everything else is just an optimization.
I thought I already did this in my second commit. Did I miss some obvious ones? |
It is just that if one reverses the order |
|
I was already under the impression that I replaced every internal use with
I doubt this is actually true right now - there's more overhead due to a lack of #8924 right now than there is due to one level of function call overhead. Also, there's a little too much overhead in calling the ndarray ctor, it seems. |
0e0fba1 to
acbc218
Compare
There was a problem hiding this comment.
Arguments against a writeable kwarg - we are bad at spelling it!
There was a problem hiding this comment.
it seems there is no agreed upon way to write this ._.
There was a problem hiding this comment.
You could say it's not particular... writeable.
In numpy we should write it writeable, because arr.flags.writeable is part of the public API.
The internal function should have the shallower callstack, for speed.
acbc218 to
d0d4ed5
Compare
| Input `MaskedArray` for which the mask is required. | ||
| allow_readonly : bool, optional | ||
| If True, allow this function to produce a readonly array when doing so | ||
| would increase performance. The default is False. |
There was a problem hiding this comment.
this and the one in make_mask_none need a .. versionadded:: tag
I clearly looked at an earlier version... Anyway, still found one. I changed this branch directly, but feel free to just make this part of your single commit. |
| out = out.view(MaskedArray) | ||
| out._mask = np.array([tuple(flatten_sequence(d.item())) | ||
| for d in getmaskarray(a)]) | ||
| for d in _viewmaskarray(a)]) |
There was a problem hiding this comment.
Yeah, I saw this and thought the whole function looked broken, so didn't touch it. But I was wrong, this is fine
|
Marked with "Needs work" since I do not think this should be merged while it causes a performance hit. |
|
☔ The latest upstream changes (presumably #5580) made this pull request unmergeable. Please resolve the merge conflicts. |
Public API still has to return a full array, but we can avoid a lot of copying and memory by returning
np.broadcast_to(np.zeros((), dtype), shape)instead ofnp.zeros(dtype, shape)whenarr.mask is nomask.This seems to add 2us overhead for small arrays, and starts to break even at around 10000 elements
Most of the time here is lost to the
ndarrayconstructor, when really all we want to do is modify->stridesand->shapewithout checking