List.count() Vs Counter() Performance
Solution 1:
Counter
is faster in theory, but has higher fixed overhead, especially compared to str.count
, which can scan the underlying C array with direct memory comparisons, where list.count
has to do rich comparisons for each element; converting moves
to a list
of single characters nearly triples the time for foo1
in local tests, from 448 ns to 1.3 μs (while foo2
actually gets a tiny bit faster, dropping from 5.6 μs to 5.48 μs).
Other problems:
- Importing an already imported module uses the cached import, but there is a surprising amount of overhead involved in even a cached import (the loading machinery has a lot of stuff to check to make sure it's okay to do so); in local tests, moving
from collections import Counter
to the top level reduced the runtime offoo2
by 1.6 μs (5.6 μs with single global import, 7.2 μs with local per-call import). This will vary a lot by environment; on another machine (with less stuff installed in both user and system site-packages), the overhead was only 0.75 μs. Regardless, it's a significant, avoidable disadvantage forfoo2
. Counter
on modern Python uses a C accelerator to speed up counting, but the accelerator only provides a benefit when the iterable is long enough. If you use thelist
form ofmoves
, but multiply it by 100 to make a longer sequence, the difference drops, relatively speaking (to 106 µs forfoo1
vs. 140 µs forfoo2
)- You're just not counting very many things; when there are only four things you care about, paying
O(n)
four times can easily beat payingO(n)
once if the former case has lower constant multipliers (which aren't included in big-O notation) than the latter.Counter
remainsO(n)
for any number of unique things being counted; calling.count
isO(n)
per call, but if you need to know the count of every unique thing in the input, for inputs that are mostly unique, individual.count
calls for each will be asymptoticallyO(n²)
. - The
.count
approach is short-circuiting in your specific case, so it isn't even doingO(n)
work four times, just twice; theU
andD
counts don't match, so it never countsL
andR
at all.Counter
doesn't get meaningfully slower if it can't short-circuit (all the cost is paid in the single counting pass), but yourfoo1
, in the same benchmark I used from point #2 (longer input, inlist
form), goes from 106 µs to 185 µs if I just add a singleD
to the end of the (pre-multiplication)moves
(making theU
andD
counts the same, and requiring two morecount
calls);foo2
only goes up to 143 µs (from 140 µs), presumably becausemoves
actually got longer (adding theD
before multiplying by 100 meant it went from 2900 elements to count to 3000).
Basically, you had some minor implementation weaknesses, but mostly, you happened to choose a use case that gave all the advantage to .count
, none to Counter
. If your inputs are always str
, and you're only count
ing them a small, fixed number of times, then sure, repeated calls to count
are generally going to win. But for arbitrary input types (especially iterators, where count
is impossible, both because it doesn't exist, and because you can only iterate it once), especially larger ones, with more unique things to count, where consistent performance counts (so relying on short-circuiting to reduce the number of count
calls isn't acceptable), Counter
will win.
Post a Comment for "List.count() Vs Counter() Performance"