operator not logical false example assignment and python python-2.7 if-statement logical-operators micro-optimization

not - python if is false



¿Por qué es "if not(a and b)" más rápido que "if not a or not b"? (2)

Por un capricho, recientemente probé estos dos métodos con timeit , para ver qué método de evaluación era más rápido:

import timeit """Test method returns True if either argument is falsey, else False.""" def and_chk((a, b)): if not (a and b): return True return False def not_or_chk((a, b)): if not a or not b: return True return False

... y obtuve estos resultados:

VALUES FOR 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.

La diferencia en la eficiencia está entre uno y nueve por ciento, siempre a favor de if not (a and b) , que es lo contrario de lo que podría esperar, ya que entiendo que if not a or not b evaluará sus términos ( if not a y luego if not b ) en orden, ejecutando el bloque if una vez que encuentra una expresión verdadera (y hay no and cláusulas). En contraste, el método and_chk necesita evaluar ambas cláusulas antes de que pueda devolver cualquier resultado al if not.. que lo envuelve.

Los resultados de tiempo, sin embargo, refutan esta comprensión. ¿Cómo, entonces, se evalúa la condición if ? Soy perfectamente consciente del hecho de que este grado de microoptimización es prácticamente, si no del todo, inútil. Solo quiero entender cómo lo está haciendo Python.

Para completar, así es como configuré el 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 as tups, and_chk'') time_zeroTrue_and = timeit.Timer(''and_chk(next(tups))'', ''from __main__ import zeroTrue_and as tups, and_chk'') time_oneTrue_and = timeit.Timer(''and_chk(next(tups))'', ''from __main__ import oneTrue_and as tups, and_chk'') time_bothTrue_and = timeit.Timer(''and_chk(next(tups))'', ''from __main__ import bothTrue_and as tups, and_chk'') time_bothFalse_notor = timeit.Timer(''not_or_chk(next(tups))'', ''from __main__ import bothFalse_notor as tups, not_or_chk'') time_zeroTrue_notor = timeit.Timer(''not_or_chk(next(tups))'', ''from __main__ import zeroTrue_notor as tups, not_or_chk'') time_oneTrue_notor = timeit.Timer(''not_or_chk(next(tups))'', ''from __main__ import oneTrue_notor as tups, not_or_chk'') time_bothTrue_notor = timeit.Timer(''not_or_chk(next(tups))'', ''from __main__ import bothTrue_notor as tups, not_or_chk'')

... luego ejecuta cada timeit.Timer(..) función con .timeit(cyc) para obtener los resultados publicados.


TL; DR

La función not_or_chk requiere dos operaciones unarias además de dos saltos (en el peor de los casos), mientras que la función and_chk solo tiene los dos saltos (nuevamente, en el peor de los casos).

Detalles

El módulo de des al rescate! El módulo dis permite ver el desmontaje del código de Python de su código. Por ejemplo:

import dis """Test method returns True if either argument is falsey, else False.""" def and_chk((a, b)): if not (a and b): return True return False def not_or_chk((a, b)): if not a 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))

Produce este resultado:

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) * This block is the * 15 JUMP_IF_FALSE_OR_POP 21 * disassembly of * 18 LOAD_FAST 2 (b) * the "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) * This block is the * 15 UNARY_NOT * disassembly of * 16 POP_JUMP_IF_TRUE 26 * the "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

Eche un vistazo a los dos bloques de código de bytes Python que he marcado con los asteriscos. Esos bloques son sus dos funciones desmontadas. Tenga en cuenta que and_chk solo tiene dos saltos, y los cálculos en la función se realizan al decidir si se toma el salto o no .

Por otro lado, la función not_or_chk requiere que la operación not se lleve a cabo dos veces en el peor de los casos, además del intérprete que decide si tomar el salto o no.


Acabo de notar esta pregunta a través de su pregunta de Meta SO: ¿es apropiado compartir los resultados de mi investigación para resolver mis propias preguntas menores? . Como mencionas en esa pregunta (y una de las etiquetas en esta pregunta lo indica), este tipo de cosas cae en la categoría de micro-optimización. Idealmente, uno no debería tener que preocuparse por diferencias de rendimiento menores, y como dice Knuth, la optimización prematura es la raíz de todo mal . Pero creo que es divertido e instructivo investigar estos asuntos, ya que puede darte una mejor idea de cómo funciona Python "bajo el capó".

El comentario de Mephy me impulsó a ver cuáles eran las diferencias de tiempo para las versiones sin sentido de tus funciones. Los resultados son interesantes, en mi humilde opinión. También aproveché la oportunidad para simplificar su procedimiento de prueba.

#!/usr/bin/env python '''''' Do timeit tests on various implementations of NAND NAND returns True if either argument is falsey, else False. From https://.com/q/29551438/4014959 Written by PM 2Ring 2015.04.09 '''''' from timeit import Timer import dis def and_chk(a, b): return not (a and b) def and_chk_if(a, b): if not (a and b): return True else: return False def not_or_chk(a, b): return not a or not b def not_or_chk_if(a, b): if not a or not b: return True else: return False #All the functions funcs = ( and_chk, and_chk_if, not_or_chk, not_or_chk_if, ) #Argument tuples to test the functions with bools = (0, 1) bool_tups = [(u, v) for u in bools for v in bools] def show_dis(): '''''' Show the disassembly for each function '''''' print ''Disassembly'' for func in funcs: fname = func.func_name print ''/n%s'' % fname dis.dis(func) print def verify(): '''''' Verify that the functions actually perform as intended '''''' print ''Verifying...'' for func in funcs: fname = func.func_name print ''/n%s'' % fname for args in bool_tups: print args, func(*args) print def time_test(loops, reps): '''''' Print timing stats for all the functions '''''' print ''Timing tests/nLoops = %d, Repetitions = %d'' % (loops, reps) for func in funcs: fname = func.func_name print ''/n%s'' % fname setup = ''from __main__ import %s'' % fname for args in bool_tups: t = Timer(''%s%s'' % (fname, args), setup) r = t.repeat(reps, loops) r.sort() print args, r show_dis() verify() time_test(loops=520000, reps=3)

