From 464627feb864b6a593d28ea2f983a6e1c32738d9 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 20 Aug 2026 09:32:45 +0300 Subject: [PATCH 1/2] Add benchmark for get_flattened_data --- Tests/benchmarks.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Tests/benchmarks.py b/Tests/benchmarks.py index 11ed3cf743b..7bada08ba0c 100644 --- a/Tests/benchmarks.py +++ b/Tests/benchmarks.py @@ -898,3 +898,15 @@ def test_quantize_to_palette( result = bench(lambda: im._new(im.im.convert(output_mode, dither, palette.im))) assert result.mode == output_mode benchmark_save(result) + + +@pytest.mark.benchmark +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("size", SIZES, ids=_format_size) +def test_get_flattened_data( + bench: BenchmarkFixture, + mode: str, + size: tuple[int, int], +) -> None: + im = make_pillow_image(mode, size) + bench(im.get_flattened_data) From eb40a75d0a974c9ae125fe963b87dee79f8e5e9d Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 20 Aug 2026 11:08:28 +0300 Subject: [PATCH 2/2] Speed up getpixel() (and get_flattened_data() as a side effect) * Build N-tuples by hand instead of Py_BuildValue parsing format strings over and over * Tell the Python GC that the returned tuple contains no objects that may cause cycles --- src/_imaging.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/_imaging.c b/src/_imaging.c index 9bdb6328782..bbaf83f307f 100644 --- a/src/_imaging.c +++ b/src/_imaging.c @@ -523,6 +523,27 @@ float16tofloat32(const FLOAT16 in) { return out[0]; } +static inline PyObject * +make_pixel_tuple(const UINT8 *b, Py_ssize_t bands) { + PyObject *tuple = PyTuple_New(bands); + if (tuple == NULL) { + return NULL; + } + for (Py_ssize_t i = 0; i < bands; i++) { + PyObject *v = PyLong_FromLong(b[i]); + if (v == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, v); + } + // We know these tuples will only have small integers, + // so we can tell the garbage collector to not look inside + // for cycles. + PyObject_GC_UnTrack(tuple); + return tuple; +} + static inline PyObject * getpixel(Imaging im, ImagingAccess access, int x, int y) { union { @@ -552,13 +573,11 @@ getpixel(Imaging im, ImagingAccess access, int x, int y) { case 1: return PyLong_FromLong(pixel.b[0]); case 2: - return Py_BuildValue("BB", pixel.b[0], pixel.b[1]); + return make_pixel_tuple(pixel.b, 2); case 3: - return Py_BuildValue("BBB", pixel.b[0], pixel.b[1], pixel.b[2]); + return make_pixel_tuple(pixel.b, 3); case 4: - return Py_BuildValue( - "BBBB", pixel.b[0], pixel.b[1], pixel.b[2], pixel.b[3] - ); + return make_pixel_tuple(pixel.b, 4); } break; case IMAGING_TYPE_INT32: