Skip to content

GH-132554: Specialize GET_ITER and FOR_ITER for range - #135063

Open
markshannon wants to merge 24 commits into
python:mainfrom
faster-cpython:specialize-for-iter-range
Open

GH-132554: Specialize GET_ITER and FOR_ITER for range#135063
markshannon wants to merge 24 commits into
python:mainfrom
faster-cpython:specialize-for-iter-range

Conversation

@markshannon

@markshannonmarkshannon commented Jun 3, 2025

Copy link
Copy Markdown
Member

Extends the idea of "virtual iterators" to ranges as well. Most ranges have a step of one. For these ranges we can treat them much like a C for loop, using tagged integers for the current value and the limit.

The stack during iteration now looks like this:

Original iterable2nd on stacktop of stack
range (step=1)limit (tagged)current (tagged)
indexable objectiterableindex (tagged)
otheriteratorNULL

GET_ITER is specialized for the above cases plus any iterable with Py_TYPE(self)->tp_iter == PyObject_SelfIter which avoids the call to PyObject_GetIter; simply pushing NULL instead.

An "indexable item" is one that has fast indexing operations that can used instead of creating a new iterator object.
This includes list, tuple, bytes, bytearray and str objects.

Also fixes stats for FOR_ITER and GET_ITER.

@markshannonmarkshannon changed the title Specialize GET_ITER and FOR_ITER for rangeGH-132554: Specialize GET_ITER and FOR_ITER for rangeJun 3, 2025
Comment threadPython/bytecodes.c Outdated
}
else {
PyObject *iter_o = PyStackRef_AsPyObjectBorrow(iter);
next = _PyForIter_NextWithIndex(iter_o, null_or_index);

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.

The _PyForIter_NextWithIndex handles the exact lists and exact tuples.

In this PR we have the code to handle range iteration by pushing the index and limit to the stack. Could we simplify _PyForIter_NextWithIndex to only deal with lists and for tuples push the index and length of the tuple to the stack (e.g. use the same approach as range)?

(if this is possible maybe in a followup PR)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Are you proposing pushing a third value to the stack during iteration?
I doubt that would be worth it.

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.

Yes. Might not be worth it indeed, but I will check once this pr has settled.

@eendebakpt

Copy link
Copy Markdown
Contributor

Micro benchmarks look good:

for loop length 1: Mean +- std dev: [mainx] 152 ns +- 1 ns -> [prx] 124 ns +- 1 ns: 1.23x faster
repeat loop length 1: Mean +- std dev: [mainx] 234 ns +- 1 ns -> [prx] 227 ns +- 2 ns: 1.03x faster
for loop length 2: Mean +- std dev: [mainx] 169 ns +- 1 ns -> [prx] 142 ns +- 3 ns: 1.19x faster
repeat loop length 2: Mean +- std dev: [mainx] 252 ns +- 2 ns -> [prx] 245 ns +- 4 ns: 1.03x faster
for loop length 8: Mean +- std dev: [mainx] 291 ns +- 4 ns -> [prx] 256 ns +- 4 ns: 1.14x faster
repeat loop length 8: Mean +- std dev: [mainx] 367 ns +- 6 ns -> [prx] 359 ns +- 3 ns: 1.02x faster
for loop length 10000: Mean +- std dev: [mainx] 341 us +- 6 us -> [prx] 330 us +- 3 us: 1.03x faster
repeat loop length 10000: Mean +- std dev: [mainx] 254 us +- 3 us -> [prx] 258 us +- 2 us: 1.02x slower
Geometric mean: 1.08x faster
Script
import pyperf
runner = pyperf.Runner()
loop = """
import itertools
def g(n):
x=0
for ii in range(n):
x += 1
def repeat(n):
x = 0
r = itertools.repeat(None, n)
for ii in r:
x += 1
"""
for s in [1, 2, 8, 10_000]:
time = runner.timeit(name=f"for loop length {s}", stmt=f"g({s})", setup=loop)
time = runner.timeit(name=f"repeat loop length {s}", stmt=f"repeat({s})", setup=loop)

The repeat loop length x is a control benchmark: it does not involve a for loop, but is slightly faster because pyperf itself includes a for loop in the timing. That effect is relatively small though.

@markshannon
markshannon marked this pull request as ready for review June 9, 2025 10:33
@markshannon

Copy link
Copy Markdown
MemberAuthor

Performance is varied. Linux is unchanged, Windows shows an insignificant 0.3% speedup and Mac shows a suspiciously high 8% speedup.
Our Mac numbers have been a bit all over the place lately.
Since the aim of this is to support tagged ints and better JIT optimizations, not slower is good enough.

The stats show that GET_ITER is now well specialized. FOR_ITER_RANGE numbers are down a bit, presumably due to ranges with step != 1, but not worryingly so.

@python-cla-bot

python-cla-botBot commented Oct 7, 2025

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

uint16_t tp_versions_used;
/* Returns the object at the index given, or NULL if out-of-bounds
* Never raises an exception. */
iterindexfunc tp_iterindex;

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.

A comment above states:

If this structure is modified, Doc/includes/typestruct.h should be updated

Should that document be updated?

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 30 days with no activity.

@github-actionsgithub-actionsBot added the stale Stale PR or inactive for long period of time. label Apr 25, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting core reviewstaleStale PR or inactive for long period of time.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@markshannon@eendebakpt@nazeerali4325-commits