Skip to content

Commit 90c35d5

Browse files
committed
Re-use of a PyLong for integer range iterator
- By not allocating a new PyLong on each iteration, we save significant time; in a benchmark with a large range a bit more than a factor of 2, with a small range a few percent (~8% with only interned ints) - To make sure this doesn't break when assigning to different variables using next(), we have to decrement the target object's reference counter before calling iternext(). We do this by checking if the next instruction is a STORE_FAST, and treat the FOR_ITER/STORE_FAST pair as a sort of super-instruction, saving some pushing and popping from the stack as well. - We do have to be careful with DECREFing before setting the new local, as mentioned in the comment to the SETLOCAL macro: 'This is because it is possible that during the DECREF the frame is accessed by other code (e.g. a __del__ method or gc.collect()) and the variable would be pointing to already-freed memory.' Therefore we restrict this only to long types with a ref count >1 - One caveat is that range will no longer result in interned integers, so 'for i in range(10)' will result in 'i is 5' always False.
1 parent f8a95df commit 90c35d5

File tree

2 files changed

+79
-10
lines changed

2 files changed

+79
-10
lines changed

Objects/rangeobject.c

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -770,16 +770,45 @@ typedef struct {
770770
long start;
771771
long step;
772772
long len;
773+
PyObject *fast_obj;
773774
} rangeiterobject;
774775

775776
static PyObject *
776777
rangeiter_next(rangeiterobject *r)
777778
{
778-
if (r->index < r->len)
779+
if (r->index < r->len) {
779780
/* cast to unsigned to avoid possible signed overflow
780781
in intermediate calculations. */
781-
return PyLong_FromLong((long)(r->start +
782-
(unsigned long)(r->index++) * r->step));
782+
long cur_val = (long)(r->start + (unsigned long)(r->index++) * r->step);
783+
784+
// If we are the only one holding a reference, modify object in place
785+
if (Py_REFCNT(r->fast_obj) == 1) {
786+
long abs_val, abs_val_msb;
787+
int sign;
788+
PyLongObject *l = (PyLongObject*)r->fast_obj;
789+
Py_INCREF(r->fast_obj);
790+
if (cur_val < 0) {
791+
abs_val = 0U - (unsigned long)cur_val;
792+
sign = -1;
793+
}
794+
else {
795+
abs_val = cur_val;
796+
sign = cur_val ? 1 : 0;
797+
}
798+
abs_val_msb = abs_val >> PYLONG_BITS_IN_DIGIT;
799+
if (!abs_val_msb)
800+
l->ob_digit[0] = Py_SAFE_DOWNCAST(abs_val, unsigned long, digit);
801+
else {
802+
sign *= 2;
803+
l->ob_digit[0] = Py_SAFE_DOWNCAST(abs_val&PyLong_MASK, unsigned long, digit);
804+
l->ob_digit[1] = Py_SAFE_DOWNCAST(abs_val_msb, unsigned long, digit);
805+
}
806+
Py_SET_SIZE(l, sign);
807+
return r->fast_obj;
808+
}
809+
else
810+
return PyLong_FromLong(cur_val);
811+
}
783812
return NULL;
784813
}
785814

@@ -837,6 +866,13 @@ rangeiter_setstate(rangeiterobject *r, PyObject *state)
837866
Py_RETURN_NONE;
838867
}
839868

869+
static void
870+
rangeiter_dealloc(rangeiterobject *r)
871+
{
872+
Py_XDECREF(r->fast_obj);
873+
PyObject_Free(r);
874+
}
875+
840876
PyDoc_STRVAR(reduce_doc, "Return state information for pickling.");
841877
PyDoc_STRVAR(setstate_doc, "Set state information for unpickling.");
842878

@@ -856,7 +892,7 @@ PyTypeObject PyRangeIter_Type = {
856892
sizeof(rangeiterobject), /* tp_basicsize */
857893
0, /* tp_itemsize */
858894
/* methods */
859-
(destructor)PyObject_Del, /* tp_dealloc */
895+
(destructor)rangeiter_dealloc, /* tp_dealloc */
860896
0, /* tp_vectorcall_offset */
861897
0, /* tp_getattr */
862898
0, /* tp_setattr */
@@ -921,6 +957,16 @@ fast_range_iter(long start, long stop, long step)
921957
unsigned long ulen;
922958
if (it == NULL)
923959
return NULL;
960+
961+
// Allocate fast return object; make sure it's a 2 digit PyLong
962+
// We hold a reference that we have to release in _dealloc
963+
it->fast_obj = (PyObject*)PyLong_FromLong(1L<<(PYLONG_BITS_IN_DIGIT+1));
964+
if (it->fast_obj == NULL)
965+
{
966+
Py_DECREF(it);
967+
return NULL;
968+
}
969+
924970
it->start = start;
925971
it->step = step;
926972
ulen = get_len_of_range(start, stop, step);

Python/ceval.c

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,7 +1842,6 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
18421842
}
18431843

18441844
case TARGET(STORE_FAST): {
1845-
PREDICTED(STORE_FAST);
18461845
PyObject *value = POP();
18471846
SETLOCAL(oparg, value);
18481847
DISPATCH();
@@ -3995,14 +3994,38 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, PyFrameObject *f, int throwflag)
39953994
case TARGET(FOR_ITER): {
39963995
PREDICTED(FOR_ITER);
39973996
/* before: [iter]; after: [iter, iter()] *or* [] */
3998-
PyObject *iter = TOP();
3999-
PyObject *next = (*Py_TYPE(iter)->tp_iternext)(iter);
3997+
PyObject *cur_local, *next, *iter = TOP();
3998+
3999+
// If FOR_ITER is followed by STORE_FAST, pre-release a reference on PyLong before
4000+
// calling iternext, indicating that the target object can be reused (e.g. for range())
4001+
int for_target, for_store_fast = _Py_OPCODE(*next_instr) == STORE_FAST ? 1 : 0;
4002+
if (for_store_fast) {
4003+
for_target = _Py_OPARG(*next_instr);
4004+
cur_local = GETLOCAL(for_target);
4005+
// Can't do this when ref count == 1, because an object should persist after last iteration
4006+
if (cur_local && PyLong_Check(cur_local) && Py_REFCNT(cur_local)>1) {
4007+
Py_DECREF(cur_local);
4008+
for_store_fast = 2; // Indicate reference count was already decremented
4009+
}
4010+
}
4011+
next = (*Py_TYPE(iter)->tp_iternext)(iter);
40004012
if (next != NULL) {
4001-
PUSH(next);
4002-
PREDICT(STORE_FAST);
4003-
PREDICT(UNPACK_SEQUENCE);
4013+
if (for_store_fast) {
4014+
if (for_store_fast == 2) // PyLong with ref count that was >1, no need for additional book keeping
4015+
GETLOCAL(for_target) = next;
4016+
else
4017+
SETLOCAL(for_target, next);
4018+
next_instr++; // STORE_FAST already performed
4019+
}
4020+
else {
4021+
PUSH(next);
4022+
PREDICT(UNPACK_SEQUENCE);
4023+
}
40044024
DISPATCH();
40054025
}
4026+
// Restore reference count of local if it was pre-decremented
4027+
if (for_store_fast == 2)
4028+
Py_INCREF(cur_local);
40064029
if (_PyErr_Occurred(tstate)) {
40074030
if (!_PyErr_ExceptionMatches(tstate, PyExc_StopIteration)) {
40084031
goto error;

0 commit comments

Comments
 (0)