Skip to content

gh-158592: Skip reallocation when shrinking a small list - #158787

Open
corona10 wants to merge 1 commit into
python:mainfrom
corona10:gh-158592
Open

corona10 wants to merge 1 commit into
python:mainfrom
corona10:gh-158592

Conversation

@corona10

@corona10 corona10 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Co-authored-by: Ilya Egorov <0x42005e1f@gmail.com>
@corona10
corona10 marked this pull request as draft October 4, 2026 03:09
@corona10

corona10 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member Author

cc @x42005e1f @ZeroIntensity @picnixz

Here is the data (based on unittests, bm_nbody, bm_fannkuch), why I choose this way.
Shows a similar distribution.

allocated shrink calls % of calls cumulative realloc to same capacity bytes / real realloc
<= 4 351,958 27.8% 27.8% 351,935 (100%) 0 B
5 - 8 718,926 56.9% 84.7% 290,077 (40%) 38 B
9 - 16 158,291 12.5% 97.2% 0 45 B
17 - 32 30,160 2.4% 99.6% 0 121 B
33 - 64 4,031 0.3% 99.9% 0 258 B
65 - 128 646 0.05% 100.0% 0 404 B
> 128 501 0.04% 0 1.2 KB - 267 KB
total 1,264,513 642,012 (51%)

And here is the microbenchmark

Free-threading

main vs this PR

Benchmark main this PR
append+del[-1] size=0 14.8 ns 16.4 ns: 1.11x slower
append+pop size=0 20.7 ns 22.4 ns: 1.08x slower
append+del[-1] size=1 14.9 ns 16.4 ns: 1.10x slower
append+pop size=1 19.2 ns 13.7 ns: 1.40x faster
append+del[-1] size=3 15.0 ns 16.5 ns: 1.10x slower
append+pop size=3 18.3 ns 13.8 ns: 1.33x faster
append+del[-1] size=7 15.1 ns 16.3 ns: 1.08x slower
append+pop size=7 13.3 ns 13.7 ns: 1.03x slower
append+del[-1] size=15 15.0 ns 16.5 ns: 1.10x slower
append+pop size=15 13.4 ns 13.7 ns: 1.02x slower
append+del[-1] size=100 15.2 ns 16.3 ns: 1.08x slower
append+pop size=100 13.4 ns 13.7 ns: 1.02x slower
extend+del[S:] S=1 45.9 ns 40.4 ns: 1.14x faster
extend+del[S:] S=2 45.7 ns 41.0 ns: 1.11x faster
extend+del[S:] S=4 41.9 ns 43.3 ns: 1.03x slower
extend+del[S:] S=16 51.6 ns 52.3 ns: 1.01x slower
extend+del[S:] S=32 62.1 ns 63.5 ns: 1.02x slower
extend+del[S:] S=64 84.6 ns 87.5 ns: 1.03x slower
extend+del[S:] S=256 233 ns 243 ns: 1.04x slower
copy only N=32 69.3 ns 71.2 ns: 1.03x slower
del[-1] x N N=32 465 ns 573 ns: 1.23x slower
copy only N=64 90.6 ns 92.9 ns: 1.03x slower
del[-1] x N N=64 863 ns 1.04 us: 1.21x slower
copy only N=256 228 ns 232 ns: 1.02x slower
del[-1] x N N=256 3.29 us 3.71 us: 1.13x slower
del[-1] x N N=1024 12.9 us 14.2 us: 1.10x slower
copy only N=4096 6.12 us 6.23 us: 1.02x slower
del[-1] x N N=4096 70.6 us 77.6 us: 1.10x slower
copy only N=65536 108 us 111 us: 1.04x slower
del[-1] x N N=65536 1.18 ms 1.31 ms: 1.10x slower
del[0] x N N=256 12.3 us 12.9 us: 1.05x slower
del[0] x N N=4096 2.28 ms 2.31 ms: 1.02x slower
Geometric mean (ref) 1.03x slower

Benchmark hidden because not significant (4): extend+del[S:] S=8, extend+del[S:] S=1024, extend+del[S:] S=4096, copy only N=1024

main vs #158602 alone

