Surely a loop over a generator expression is slower compared to a list. But in this case the iteration within the generator is basically a loop over the list itself, hence the next()
calls on generator basically delegate to list's next()
method.
For example in this case there is no 2x performance difference.
>>> lst = list(range(10**5))
>>> %%timeit
... sum(x for x in lst)
...
100 loops, best of 3: 6.39 ms per loop
>>> %%timeit
... c = 0
... for x in lst: c += x
...
100 loops, best of 3: 6.69 ms per loop
First let's check the byte codes of both the approaches:
def gt_0(lst):
for elm in lst:
if elm > 0:
return True
return False
def any_with_ge(lst):
return any(elm > 0 for elm in lst)
Bytecodes:
>>> dis.dis(gt_0)
10 0 SETUP_LOOP 30 (to 33)
3 LOAD_FAST 0 (lst)
6 GET_ITER
>> 7 FOR_ITER 22 (to 32)
10 STORE_FAST 1 (elm)
11 13 LOAD_FAST 1 (elm)
16 LOAD_CONST 1 (0)
19 COMPARE_OP 4 (>)
22 POP_JUMP_IF_FALSE 7
12 25 LOAD_GLOBAL 0 (True)
28 RETURN_VALUE
29 JUMP_ABSOLUTE 7
>> 32 POP_BLOCK
13 >> 33 LOAD_GLOBAL 1 (False)
36 RETURN_VALUE
>>> dis.dis(any_with_ge.func_code.co_consts[1])
17 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 17 (to 23)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 YIELD_VALUE
19 POP_TOP
20 JUMP_ABSOLUTE 3
>> 23 LOAD_CONST 1 (None)
26 RETURN_VALUE
As you can see there's no jump condition in the any()
version, it basically gets the value of the >
comparison and then again checks its truthy value using PyObject_IsTrue
again. On the other hand the gt_0
checks the truthy value of the condition once and returns True
or False
based on that.
Now let's add another any()
based version that has an if-condition like in the for-loop.
def any_with_ge_and_condition(lst):
return any(True for elm in lst if elm > 0)
Bytecode:
>>> dis.dis(any_with_ge_and_condition.func_code.co_consts[1])
21 0 LOAD_FAST 0 (.0)
>> 3 FOR_ITER 23 (to 29)
6 STORE_FAST 1 (elm)
9 LOAD_FAST 1 (elm)
12 LOAD_CONST 0 (0)
15 COMPARE_OP 4 (>)
18 POP_JUMP_IF_FALSE 3
21 LOAD_GLOBAL 0 (True)
24 YIELD_VALUE
25 POP_TOP
26 JUMP_ABSOLUTE 3
>> 29 LOAD_CONST 1 (None)
32 RETURN_VALUE
Now we have reduced the work done by any()
by adding the condition(check last section for more details) and it will have to check truthy twice only once when the condition is going to be True
, else it will basically skip to next item.
Now let's compare the timings of these 3:
>>> %timeit gt_0(lst)
10 loops, best of 3: 26.1 ms per loop
>>> %timeit any_with_ge(lst)
10 loops, best of 3: 57.7 ms per loop
>>> %timeit any_with_ge_and_condition(lst)
10 loops, best of 3: 26.8 ms per loop
Let's modify gt_0
to include two checks as in the simple any()
version and check its timing.
from operator import truth
# This calls `PyObject_IsTrue` internally
# https://github.com/python/cpython/blob/master/Modules/_operator.c#L30
def gt_0_truth(lst, truth=truth): # truth=truth to prevent global lookups
for elm in lst:
condition = elm > 0
if truth(condition):
return True
return False
Timing:
>>> %timeit gt_0_truth(lst)
10 loops, best of 3: 56.6 ms per loop
Now, let's see what happens when we try to check truthy value of an item twice using operator.truth
.
>> %%timeit t=truth
... [t(i) for i in xrange(10**5)]
...
100 loops, best of 3: 5.45 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**5)]
...
100 loops, best of 3: 9.06 ms per loop
>>> %%timeit t=truth
[t(i) for i in xrange(10**6)]
...
10 loops, best of 3: 58.8 ms per loop
>>> %%timeit t=truth
[t(t(i)) for i in xrange(10**6)]
...
10 loops, best of 3: 87.8 ms per loop
That's quite a difference even though we are simply calling truth()
(i.e PyObject_IsTrue
) on an already boolean object, I guess that sort of explains the slowness of basic any()
version.
You may argue that if
condition in any()
will also result in two truthiness check, but that's not the case when the comparison operation returns Py_True
or Py_False
. POP_JUMP_IF_FALSE
simply jumps to the next OP code and no call to PyObject_IsTrue
is made.
lst
that doesn't start with0
? – Underhill%timeit any(True for elm in lst if elm > 0)
. – Piculany()
is in Python or is that just the equivalent Python syntax? – Underhillany(elm > 0 for elm in lst)
? – Anallise