Skip to content

ENH: speed up the fixed-width string casts and unicode scalars - #32550

Merged
ngoldbaum merged 2 commits into
numpy:mainfrom
eendebakpt:perf/unicode-scalar
Sep 18, 2026
Merged

ngoldbaum merged 2 commits into
numpy:mainfrom
eendebakpt:perf/unicode-scalar

Conversation

@eendebakpt

Copy link
Copy Markdown
Contributor

PR summary

Fixes #11014 and improves string conversions in general:

  • The cast loops build an array scalar per element.** STRING/UNICODE/VOID_to_<T> in arraytypes.c.src calls PyArray_Scalar for every element and hands the result to the destination's setitem. For S/U sources the plain bytes/str from getitem behaves identically in every setitem and is cheaper
  • PyArray_Scalar copies when it does not have to. If no swap is needed, we now skip the copy.
  • Replace Py_BuildValue, with PyTuple_FromArray instead of Py_BuildValue
main PR
np.array(U_arr, dtype=int) 60.3 ms 20.8 ms 2.9x
U_arr.astype(np.float64) 68.5 ms 33.0 ms 2.1x
S_arr.astype(np.int64) 20.9 ms 15.1 ms 1.4x
list(U_arr) 35.1 ms 23.9 ms 1.47x
list(U_arr), byte-swapped 35.8 ms 32.5 ms 1.10x
control: U_arr.tolist() 7.11 ms 6.98 ms 1.02x
benchmark script
import timeit

import numpy as np

N = 200_000
sa = np.array(list(map(str, range(N))))            # 'U6'
fa = np.array([f"{i}.5" for i in range(N)])        # 'U8'
ba = sa.astype("S")                                # 'S6'
bs = sa.astype(">U6")
lst = sa.tolist()

CASES = {
    "np.array(U_arr, dtype=int)": lambda: np.array(sa, dtype=int),
    "np.array(U_arr.tolist(), int)": lambda: np.array(lst, dtype=int),
    "U -> float64": lambda: fa.astype(np.float64),
    "S -> int64": lambda: ba.astype(np.int64),
    "list(U_arr)": lambda: list(sa),
    "list(byteswapped)": lambda: list(bs),
    "U_arr.tolist()": lambda: sa.tolist(),         # control, unaffected
}

print(np.__version__)
for name, fn in CASES.items():
    t = min(timeit.repeat(fn, number=2, repeat=7)) / 2
    print(f"{name:32s} {t * 1e3:7.2f} ms  {t / N * 1e9:6.0f} ns/elem")

AI Disclosure

Generated with Claude Code

](#11232)

Two per-element overheads on the legacy `S`/`U` dtypes:

The `STRING/UNICODE/VOID_to_<T>` cast loops build a full array scalar per
element and hand it to the destination's setitem.  For S and U sources
that is wasteful: the plain bytes/str returned by getitem behaves
identically in every setitem and is much cheaper to create.  Keep
`PyArray_Scalar` for VOID sources (where it is what makes
`void.astype(bool)` agree with `bool(void)`, numpygh-9847) and for VOID
destinations (`VOID_setitem` reads the source through the buffer
protocol, which `np.str_` supports but `str` does not).

`PyArray_Scalar` itself, which also backs indexing and iteration,
unconditionally allocated a scratch buffer and copied the element into it
before handing it to `PyUnicode_FromKindAndData`.  The copy is only
needed to byte-swap, so skip it when the data is native-endian and
suitably aligned, and build the argument tuple with `PyTuple_FromArray`
instead of `Py_BuildValue`.

Measured on 2e5 elements: `U -> int64` 2.9x faster, `U -> float64` 2.1x,
`S -> int64` 1.4x, `list(U_arr)` 1.47x.

Closes numpygh-11014

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
@ngoldbaum

Copy link
Copy Markdown
Member

Nice this looks good to me!

Can we get a performance release note fragment?

@ngoldbaum ngoldbaum added the 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes label Sep 18, 2026
@ngoldbaum ngoldbaum added this to the 2.6.0 Release milestone Sep 18, 2026

@ngoldbaum ngoldbaum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @eendebakpt!

@ngoldbaum
ngoldbaum merged commit 9b2580b into numpy:main Sep 18, 2026
91 checks passed
antareepsarkar pushed a commit to antareepsarkar/numpy that referenced this pull request Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

01 - Enhancement 56 - Needs Release Note. Needs an entry in doc/release/upcoming_changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

np.array(string_array.tolist(), dtype=int) is faster than without the .tolist()

2 participants