Repository navigation
perf: hoist the randint dtype-bounds table out of the per-call path - #199
intel-python-devops wants to merge 8 commits into
Conversation
|
I pushed a follow-up commit on top of the original change 88c3bc5. I also shortened the comments and updated the description. The previous one said |
…og entry _choose_randint_type now reads both _rand_* method names from the table instead of formatting them on every call.
The table holds method names, not methods, and is keyed by np.dtype so that dtype aliases share an entry. The changelog entry now names the user-visible effect instead of implementation details.
randint dtype-bounds table out of the per-call hot pat
antonwolfy
left a comment
There was a problem hiding this comment.
Thanks, this looks good now.
I pushed d0ecacb on top. It rewords the comment above _randint_bounds: the table holds method names, and it is keyed by np.dtype so that aliases share an entry. It also makes the changelog entry describe the user-visible effect. I fixed the truncated PR title and two sentences in the description: the table stores method names, not the dtype name, and the dict rebuild is ~0.1–0.2 µs. The first commit's headline is still cut off ("...hot pat"), so please squash-merge, which uses the fixed title as the headline.
I rebuilt 583b424 and its base 2691015 and compared them (d0ecacb only touches a comment and the changelog):
- the test suite passes on both (380 passed, 24 skipped);
- about 2,270 seeded calls across all dtypes, sizes, full-range bounds, array bounds and brngs give identical output and generator state;
- about 1,100 dtype-like inputs behave the same through every public
randint, including aliases, dtypes with metadata and non-native byte order; - dtype hashing and equality agree on NumPy 1.26.4, 2.2.6 and 2.5.4, so the dtype-keyed lookup works on all supported versions.
Timings with NumPy 2.5.4, each build in its own process pinned to one P-core, 7 alternating rounds, median of each round's best time:
| call | base | PR | change |
|---|---|---|---|
randint(0, 10) |
1.92 µs | 0.81 µs | −58% |
randint(0, 10, size=1) |
3.22 µs | 2.02 µs | −37% |
randint(0, 10, dtype=np.uint8) |
1.90 µs | 0.76 µs | −60% |
randint(0, 10, dtype='int32') |
1.89 µs | 0.71 µs | −62% |
| array bounds, 3 elements | 13.4 µs | 11.9 µs | −11% |
| 0.5–30 ms calls (int64/int32/uint8/bool, scalar and array bounds) | within noise (0.99–1.01×) |
np.dtype(dt).name alone takes ~0.77 µs here, so it accounts for most of the saving, as the description says.
Note
This pull request is entirely AI-generated. Please review thoroughly.
mkl_random/mklrand.pyx:_choose_randint_typeruns on everyrandintcall. Most of its cost was per-call overhead, not generation:_randint_boundsdict on every call. Cython already folds the2**nconstants and caches the tuples, so this was just the dict construction, ~0.1–0.2 µs;np.dtype(dtype).name. That is a Python-level property in NumPy and costs ~1.1 µs, about 40% of arandint(0, 10)call.The table is now a module-level constant keyed by the
np.dtypeobject. A dtype hash lookup takes ~25 ns, and aliases such aslong,longlongandintcresolve to the same entry. Each entry also stores the names of the_rand_*and_rand_*_broadcastmethods, so a call is one dict lookup and twogetattrs, with no f-strings..nameis now only used to build theTypeErrormessage for unsupported dtypes. Generated values, dispatch and validation are unchanged.randint(0, 10)randint(0, 10, size=1)Large outputs (1–30 ms calls, scalar and array bounds,
int64/int32/uint8/bool) show no measurable change, and for a fixed seed the output is identical to master. The gain is only in per-call overhead.