8000 MAINT: Rewrite Floyd algorithm by thrasibule · Pull Request #13812 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

MAINT: Rewrite Floyd algorithm #13812

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

Merged
merged 13 commits into from
Jul 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions benchmarks/benchmarks/bench_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,16 @@ def time_bounded(self, bitgen, args):
self.rg.randint(0, max + 1, nom_size, dtype=dt)
else:
self.rg.integers(0, max + 1, nom_size, dtype=dt)

class Choice(Benchmark):
params = [1e3, 1e6, 1e8]

def setup(self, v):
self.a = np.arange(v)
self.rng = np.random.default_rng()

def time_legacy_choice(self, v):
np.random.choice(self.a, 1000, replace=False)

def time_choice(self, v):
self.rng.choice(self.a, 1000, replace=False)
88 changes: 60 additions & 28 deletions numpy/random/generator.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ cdef class Generator:
self.integers(low, high, size, dtype, endpoint)

@cython.wraparound(True)
def choice(self, a, size=None, replace=True, p=None, axis=0):
def choice(self, a, size=None, replace=True, p=None, axis=0, bint shuffle=True):
"""
choice(a, size=None, replace=True, p=None, axis=0):

Expand All @@ -538,6 +538,9 @@ cdef class Generator:
axis : int, optional
The axis along which the selection is performed. The default, 0,
selects by row.
shuffle : boolean, optional
Whether the sample is shuffled when sampling without replacement.
Default is True, False provides a speedup.

Returns
-------
Expand Down Expand Up @@ -593,14 +596,12 @@ cdef class Generator:
dtype='<U11')

"""
cdef char* idx_ptr
cdef int64_t buf
cdef char* buf_ptr

cdef set idx_set
cdef int64_t val, t, loc, size_i, pop_size_i
cdef int64_t *idx_data
cdef np.npy_intp j
cdef uint64_t set_size, mask
cdef uint64_t[::1] hash_set
# Format and Verify input
a = np.array(a, copy=False)
if a.ndim == 0:
Expand Down Expand Up @@ -687,36 +688,45 @@ cdef class Generator:
size_i = size
pop_size_i = pop_size
# This is a heuristic tuning. should be improvable
if pop_size_i > 200 and (size > 200 or size > (10 * pop_size // size)):
if shuffle:
cutoff = 50
else:
cutoff = 20
if pop_size_i > 10000 and (size_i > (pop_size_i // cutoff)):
# Tail shuffle size elements
idx = np.arange(pop_size, dtype=np.int64)
idx_ptr = np.PyArray_BYTES(<np.ndarray>idx)
buf_ptr = <char*>&buf
self._shuffle_raw(pop_size_i, max(pop_size_i - size_i,1),
8, 8, idx_ptr, buf_ptr)
idx = np.PyArray_Arange(0, pop_size_i, 1, np.NPY_INT64)
idx_data = <int64_t*>(<np.ndarray>idx).data
with self.lock, nogil:
self._shuffle_int(pop_size_i, max(pop_size_i - size_i, 1),
idx_data)
# Copy to allow potentially large array backing idx to be gc
idx = idx[(pop_size - size):].copy()
else:
# Floyds's algorithm with precomputed indices
# Worst case, O(n**2) when size is close to pop_size
# Floyd's algorithm
idx = np.empty(size, dtype=np.int64)
idx_data = <int64_t*>np.PyArray_DATA(<np.ndarray>idx)
idx_set = set()
loc = 0
# Sample indices with one pass to avoid reacquiring the lock
with self.lock:
for j in range(pop_size_i - size_i, pop_size_i):
idx_data[loc] = random_interval(&self._bitgen, j)
loc += 1
loc = 0
while len(idx_set) < size_i:
# smallest power of 2 larger than 1.2 * size
set_size = <uint64_t>(1.2 * size_i)
mask = _gen_mask(set_size)
set_size = 1 + mask
hash_set = np.full(set_size, <uint64_t>-1, np.uint64)
with self.lock, cython.wraparound(False), nogil:
for j in range(pop_size_i - size_i, pop_size_i):
if idx_data[loc] not in idx_set:
val = idx_data[loc]
else:
idx_data[loc] = val = j
idx_set.add(val)
loc += 1
val = random_bounded_uint64(&self._bitgen, 0, j, 0, 0)
loc = val & mask
while hash_set[loc] != <uint64_t>-1 and hash_set[loc] != <uint64_t>val:
loc = (loc + 1) & mask
if hash_set[loc] == <uint64_t>-1: # then val not in hash_set
hash_set[loc] = val
idx_data[j - pop_size_i + size_i] = val
else: # we need to insert j instead
loc = j & mask
while hash_set[loc] != <uint64_t>-1:
loc = (loc + 1) & mask
hash_set[loc] = j
idx_data[j - pop_size_i + size_i] = j
if shuffle:
self._shuffle_int(size_i, 1, idx_data)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason not to reuse the existing shuffle or _shuffle_raw ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added _shuffle_int as a specialized version of _shuffle_raw because it's ~30% faster for integers.

if shape is not None:
idx.shape = shape

Expand Down Expand Up @@ -3888,6 +3898,28 @@ cdef class Generator:
string.memcpy(data + j * stride, data + i * stride, itemsize)
string.memcpy(data + i * stride, buf, itemsize)

cdef inline void _shuffle_int(self, np.npy_intp n, np.npy_intp first,
int64_t* data) nogil:
"""
Parameters
----------
n
Number of elements in data
first
First observation to shuffle. Shuffles n-1,
n-2, ..., first, so that when first=1 the entire
array is shuffled
data
Location of data
"""
cdef np.npy_intp i, j
cdef int64_t temp
for i in reversed(range(first, n)):
j = random_bounded_uint64(&self._bitgen, 0, i, 0, 0)
temp = data[j]
data[j] = data[i]
data[i] = temp

def permutation(self, object x):
"""
permutation(x)
Expand Down
7 changes: 5 additions & 2 deletions numpy/random/tests/test_generator_mt19937.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,10 @@ def test_choice_nonuniform_replace(self):
def test_choice_uniform_noreplace(self):
random = Generator(MT19937(self.seed))
actual = random.choice(4, 3, replace=False)
desired = np.array([0, 1, 3], dtype=np.int64)
desired = np.array([2, 0, 3], dtype=np.int64)
assert_array_equal(actual, desired)
actual = random.choice(4, 4, replace=False, shuffle=False)
desired = np.arange(4, dtype=np.int64)
assert_array_equal(actual, desired)

def test_choice_nonuniform_noreplace(self):
Expand Down Expand Up @@ -688,7 +691,7 @@ def test_choice_return_type(self):
def test_choice_large_sample(self):
import hashlib

choice_hash = '5ca163da624c938bb3bc93e89a7dec4c'
choice_hash = 'd44962a0b1e92f4a3373c23222244e21'
random = Generator(MT19937(self.seed))
actual = random.choice(10000, 5000, replace=False)
if sys.byteorder != 'little':
Expand Down
0