Benchmark main #158602
append+del[-1] size=0 14.8 ns 31.0 ns: 2.09x slower
append+pop size=0 20.7 ns 22.0 ns: 1.06x slower
append+del[-1] size=1 14.9 ns 23.1 ns: 1.55x slower
append+pop size=1 19.2 ns 20.5 ns: 1.07x slower
append+del[-1] size=3 15.0 ns 21.7 ns: 1.44x slower
append+pop size=3 18.3 ns 19.2 ns: 1.05x slower
append+del[-1] size=7 15.1 ns 16.1 ns: 1.07x slower
append+pop size=7 13.3 ns 13.6 ns: 1.02x slower
append+del[-1] size=15 15.0 ns 16.3 ns: 1.08x slower
append+del[-1] size=100 15.2 ns 16.1 ns: 1.06x slower
append+pop size=100 13.4 ns 13.6 ns: 1.02x slower
extend+del[S:] S=1 45.9 ns 47.4 ns: 1.03x slower
extend+del[S:] S=2 45.7 ns 46.5 ns: 1.02x slower
extend+del[S:] S=4 41.9 ns 43.0 ns: 1.03x slower
extend+del[S:] S=16 51.6 ns 52.6 ns: 1.02x slower
extend+del[S:] S=64 84.6 ns 88.7 ns: 1.05x slower
extend+del[S:] S=256 233 ns 240 ns: 1.03x slower
del[-1] x N N=32 465 ns 559 ns: 1.20x slower
del[-1] x N N=64 863 ns 1.01 us: 1.17x slower
copy only N=256 228 ns 233 ns: 1.02x slower
del[-1] x N N=256 3.29 us 3.64 us: 1.11x slower
del[-1] x N N=1024 12.9 us 14.0 us: 1.09x slower
copy only N=4096 6.12 us 6.23 us: 1.02x slower
del[-1] x N N=4096 70.6 us 75.3 us: 1.07x slower
copy only N=65536 108 us 110 us: 1.02x slower
del[-1] x N N=65536 1.18 ms 1.25 ms: 1.06x slower
del[0] x N N=256 12.3 us 12.8 us: 1.04x slower
Geometric mean (ref) 1.08x slower

Default build

main vs this PR

Benchmark main this PR
append+del[-1] size=0 12.8 ns 13.9 ns: 1.08x slower
append+pop size=0 15.5 ns 16.7 ns: 1.08x slower
append+del[-1] size=1 12.7 ns 13.8 ns: 1.09x slower
append+pop size=1 12.6 ns 11.4 ns: 1.10x faster
append+del[-1] size=3 13.0 ns 13.9 ns: 1.07x slower
append+pop size=3 12.8 ns 11.4 ns: 1.13x faster
append+del[-1] size=7 13.1 ns 13.6 ns: 1.04x slower
append+pop size=7 10.7 ns 11.2 ns: 1.05x slower
append+del[-1] size=15 13.1 ns 13.5 ns: 1.03x slower
append+pop size=15 10.8 ns 11.2 ns: 1.04x slower
append+del[-1] size=100 13.1 ns 13.6 ns: 1.04x slower
append+pop size=100 10.9 ns 11.2 ns: 1.03x slower
extend+del[S:] S=1 34.3 ns 33.8 ns: 1.02x faster
extend+del[S:] S=2 35.7 ns 34.6 ns: 1.03x faster
extend+del[S:] S=16 45.5 ns 44.9 ns: 1.01x faster
extend+del[S:] S=4096 2.91 us 2.75 us: 1.06x faster
copy only N=32 62.0 ns 62.8 ns: 1.01x slower
del[-1] x N N=32 411 ns 489 ns: 1.19x slower
del[-1] x N N=64 744 ns 872 ns: 1.17x slower
copy only N=256 209 ns 206 ns: 1.01x faster
del[-1] x N N=256 2.82 us 3.14 us: 1.11x slower
del[-1] x N N=1024 10.9 us 11.7 us: 1.08x slower
del[-1] x N N=4096 57.5 us 60.1 us: 1.05x slower
copy only N=65536 68.8 us 68.3 us: 1.01x faster
del[-1] x N N=65536 983 us 1.02 ms: 1.04x slower
del[0] x N N=256 4.27 us 4.85 us: 1.14x slower
del[0] x N N=4096 616 us 627 us: 1.02x slower
Geometric mean (ref) 1.03x slower

