Skip to content

Simplify code to improve performance - #9860

Open
hugovk wants to merge 6 commits into
python-pillow:mainfrom
hugovk:stdlib-src
Open

Simplify code to improve performance#9860
hugovk wants to merge 6 commits into
python-pillow:mainfrom
hugovk:stdlib-src

Conversation

@hugovk

@hugovk hugovk commented Aug 9, 2026

Copy link
Copy Markdown
Member
  • Replace functools.reduce with sum
  • Replace functools.reduce and operator.addwithsum`
  • Replace loop with sum
  • Simplify dict key sorting
  • Replace Python 2's hasattr(sys, "pypy_version_info") with more clearer sys.implementation.name == "pypy" (similar to Replace custom test code #9858)
  • Replace int(color[i:j], 16) slices with bytes.fromhex()
script
import functools
import operator
import pathlib
import timeit

from PIL import Image

RESULTS = []


def bench(name, old, new, number):
    assert old() == new(), f"{name}: implementations disagree"
    t_old = min(timeit.repeat(old, number=number, repeat=5)) / number
    t_new = min(timeit.repeat(new, number=number, repeat=5)) / number
    RESULTS.append((name, t_old, t_new))


# ---------------------------------------------------------------------------
# 1. ImageFilter.Kernel scale: reduce(lambda) vs sum
gauss3 = (1.0, 2.0, 1.0, 2.0, 4.0, 2.0, 1.0, 2.0, 1.0)
gauss5 = tuple(x * y / 16.0 for x in (1, 4, 6, 4, 1) for y in (1, 4, 6, 4, 1))

for label, kernel in [("3x3 float", gauss3), ("5x5 float", gauss5)]:
    bench(
        f"1. Kernel scale ({label})",
        lambda k=kernel: functools.reduce(lambda a, b: a + b, k),
        lambda k=kernel: sum(k),
        number=200_000,
    )

# ---------------------------------------------------------------------------
# 2./3. ImageOps histogram sums, on a real image histogram
hopper_path = pathlib.Path("Tests") / "images" / "hopper.ppm"
with Image.open(hopper_path) as hopper_im:
    hopper_rgb = hopper_im.convert("RGB")
    histogram = hopper_rgb.histogram()

# equalise: nonzero buckets of one 256-bucket band (mirrors the filter there)
histo = [_f for _f in histogram[0:256] if _f]
assert len(histo) > 1

bench(
    f"2. equalise sum ({len(histo)} nonzero buckets)",
    lambda: (functools.reduce(operator.add, histo) - histo[-1]) // 255,
    lambda: (sum(histo) - histo[-1]) // 255,
    number=100_000,
)

# autocontrast: the full unfiltered 256-bucket band
h = histogram[0:256]


def old_count():
    n = 0
    for ix in range(256):
        n = n + h[ix]
    return n


bench("3. autocontrast count (256)", old_count, lambda: sum(h), number=100_000)

# ---------------------------------------------------------------------------
# 4. ImageColor hex parse: three int() slices vs bytes.fromhex
color = "#4080c0"

bench(
    "4. ImageColor #rrggbb parse",
    lambda: (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16)),
    lambda: tuple(bytes.fromhex(color[1:7])),
    number=500_000,
)

# ---------------------------------------------------------------------------
# 5. PdfParser deleted keys: sorted(set(d.keys())) vs sorted(d)
for label, deleted_entries in [
    ("1 key", {0: 65536}),
    ("8 keys", {0: 65536, **{i * 7: 0 for i in range(1, 8)}}),
]:
    bench(
        f"5. sorted deleted keys ({label})",
        lambda d=deleted_entries: sorted(set(d.keys())),
        lambda d=deleted_entries: sorted(d),
        number=100_000 if len(deleted_entries) < 100 else 20_000,
    )


# ---------------------------------------------------------------------------
def fmt(t):
    if t != t:  # nan
        return "-"
    for unit, scale in (("s", 1), ("ms", 1e3), ("us", 1e6), ("ns", 1e9)):
        if t * scale >= 1:
            return f"{t * scale:8.2f} {unit}"
    return f"{t * 1e9:8.2f} ns"


print(f"{'benchmark':<44} {'old':>12} {'new':>12} {'speedup':>8}")
for name, t_old, t_new in RESULTS:
    ratio = f"{t_old / t_new:7.2f}x" if t_new == t_new and t_new else ""
    print(f"{name:<44} {fmt(t_old):>12} {fmt(t_new):>12} {ratio:>8}")
benchmark                                             old          new  speedup
1. Kernel scale (3x3 float)                     318.35 ns     76.38 ns    4.17x
1. Kernel scale (5x5 float)                     698.84 ns    114.58 ns    6.10x
2. equalise sum (256 nonzero buckets)             3.59 us    656.71 ns    5.47x
3. autocontrast count (256)                       3.82 us    632.53 ns    6.04x
4. ImageColor #rrggbb parse                     180.65 ns    144.36 ns    1.25x
5. sorted deleted keys (1 key)                  149.04 ns     73.12 ns    2.04x
5. sorted deleted keys (8 keys)                 290.87 ns    101.38 ns    2.87x

Comment thread src/PIL/ImageColor.py
Comment on lines +65 to +66
r, g, b = bytes.fromhex(color[1:])
return r, g, b

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might as well spare the unpack-repack:

Suggested change
r, g, b = bytes.fromhex(color[1:])
return r, g, b
return tuple(bytes.fromhex(color[1:])) # r, g, b

Comment thread src/PIL/ImageColor.py
Comment on lines +69 to +70
r, g, b, a = bytes.fromhex(color[1:])
return r, g, b, a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Might as well spare the unpack-repack here too:

Suggested change
r, g, b, a = bytes.fromhex(color[1:])
return r, g, b, a
return tuple(bytes.fromhex(color[1:])) # r, g, b, a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants