-
-
Notifications
You must be signed in to change notification settings - Fork 33.3k
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:
passThe 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
isliceiterator, instead only usingconsumeto advance the direct iteratorit, then usingit. - Add
1tod, which is more often one of the existing small ints (up to 256) than adding1toi.
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