python - Why is "if not (a and b)" faster than "if not a or not b"? -
on whim, tested these 2 methods timeit
, see evaluation method faster:
import timeit """test method returns true if either argument falsey, else false.""" def and_chk((a, b)): if not (a , b): return true return false def not_or_chk((a, b)): if not or not b: return true return false
...and got these results:
values a,b -> 0,0 0,1 1,0 1,1 method and_chk(a,b) 0.95559 0.98646 0.95138 0.98788 not_or_chk(a,b) 0.96804 1.07323 0.96015 1.05874 ...seconds per 1,111,111 cycles.
the difference in efficiency between 1 , 9 percent, in favour of if not (a , b)
, opposite of might expect since understand if not or not b
evaluate terms (if not a
, if not b
) in order, running if
block once encounters true expression (and there no and
clauses). in contrast, and_chk
method needs evaluate both clauses before can return result if not..
wraps it.
the timing results, however, disprove understanding. how, then, if
condition being evaluated? aware of fact degree of microoptimization practically, if not completely, pointless. want understand how python going it.
for completion's sake, how set timeit
...
cyc = 1111111 bothfalse_and = iter([(0,0)] * cyc) zerotrue_and = iter([(1,0)] * cyc) onetrue_and = iter([(0,1)] * cyc) bothtrue_and = iter([(1,1)] * cyc) bothfalse_notor = iter([(0,0)] * cyc) zerotrue_notor = iter([(1,0)] * cyc) onetrue_notor = iter([(0,1)] * cyc) bothtrue_notor = iter([(1,1)] * cyc) time_bothfalse_and = timeit.timer('and_chk(next(tups))', 'from __main__ import bothfalse_and tups, and_chk') time_zerotrue_and = timeit.timer('and_chk(next(tups))', 'from __main__ import zerotrue_and tups, and_chk') time_onetrue_and = timeit.timer('and_chk(next(tups))', 'from __main__ import onetrue_and tups, and_chk') time_bothtrue_and = timeit.timer('and_chk(next(tups))', 'from __main__ import bothtrue_and tups, and_chk') time_bothfalse_notor = timeit.timer('not_or_chk(next(tups))', 'from __main__ import bothfalse_notor tups, not_or_chk') time_zerotrue_notor = timeit.timer('not_or_chk(next(tups))', 'from __main__ import zerotrue_notor tups, not_or_chk') time_onetrue_notor = timeit.timer('not_or_chk(next(tups))', 'from __main__ import onetrue_notor tups, not_or_chk') time_bothtrue_notor = timeit.timer('not_or_chk(next(tups))', 'from __main__ import bothtrue_notor tups, not_or_chk')
...then ran each timeit.timer(..)
function .timeit(cyc)
results posted.
tl;dr
the not_or_chk
function requires 2 unary operations in addition 2 jumps (in worst case), while and_chk
function has 2 jumps (again, in worst case).
details
the dis module rescue! dis
module lets take @ python bytecode disassembly of code. example:
import dis """test method returns true if either argument falsey, else false.""" def and_chk((a, b)): if not (a , b): return true return false def not_or_chk((a, b)): if not or not b: return true return false print("and check:\n") print(dis.dis(and_chk)) print("or check:\n") print(dis.dis(not_or_chk))
produces output:
and check: 5 0 load_fast 0 (.0) 3 unpack_sequence 2 6 store_fast 1 (a) 9 store_fast 2 (b) 6 12 load_fast 1 (a) * block * 15 jump_if_false_or_pop 21 * disassembly of * 18 load_fast 2 (b) * "and_chk" * >> 21 pop_jump_if_true 28 * function * 7 24 load_global 0 (true) 27 return_value 8 >> 28 load_global 1 (false) 31 return_value none or check: 10 0 load_fast 0 (.0) 3 unpack_sequence 2 6 store_fast 1 (a) 9 store_fast 2 (b) 11 12 load_fast 1 (a) * block * 15 unary_not * disassembly of * 16 pop_jump_if_true 26 * "not_or_chk" * 19 load_fast 2 (b) * function * 22 unary_not 23 pop_jump_if_false 30 12 >> 26 load_global 0 (true) 29 return_value 13 >> 30 load_global 1 (false) 33 return_value none
take @ 2 blocks of python bytecode i've marked asterisks. blocks 2 disassembled functions. note and_chk
has 2 jumps, , calculations in function made while deciding whether or not take jump.
on other hand, not_or_chk
function requires not
operation carried out twice in worst case, in addition interpreter deciding whether or not take jump.