Benchmark hidden because not significant (9): extend+del[S:] S=4, S=8, S=32, S=64, S=256, S=1024, copy only N=64, N=1024, N=4096

main vs #158602 alone

Benchmark main #158602
append+del[-1] size=0 12.8 ns 64.1 ns: 5.01x slower
append+pop size=0 15.5 ns 16.0 ns: 1.03x slower
append+del[-1] size=1 12.7 ns 15.3 ns: 1.20x slower
append+del[-1] size=3 13.0 ns 15.5 ns: 1.19x slower
append+del[-1] size=7 13.1 ns 13.4 ns: 1.02x slower
append+pop size=7 10.7 ns 10.9 ns: 1.02x slower
append+del[-1] size=100 13.1 ns 13.3 ns: 1.02x slower
extend+del[S:] S=1 34.3 ns 33.4 ns: 1.03x faster
extend+del[S:] S=2 35.7 ns 34.8 ns: 1.03x faster
extend+del[S:] S=8 46.7 ns 46.1 ns: 1.01x faster
extend+del[S:] S=32 55.0 ns 53.6 ns: 1.03x faster
del[-1] x N N=32 411 ns 466 ns: 1.13x slower
del[-1] x N N=64 744 ns 853 ns: 1.15x slower
del[-1] x N N=256 2.82 us 3.18 us: 1.13x slower
del[-1] x N N=1024 10.9 us 11.5 us: 1.06x slower
del[-1] x N N=4096 57.5 us 60.4 us: 1.05x slower
del[-1] x N N=65536 983 us 1.01 ms: 1.02x slower
del[0] x N N=256 4.27 us 4.85 us: 1.13x slower
del[0] x N N=4096 616 us 627 us: 1.02x slower
Geometric mean (ref) 1.07x slower

Benchmark hidden because not significant (17): append+pop size=1, 3, 15, 100; append+del[-1] size=15; extend+del[S:] S=4, 16, 64, 256, 1024, 4096; copy only N=32, 64, 256, 1024, 4096, 65536

Notes on the default build:

Script

import pyperf

INNER = 1000


def append_del_last(obj, inner=INNER):
    for _ in range(inner):
        obj.append(None)
        del obj[-1]


def append_pop(obj, inner=INNER):
    for _ in range(inner):
        obj.append(None)
        obj.pop()


def extend_del_slice(obj, blk, size, inner):
    for _ in range(inner):
        obj.extend(blk)
        del obj[size:]


def copy_only(blk):
    obj = blk[:]
    return obj


def del_last_repeatedly(blk, r):
    obj = blk[:]
    for _ in r:
        del obj[-1]
    return obj


def del_first_repeatedly(blk, r):
    obj = blk[:]
    for _ in r:
        del obj[0]
    return obj


def main():
    runner = pyperf.Runner()

    for size in (0, 1, 3, 7, 15, 100):
        obj = [None] * size
        runner.bench_func(f"append+del[-1] size={size}", append_del_last,
                          obj, inner_loops=INNER)
        obj = [None] * size
        runner.bench_func(f"append+pop size={size}", append_pop,
                          obj, inner_loops=INNER)

    for size in (1, 2, 4, 8, 16, 32, 64, 256, 1024, 4096):
        inner = 100 if size <= 64 else 10
        obj = [None] * size
        blk = [None] * size
        runner.bench_func(f"extend+del[S:] S={size}", extend_del_slice,
                          obj, blk, size, inner, inner_loops=inner)

    for n in (32, 64, 256, 1024, 4096, 65536):
        blk = list(range(n))
        r = range(n - 1)
        runner.bench_func(f"copy only N={n}", copy_only, blk)
        runner.bench_func(f"del[-1] x N N={n}", del_last_repeatedly, blk, r)

    for n in (256, 4096):
        blk = list(range(n))
        r = range(n - 1)
        runner.bench_func(f"del[0] x N N={n}", del_first_repeatedly, blk, r)


if __name__ == "__main__":
    main()

A nice side effect of this approach is that list.pop() benefits as well
(1.33x-1.40x faster on small lists), since it already goes through
list_resize().

@corona10
corona10 marked this pull request as ready for review October 4, 2026 03:28
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.

1 participant