Faster random.shuffle via static typing. - #6776
Conversation
|
To reduce the overhead in common cases, you could write a single loop for
|
|
Indeed, this shaves off another 10% or so to the runtime (in the case where itemsize is the same as intp.itemsize). Side question: currently, building numpy (via setup.py or runtests.py) fails (even after git clean -xf) with which I can locally solve by first importing numpy.core in setup.py (to break the circular dependency), but is that a known issue? |
There was a problem hiding this comment.
I doubt np.take does not work in-place or checks this. Which means the result can be corrupted by memory layout. I think you have to create a copy unfortunately.
Depends a bit on the shape, but unless your array is (N, m) with m being quite small, indexing is probably faster, but does not matter much.
There was a problem hiding this comment.
If mode is the default 'raise', it always allocates an intermediate buffer, so that if an error in the indexing is detected, and an error has to be raised, the output array is left unchanged. If you set it to 'clip' or 'wrap' then yes, you would start seeing all kind of funny things if you tried to do the operation in place.
There was a problem hiding this comment.
Ah ok, then take is nicer in that regard. Hopefully we have cipy on overlap at some point in indexing....
On Sun Dec 6 09:54:39 2015 GMT+0100, Jaime wrote:
with self.lock:while i > 0:j = rk_interval(i, self.internal_state)buf[...] = x[j]x[j] = x[i]x[i] = bufi = i - 1if isinstance(x, np.ndarray):if x.ndim == 1 and x.dtype.itemsize == np.dtype(np.intp).itemsize:# Directly shuffle the array if possible.self._shuffle_intpsized(x.view(np.intp))else:# Take from a shuffled range to benefit from static typing.idxs = np.arange(n, dtype=np.intp)self._shuffle_intpsized(idxs)x.take(idxs, 0, out=x)If
modeis the default'raise', it always allocates an intermediate buffer, so that if an error in the indexing is detected, and an error has to be raised, the output array is left unchanged. If you set it to'clip'or'wrap'then yes, you would start seeing all kind of funny things if you tried to do the operation in place.
Reply to this email directly or view it on GitHub:
https://github.com/numpy/numpy/pull/6776/files#r4676812
|
There's a test failure on 2.6, due to MaskedArrays not supporting the buffer protocol there. To be honest I don't really understand how the buffer protocol works on maskedarrays (how exactly is the mask information propagated?), moreover |
There was a problem hiding this comment.
Does cython know how to compile this down to a C for loop?
There was a problem hiding this comment.
Yes (and I much prefer this to range(n-1, 0, -1) which I always find slightly confusing).
|
Actually I suspect that your implementation is broken for masked arrays in
|
|
Indeed, the current test suite does not cover this case. In fact the current implementation messes up the underlying array (i.e masking, shuffling and unmasking reveals that masked values have been overwritten by unmasked values), which should probably be considered a bug too. Both issues are fixed by the new patch (which simply shuffle masked arrays by indexing with a shuffled index array). |
There was a problem hiding this comment.
I guess to be safer we might as well make it type(x) is np.ndarray to disallow all subclasses (since there's no guarantee that MaskedArray is the only weird subclass)
|
I think that as written this code should be safe WRT not changing the random stream. But can you make sure that we have tests for that, that cover all the different branches, and add them if they're missing? |
|
Aside from the minor nits above, the one thing I'm wary of is the memory overhead in the One idea: would it be easy to use fused types to at least generate u8, u16, u32, u64 versions of |
|
This latest commit should handle all the issues you mentioned. I added tests for various sizes of items. |
|
There should also be some special handling of object arrays, because these can't be viewed as int. You could branch on |
|
FWIW, I realized that you can bypass the checks in This handles the object array case but not the struct-with-object-fields case, which is arguably a bug in |
|
☔ The latest upstream changes (presumably #6910) made this pull request unmergeable. Please resolve the merge conflicts. |
This patch modifies random.shuffle so that (when working on a ndarray) an array of indices is shuffled and then elements are take()n from that array in that order. This allows the inner loop to be statically typed (it turns out this is not so easy to write a generic shuffling code using Cython fused types) and thus much faster (~6x for me), at the expense of a threefold increase in memory use (I guess take() needs to create a copy, and an additional array of indices is created.). See numpy#5514.
This avoids the memory overhead of allocating an index array and take()ing from the original array when possible.
Also ensure that the masked data is preserved upon shuffling, which was not the case before.
Do not rely on take(), which for non-standard sized arrays, thus ensuing the previous memory performance at the expense of speed. Also get rid of the guarantee that shuffling masked arrays maintains the masked values as well, which should probably be handled on np.ma's side anyways.
|
Superseded by #6933. |
Only for 1d-ndarrays exactly, as subtypes (e.g. masked arrays) may not allow direct shuffle of the underlying buffer (in fact, the old implementation destroyed the underlying values of masked arrays while shuffling). Also handles struct-containing-object 1d ndarrays properly. See numpy#6776 for an earlier, less general (but even faster: ~6x) improvement attempt, numpy#5514 for the original issue.
Apparently gcc only specializes one branch (the last one) so I went for another 33% performance increase (matching numpy#6776) in what's likely the most common use case.
Only for 1d-ndarrays exactly, as subtypes (e.g. masked arrays) may not allow direct shuffle of the underlying buffer (in fact, the old implementation destroyed the underlying values of masked arrays while shuffling). Also handles struct-containing-object 1d ndarrays properly. See numpy#6776 for an earlier, less general (but even faster: ~6x) improvement attempt, numpy#5514 for the original issue.
Apparently gcc only specializes one branch (the last one) so I went for another 33% performance increase (matching numpy#6776) in what's likely the most common use case.
This patch modifies random.shuffle so that (when working on a ndarray) an array
of indices is shuffled and then elements are take()n from that array in that
order. This allows the inner loop to be statically typed (it turns out this
is not so easy to write a generic shuffling code using Cython fused types) and
thus much faster (~6x for me), at the expense of a threefold increase in memory
use (I guess take() needs to create a copy, and an additional array of indices
is created.).
See #5514.