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_chkfunction requires not operation carried out twice in worst case, in addition interpreter deciding whether or not take jump.


Popular posts from this blog