5 ms·
This is interesting, and probably worth knowing, but I would like to see when it would be worth preallocating instead of a comprehension. Maybe there is a situa
by dec0dedab0de 5y ago
This is interesting, and probably worth knowing, but I would like to see when it would be worth preallocating instead of a comprehension. Maybe there is a situation where the code is too complex for a comprehension directly, but putting it into a separate function for the comprehension to call would slow things down more than pre allocating the list.
EDIT:
I just tried it out and answered my own question.
length = 10_000
def func(i):
"""simplest function"""
return i
%%timeit
#preallocating list
pre = [None] * length
for i in range(length):
pre[i] = i
366 µs ± 2.03 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
#comprehension
comp = [i for i in range(length)]
267 µs ± 2.75 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
#preallocating list with function
pre_func = [None] * length
for i in range(length):
pre_func[i] = func(i)
1.04 ms ± 11.2 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
#comprehension with function
comp_func = [func(i) for i in range(length)]
844 µs ± 8.61 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
So it looks like the act of calling a function in a comprehension is definitely slower than preallocating a list and running the code in-line. I thought it would take more than 10,000 iterations to be faster, but I was very wrong. It turns out that even with 1 iteration it is faster to preallocate a list and do the code in-line, then to do a comprehension while calling a function.
It's still fast enough that I doubt I'll be doing this. I prefer lots of smaller functions to keep things maintainable, but now I'll keep in mind the cost of calling a function for code that is moving too slow.
EDIT2:
While working out a way to test this without using range I was about to do use enumerate, and instead I tested with enumerate and range to see what kind of an effect enumerate would add, and it turns out that it is even faster to just append the list than preallocate if you need to use enumerate, which you would likely need to do if your data doesn't already have it's own index.
%%timeit
#just appending
append = []
for i in range(length):
append.append(i)
551 µs ± 5.18 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%%timeit
#preallocating list with enumerate
pre = [None] \* length
for i, v in enumerate(range(length)):
pre[i] = v
625 µs ± 8.56 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
EDIT3:
since we know the length we could avoid using enumerate with zip(range(length), iterable), and that seems to be an improvement
%%timeit
#preallocating list, avoiding enumerate
pre = [None] * length
for i, v in zip(range(length), range(length)): #silly, but pretend the second one is actual data
pre[i] = v
463 µs ± 5.55 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
EDIT4:(I really shouldn't have drank coffee so late)
I just tried using map with list, and it was the fastest way to call a function, but still slower than all of the ways that didn't call a function. Again, I'm surprised it was faster than a comprehension, I thought they were effectively the same thing.
%%timeit
#map with range
map_func = list(map(func, range(length)))
676 µs ± 15.8 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
FYI: I did these tests in jupyter with python 3.10.0, on macOS 11.6 intel. I ran them more than once, and the numbers varied, but the order remained the same.
I'm not going any farther down this rabbithole. I always optimize for readability over speed anyway. But I would be curious if someone else could get faster speeds, or could explain why my tests weren't valid.
- chrsig 5y agowhen you know the number of elements you'll be putting in the list, you can save on resizing and copying. A comprehension doesn't always know how many elements it will require.
- dec0dedab0de 5y agoI think the article, and my test just showed that the comprehension is faster than preallocating. So if it's something that works well in a comprehension, you should do it that way. The preallocation is only an improvement when the code you're doing per loop is too complex to be done in a comprehension. My initial thought was that a function in a comprehension would be better than preallocation with code in the loop, but I was wrong. Unless you're suggesting that Python has a special optimization for the range generator in comprehensions, which is plausible, let me think of a way to test for that. EDIT: I changed it to iterate over the characters in my shadow file, and comprehensions were still faster. I'm not sure if that is enough to fake out possible optimizations, but it definitely rules out range. def pgen(): with open('/etc/passwd') as f: for char in f.read(): yield char def func(i): """simplest function""" return i length = len(list(pgen())) %%timeit #preallocating list without range pre = [None] * length for i, v in enumerate(pgen()): pre[i] = v 711 µs ± 14 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) %%timeit #preallocating list from custom generator, and zipping from range to avoid enumerate pre = [None] * length for i,v in zip(range(length), pgen()): pre[i] = v 687 µs ± 1.93 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) %%timeit #comprehension without range comp = [v for v in pgen()] 456 µs ± 6.48 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each) %%timeit #comprehension without range, and using enumerate to judge the difference fairly comp = [v for i, v in enumerate(pgen())] 648 µs ± 14.3 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
- bobbylarrybobby 5y agoPython does have a special optimization: when the iterable in a comprehension has len() defined, then Python preallocates the list. The comprehension is faster than manual preallocation+filling because the indexing happens in C instead of in Python.