salida

Disassembly and_chk 13 0 LOAD_FAST 0 (a) 3 JUMP_IF_FALSE 4 (to 10) 6 POP_TOP 7 LOAD_FAST 1 (b) >> 10 UNARY_NOT 11 RETURN_VALUE and_chk_if 16 0 LOAD_FAST 0 (a) 3 JUMP_IF_FALSE 4 (to 10) 6 POP_TOP 7 LOAD_FAST 1 (b) >> 10 JUMP_IF_TRUE 5 (to 18) 13 POP_TOP 17 14 LOAD_GLOBAL 0 (True) 17 RETURN_VALUE >> 18 POP_TOP 19 19 LOAD_GLOBAL 1 (False) 22 RETURN_VALUE 23 LOAD_CONST 0 (None) 26 RETURN_VALUE not_or_chk 22 0 LOAD_FAST 0 (a) 3 UNARY_NOT 4 JUMP_IF_TRUE 5 (to 12) 7 POP_TOP 8 LOAD_FAST 1 (b) 11 UNARY_NOT >> 12 RETURN_VALUE not_or_chk_if 25 0 LOAD_FAST 0 (a) 3 UNARY_NOT 4 JUMP_IF_TRUE 8 (to 15) 7 POP_TOP 8 LOAD_FAST 1 (b) 11 UNARY_NOT 12 JUMP_IF_FALSE 5 (to 20) >> 15 POP_TOP 26 16 LOAD_GLOBAL 0 (True) 19 RETURN_VALUE >> 20 POP_TOP 28 21 LOAD_GLOBAL 1 (False) 24 RETURN_VALUE 25 LOAD_CONST 0 (None) 28 RETURN_VALUE Verifying... and_chk (0, 0) True (0, 1) True (1, 0) True (1, 1) False and_chk_if (0, 0) True (0, 1) True (1, 0) True (1, 1) False not_or_chk (0, 0) True (0, 1) True (1, 0) True (1, 1) False not_or_chk_if (0, 0) True (0, 1) True (1, 0) True (1, 1) False Timing tests Loops = 520000, Repetitions = 3 and_chk (0, 0) [0.36773014068603516, 0.37793493270874023, 0.38387489318847656] (0, 1) [0.36292791366577148, 0.37119913101196289, 0.37146902084350586] (1, 0) [0.38673520088195801, 0.39340090751647949, 0.39670205116271973] (1, 1) [0.38938498497009277, 0.39505791664123535, 0.40034103393554688] and_chk_if (0, 0) [0.4021449089050293, 0.40345501899719238, 0.41558098793029785] (0, 1) [0.40686416625976562, 0.41213202476501465, 0.44800615310668945] (1, 0) [0.4281308650970459, 0.42916202545166016, 0.43681907653808594] (1, 1) [0.46246123313903809, 0.46759700775146484, 0.47544980049133301] not_or_chk (0, 0) [0.35435700416564941, 0.36368083953857422, 0.36867713928222656] (0, 1) [0.35602092742919922, 0.35732793807983398, 0.36237406730651855] (1, 0) [0.39550518989562988, 0.40660715103149414, 0.40977287292480469] (1, 1) [0.4060060977935791, 0.4112389087677002, 0.41296815872192383] not_or_chk_if (0, 0) [0.4308779239654541, 0.44109201431274414, 0.45827698707580566] (0, 1) [0.43600606918334961, 0.4370419979095459, 0.47623395919799805] (1, 0) [0.48452401161193848, 0.48769593238830566, 0.49147915840148926] (1, 1) [0.53107500076293945, 0.54048299789428711, 0.55434417724609375]

Estos tiempos se realizaron usando Python 2.6.6 en un Pentium 4 de 2 GHz (un solo núcleo de 32 bits) ejecutando Mepis 11 (una distribución de Linux de la familia Debian).

next(tups) que he evitado usar tu next(tups) estrategia next(tups) para obtener los argumentos para cada llamada a función y, en cambio, estoy pasando los argumentos directamente, como constantes. El tiempo necesario para realizar el next(tups) debe ser bastante constante, pero lo mejor es eliminar esos gastos generales, cuando sea práctico, de modo que las mediciones de tiempo informadas reflejen con mayor precisión el rendimiento del código que realmente nos interesa.

Además, es habitual realizar varias repeticiones del ciclo de temporización y tomar el valor mínimo; FWIW, generalmente hago de 3 a 5 repeticiones. De los documentos timeit :

Nota

Es tentador calcular la media y la desviación estándar del vector de resultados e informarlos. Sin embargo, esto no es muy útil. En un caso típico, el valor más bajo proporciona un límite inferior de la velocidad con la que su máquina puede ejecutar el fragmento de código dado; los valores más altos en el vector de resultados generalmente no son causados ​​por la variabilidad en la velocidad de Python, sino por otros procesos que interfieren con su precisión de tiempo. Entonces, el mínimo () del resultado es probablemente el único número que le interese. Después de eso, debe observar el vector completo y aplicar el sentido común en lugar de las estadísticas.

Tu meta post dice que quieres realizar e informar sobre otros experimentos de micro-optimización, por lo que podrías estar interesado en echar un vistazo a algún código que publiqué hace unos meses que hace pruebas de tiempo en varias implementaciones de la función factorial .