-
-
Notifications
You must be signed in to change notification settings - Fork 32.7k
Closed
Labels
docsDocumentation in the Doc dirDocumentation in the Doc dirperformancePerformance or resource usagePerformance or resource usage
Description
Documentation
The "Slow path for general iterables" somewhat reinvents operator.indexOf
. it seems faster to use it, and could show off an effective combination of the other itertools.
Benchmark with the current implementation and two new alternatives, finding the indices of 0
in a million random digits:
108.1 ms ± 0.1 ms iter_index_current
39.7 ms ± 0.3 ms iter_index_new1
31.0 ms ± 0.1 ms iter_index_new2
Python version:
3.10.8 (main, Oct 11 2022, 11:35:05) [GCC 11.3.0]
Code of all three (just the slow path portion):
def iter_index_current(iterable, value, start=0):
it = islice(iterable, start, None)
for i, element in enumerate(it, start):
if element is value or element == value:
yield i
def iter_index_new1(iterable, value, start=0):
it = islice(iterable, start, None)
i = start - 1
try:
while True:
yield (i := i+1 + operator.indexOf(it, value))
except ValueError:
pass
def iter_index_new2(iterable, value, start=0):
it = iter(iterable)
consume(it, start)
i = start - 1
try:
for d in starmap(operator.indexOf, repeat((it, value))):
yield (i := i + (1 + d))
except ValueError:
pass
The new1
version is written to be similar to the "Fast path for sequences". The new2
version has optimizations and uses three more itertools /recipes. Besides using starmap(...)
, the optimizations are:
- Not piping all values through an
islice
iterator, instead only usingconsume
to advance the direct iteratorit
, then usingit
. - Add
1
tod
, which is more often one of the existing small ints (up to 256) than adding1
toi
.
Rest of benchmark script
funcs = iter_index_current, iter_index_new1, iter_index_new2
from itertools import islice, repeat, starmap
from timeit import timeit
import collections
import random
from statistics import mean, stdev
import sys
import operator
# Existing recipe
def consume(iterator, n=None):
if n is None:
collections.deque(iterator, maxlen=0)
else:
next(islice(iterator, n, n), None)
# Test arguments
iterable = random.choices(range(10), k=10**6)
value = 0
start = 100
def args():
return iter(iterable), value, start
# Correctness
expect = list(funcs[0](*args()))
for f in funcs[1:]:
print(list(f(*args())) == expect, f.__name__)
# For timing
times = {f: [] for f in funcs}
def stats(f):
ts = [t*1e3 for t in sorted(times[f])[:5]]
return f'{mean(ts):6.1f} ms ± {stdev(ts):3.1f} ms '
# Timing
number = 1
for _ in range(25):
for f in funcs:
t = timeit(lambda: consume(f(*args())), number=number) / number
times[f].append(t)
# Timing results
for f in sorted(funcs, key=stats, reverse=True):
print(stats(f), f.__name__)
print('\nPython version:')
print(sys.version)
Linked PRs
Metadata
Metadata
Assignees
Labels
docsDocumentation in the Doc dirDocumentation in the Doc dirperformancePerformance or resource usagePerformance or resource usage