dis — Desensamblador de bytecodes de Python¶
Propósito: | Convierte objetos de código en una representación legible por humanos de los bytecodes para su análisis. |
---|
El módulo dis
incluye funciones para trabajar con el bytecode de Python al
desensamblarlo en una forma más legible para los humanos. Revisar los
bytecodes que ejecuta el intérprete es una buena manera de ajustar a mano los
bucles ajustados y realizar otros tipos de optimizaciones. También es útil
para encontrar condiciones de carrera en aplicaciones de subprocesos múltiples,
ya que se puede usar para estimar el punto en el código donde puede cambiar el
control de subprocesos.
Advertencia
El uso de bytecodes es un detalle de implementación específico de la
versión del intérprete CPython. Consulte Include/opcode.h
en el código
fuente de la versión del intérprete que estás utilizando para encontrar la
lista canónica de bytecodes.
Desensamblado básico¶
La función dis()
imprime la representación desensamblada de una fuente de
código Python (módulo, clase, método, función u objeto de código). Un módulo
como dis_simple.py
se puede desensamblar ejecutando dis
desde la línea
de comandos.
1 2 3 4 | #!/usr/bin/env python3
# encoding: utf-8
my_dict = {'a': 1}
|
La salida se organiza en columnas con el número de línea de origen original, la dirección de instrucción dentro del objeto de código, el nombre del código de operación y cualquier argumento pasado al código de operación.
$ python3 -m dis dis_simple.py
4 0 LOAD_CONST 0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
En este caso, la fuente se traduce en cuatro operaciones diferentes para crear
y llenar el diccionario, luego guardar los resultados en una variable local.
Dado que el intérprete de Python está basado en la pila, los primeros pasos son
colocar las constantes en la pila en el orden correcto con LOAD_CONST
, y
luego usar BUILD_MAP
para extraer la nueva clave y el valor que se agregará
al diccionario. El objeto dict
resultante está vinculado al nombre
my_dict
con STORE_NAME
.
Desensamblado de funciones¶
Desafortunadamente, desensamblar un módulo completo no se repite en funciones automáticamente.
1 2 3 4 5 6 7 8 9 10 11 12 | #!/usr/bin/env python3
# encoding: utf-8
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.dis(f)
|
Los resultados de desensamblar dis_function.py
muestran las operaciones
para cargar el objeto de código de la función en la pila y luego convertirlo en
una función (LOAD_CONST
, MAKE_FUNCTION
), pero no el cuerpo de la
función.
$ python3 -m dis dis_function.py
5 0 LOAD_CONST 0 (<code object f at
0x1044fcf60, file "dis_function.py", line 5>)
2 LOAD_CONST 1 ('f')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
10 8 LOAD_NAME 1 (__name__)
10 LOAD_CONST 2 ('__main__')
12 COMPARE_OP 2 (==)
14 POP_JUMP_IF_FALSE 34
11 16 LOAD_CONST 3 (0)
18 LOAD_CONST 4 (None)
20 IMPORT_NAME 2 (dis)
22 STORE_NAME 2 (dis)
12 24 LOAD_NAME 2 (dis)
26 LOAD_ATTR 2 (dis)
28 LOAD_NAME 0 (f)
30 CALL_FUNCTION 1
32 POP_TOP
>> 34 LOAD_CONST 4 (None)
36 RETURN_VALUE
Para ver dentro de la función, la función en sí debe pasarse a dis()
.
$ python3 dis_function.py
6 0 LOAD_GLOBAL 0 (len)
2 LOAD_FAST 0 (args)
4 CALL_FUNCTION 1
6 STORE_FAST 1 (nargs)
7 8 LOAD_GLOBAL 1 (print)
10 LOAD_FAST 1 (nargs)
12 LOAD_FAST 0 (args)
14 CALL_FUNCTION 2
16 POP_TOP
18 LOAD_CONST 0 (None)
20 RETURN_VALUE
Para imprimir un resumen de la función, incluida la información sobre los
argumentos y los nombres que usa, llama a show_code()
, pasando la función
como primer argumento.
#!/usr/bin/env python3
# encoding: utf-8
def f(*args):
nargs = len(args)
print(nargs, args)
if __name__ == '__main__':
import dis
dis.show_code(f)
El argumento de show_code()
se pasa a code_info()
, que devuelve un
resumen bien formateado de la función, método, cadena de código u otro objeto
de código, listo para imprimir.
$ python3 dis_show_code.py
Name: f
Filename: dis_show_code.py
Argument count: 0
Kw-only arguments: 0
Number of locals: 2
Stack size: 3
Flags: OPTIMIZED, NEWLOCALS, VARARGS, NOFREE
Constants:
0: None
Names:
0: len
1: print
Variable names:
0: args
1: nargs
Clases¶
Las clases se pueden pasar a dis()
, en cuyo caso todos los métodos se
desensamblan a su vez.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #!/usr/bin/env python3
# encoding: utf-8
import dis
class MyObject:
"""Example for dis."""
CLASS_ATTRIBUTE = 'some value'
def __str__(self):
return 'MyObject({})'.format(self.name)
def __init__(self, name):
self.name = name
dis.dis(MyObject)
|
Los métodos se enumeran en orden alfabético, no en el orden en que aparecen en el archivo.
$ python3 dis_class.py
Disassembly of __init__:
16 0 LOAD_FAST 1 (name)
2 LOAD_FAST 0 (self)
4 STORE_ATTR 0 (name)
6 LOAD_CONST 0 (None)
8 RETURN_VALUE
Disassembly of __str__:
13 0 LOAD_CONST 1 ('MyObject({})')
2 LOAD_ATTR 0 (format)
4 LOAD_FAST 0 (self)
6 LOAD_ATTR 1 (name)
8 CALL_FUNCTION 1
10 RETURN_VALUE
Código fuente¶
A menudo es más conveniente trabajar con el código fuente de un programa que
con los propios objetos de código. Las funciones en dis
aceptan argumentos
de cadena que contienen código fuente y los convierten en objetos de código
antes de producir el desensamblado u otra salida.
import dis
code = """
my_dict = {'a': 1}
"""
print('Disassembly:\n')
dis.dis(code)
print('\nCode details:\n')
dis.show_code(code)
Pasar una cadena le permite guardar el paso de compilar el código y mantener una referencia a los resultados usted mismo, lo cual es más conveniente en los casos en que se examinan declaraciones fuera de una función.
$ python3 dis_string.py
Disassembly:
2 0 LOAD_CONST 0 ('a')
2 LOAD_CONST 1 (1)
4 BUILD_MAP 1
6 STORE_NAME 0 (my_dict)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Code details:
Name: <module>
Filename: <disassembly>
Argument count: 0
Kw-only arguments: 0
Number of locals: 0
Stack size: 2
Flags: NOFREE
Constants:
0: 'a'
1: 1
2: None
Names:
0: my_dict
Uso del desensamblado para depurar¶
A veces, al depurar una excepción, puede ser útil ver qué bytecode causó un
problema. Hay un par de formas de desensamblar el código en torno a un error.
La primera es mediante el uso de dis()
en el intérprete interactivo para
informar sobre la última excepción. Si no se pasa ningún argumento a
dis()
, entonces busca una excepción y muestra el desensamblado de la parte
superior de la pila que lo causó.
$ python3
Python 3.5.1 (v3.5.1:37a07cee5969, Dec 5 2015, 21:12:44)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> j = 4
>>> i = i + 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'i' is not defined
>>> dis.dis()
1 --> 0 LOAD_NAME 0 (i)
3 LOAD_CONST 0 (4)
6 BINARY_ADD
7 STORE_NAME 0 (i)
10 LOAD_CONST 1 (None)
13 RETURN_VALUE
>>>
El -->
después del número de línea indica el código de operación que causó
el error. No hay una variable i
definida, por lo que el valor asociado con
el nombre no se puede cargar en la pila.
Un programa también puede imprimir la información sobre un rastreo activo
pasándolo directamente a distb()
. En este ejemplo, hay una excepción
DivideByZero
, pero dado que la fórmula tiene dos divisiones, puede que no
esté claro qué parte es cero.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #!/usr/bin/env python3
# encoding: utf-8
i = 1
j = 0
k = 3
try:
result = k * (i / j) + (i / k)
except Exception:
import dis
import sys
exc_type, exc_value, exc_tb = sys.exc_info()
dis.distb(exc_tb)
|
El error es fácil de detectar cuando se carga en la pila en la versión
desensamblada. La operación incorrecta se resalta con -->
, y la línea
anterior empuja el valor de j
en la pila.
$ python3 dis_traceback.py
4 0 LOAD_CONST 0 (1)
2 STORE_NAME 0 (i)
5 4 LOAD_CONST 1 (0)
6 STORE_NAME 1 (j)
6 8 LOAD_CONST 2 (3)
10 STORE_NAME 2 (k)
8 12 SETUP_EXCEPT 24 (to 38)
9 14 LOAD_NAME 2 (k)
16 LOAD_NAME 0 (i)
18 LOAD_NAME 1 (j)
--> 20 BINARY_TRUE_DIVIDE
22 BINARY_MULTIPLY
24 LOAD_NAME 0 (i)
26 LOAD_NAME 2 (k)
28 BINARY_TRUE_DIVIDE
30 BINARY_ADD
32 STORE_NAME 3 (result)
...recortado...
Análisis de rendimiento de bucles¶
Además de los errores de depuración, dis
también puede ayudar a identificar
problemas de rendimiento. Examinar el código desensamblado es especialmente
útil con bucles estrechos donde el número de instrucciones de Python es bajo
pero se traducen en un conjunto ineficiente de bytecodes. La utilidad del
desensamblado se puede ver al examinar algunas implementaciones diferentes de
una clase, Diccionary
, que lee una lista de palabras y las agrupa por su
primera letra.
import dis
import sys
import textwrap
import timeit
module_name = sys.argv[1]
module = __import__(module_name)
Dictionary = module.Dictionary
dis.dis(Dictionary.load_data)
print()
t = timeit.Timer(
'd = Dictionary(words)',
textwrap.dedent("""
from {module_name} import Dictionary
words = [
l.strip()
for l in open('/usr/share/dict/words', 'rt')
]
""").format(module_name=module_name)
)
iterations = 10
print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))
La aplicación del controlador de prueba dis_test_loop.py
se puede usar para
ejecutar cada encarnación de la clase Diccionary
, comenzando con una
implementación sencilla pero lenta.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #!/usr/bin/env python3
# encoding: utf-8
class Dictionary:
def __init__(self, words):
self.by_letter = {}
self.load_data(words)
def load_data(self, words):
for word in words:
try:
self.by_letter[word[0]].append(word)
except KeyError:
self.by_letter[word[0]] = [word]
|
La ejecución del programa de prueba con esta versión muestra el programa desensamblado y la cantidad de tiempo que tarda en ejecutarse.
$ python3 dis_test_loop.py dis_slow_loop
12 0 SETUP_LOOP 83 (to 86)
3 LOAD_FAST 1 (words)
6 GET_ITER
>> 7 FOR_ITER 75 (to 85)
10 STORE_FAST 2 (word)
13 13 SETUP_EXCEPT 28 (to 44)
14 16 LOAD_FAST 0 (self)
19 LOAD_ATTR 0 (by_letter)
22 LOAD_FAST 2 (word)
25 LOAD_CONST 1 (0)
28 BINARY_SUBSCR
29 BINARY_SUBSCR
30 LOAD_ATTR 1 (append)
33 LOAD_FAST 2 (word)
36 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
39 POP_TOP
40 POP_BLOCK
41 JUMP_ABSOLUTE 7
15 >> 44 DUP_TOP
45 LOAD_GLOBAL 2 (KeyError)
48 COMPARE_OP 10 (exception match)
51 POP_JUMP_IF_FALSE 81
54 POP_TOP
55 POP_TOP
56 POP_TOP
16 57 LOAD_FAST 2 (word)
60 BUILD_LIST 1
63 LOAD_FAST 0 (self)
66 LOAD_ATTR 0 (by_letter)
69 LOAD_FAST 2 (word)
72 LOAD_CONST 1 (0)
75 BINARY_SUBSCR
76 STORE_SUBSCR
77 POP_EXCEPT
78 JUMP_ABSOLUTE 7
>> 81 END_FINALLY
82 JUMP_ABSOLUTE 7
>> 85 POP_BLOCK
>> 86 LOAD_CONST 0 (None)
89 RETURN_VALUE
TIME: 0.0568
La salida anterior muestra dis_slow_loop.py
que tarda 0.0568 segundos en
cargar las 235886 palabras en la copia de /usr/share/dict/words
en OS X.
Eso no es tan malo, pero el desensamblado adjunto muestra que el ciclo está
haciendo más trabajo del que necesita. Cuando ingresa al bucle en el código de
operación 13, configura un contexto de excepción (SETUP_EXCEPT
). Luego, se
necesitan seis códigos de operación para encontrar self.by_letter [word[0]]
antes de agregar word
a la lista. Si hay una excepción porque word[0]
aún no está en el diccionario, el controlador de excepciones hace todo el mismo
trabajo para determinar word[0]
(tres códigos de operación) y establece
self.by_letter[word[0]]
a una nueva lista que contiene la palabra.
Una técnica para eliminar la configuración de excepción es rellenar previamente
self.by_letter
con una lista para cada letra del alfabeto. Eso significa
que siempre se debe encontrar la lista para la nueva palabra, y el valor se
puede guardar después de la búsqueda.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #!/usr/bin/env python3
# encoding: utf-8
import string
class Dictionary:
def __init__(self, words):
self.by_letter = {
letter: []
for letter in string.ascii_letters
}
self.load_data(words)
def load_data(self, words):
for word in words:
self.by_letter[word[0]].append(word)
|
El cambio reduce el número de códigos de operación a la mitad, pero solo reduce el tiempo a 0.0567 segundos. Obviamente, el manejo de excepciones tuvo algunos gastos generales, pero no una cantidad significativa.
$ python3 dis_test_loop.py dis_faster_loop
17 0 SETUP_LOOP 38 (to 41)
3 LOAD_FAST 1 (words)
6 GET_ITER
>> 7 FOR_ITER 30 (to 40)
10 STORE_FAST 2 (word)
18 13 LOAD_FAST 0 (self)
16 LOAD_ATTR 0 (by_letter)
19 LOAD_FAST 2 (word)
22 LOAD_CONST 1 (0)
25 BINARY_SUBSCR
26 BINARY_SUBSCR
27 LOAD_ATTR 1 (append)
30 LOAD_FAST 2 (word)
33 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
36 POP_TOP
37 JUMP_ABSOLUTE 7
>> 40 POP_BLOCK
>> 41 LOAD_CONST 0 (None)
44 RETURN_VALUE
TIME: 0.0567
El rendimiento se puede mejorar aún más moviendo la búsqueda de
self.by_letter
fuera del bucle (el valor no cambia, después de todo).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #!/usr/bin/env python3
# encoding: utf-8
import collections
class Dictionary:
def __init__(self, words):
self.by_letter = collections.defaultdict(list)
self.load_data(words)
def load_data(self, words):
by_letter = self.by_letter
for word in words:
by_letter[word[0]].append(word)
|
Los códigos de operación 0-6 ahora encuentran el valor de self.by_letter
y
lo guardan como una variable local by_letter
. El uso de una variable local
solo toma un único código de operación, en lugar de dos (la declaración 22 usa
LOAD_FAST
para colocar el diccionario en la pila). Después de este cambio,
el tiempo de ejecución ha bajado a 0.0473 segundos.
$ python3 dis_test_loop.py dis_fastest_loop
14 0 LOAD_FAST 0 (self)
3 LOAD_ATTR 0 (by_letter)
6 STORE_FAST 2 (by_letter)
15 9 SETUP_LOOP 35 (to 47)
12 LOAD_FAST 1 (words)
15 GET_ITER
>> 16 FOR_ITER 27 (to 46)
19 STORE_FAST 3 (word)
16 22 LOAD_FAST 2 (by_letter)
25 LOAD_FAST 3 (word)
28 LOAD_CONST 1 (0)
31 BINARY_SUBSCR
32 BINARY_SUBSCR
33 LOAD_ATTR 1 (append)
36 LOAD_FAST 3 (word)
39 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
42 POP_TOP
43 JUMP_ABSOLUTE 16
>> 46 POP_BLOCK
>> 47 LOAD_CONST 0 (None)
50 RETURN_VALUE
TIME: 0.0473
Una optimización adicional, sugerida por Brandon Rhodes, es eliminar por
completo la versión de Python del ciclo for
. Si itertools.groupby()
se
usa para organizar la entrada, la iteración se mueve a C. Esto es seguro
porque se sabe que las entradas están ordenadas. Si ese no fuera el caso, el
programa necesitaría ordenarlos primero.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #!/usr/bin/env python3
# encoding: utf-8
import operator
import itertools
class Dictionary:
def __init__(self, words):
self.by_letter = {}
self.load_data(words)
def load_data(self, words):
# Arrange by letter
grouped = itertools.groupby(
words,
key=operator.itemgetter(0),
)
# Save arranged sets of words
self.by_letter = {
group[0][0]: group
for group in grouped
}
|
La versión de itertools
tarda solo 0.0332 segundos en ejecutarse,
aproximadamente el 60% del tiempo de ejecución del original.
$ python3 dis_test_loop.py dis_eliminate_loop
16 0 LOAD_GLOBAL 0 (itertools)
3 LOAD_ATTR 1 (groupby)
17 6 LOAD_FAST 1 (words)
9 LOAD_CONST 1 ('key')
18 12 LOAD_GLOBAL 2 (operator)
15 LOAD_ATTR 3 (itemgetter)
18 LOAD_CONST 2 (0)
21 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
24 CALL_FUNCTION 257 (1 positional, 1
keyword pair)
27 STORE_FAST 2 (grouped)
21 30 LOAD_CONST 3 (<code object
<dictcomp> at 0x101517930, file ".../dis_eliminate_loop.py",
line 21>)
33 LOAD_CONST 4
('Dictionary.load_data.<locals>.<dictcomp>')
36 MAKE_FUNCTION 0
23 39 LOAD_FAST 2 (grouped)
42 GET_ITER
43 CALL_FUNCTION 1 (1 positional, 0
keyword pair)
46 LOAD_FAST 0 (self)
49 STORE_ATTR 4 (by_letter)
52 LOAD_CONST 0 (None)
55 RETURN_VALUE
TIME: 0.0332
Optimizaciones del compilador¶
Desensamblar la fuente compilada también expone algunas de las optimizaciones realizadas por el compilador. Por ejemplo, las expresiones literales se pliegan durante la compilación, cuando es posible.
1 2 3 4 5 6 7 8 9 10 11 12 | #!/usr/bin/env python3
# encoding: utf-8
# Folded
i = 1 + 2
f = 3.4 * 5.6
s = 'Hello,' + ' World!'
# Not folded
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastic!'
|
Ninguno de los valores en las expresiones en las líneas 5-7 puede cambiar la
forma en que se realiza la operación, por lo que el resultado de las
expresiones se puede calcular en el momento de la compilación y colapsar en
instrucciones individuales LOAD_CONST
. Eso no es cierto sobre las líneas
10-12. Debido a que una variable está involucrada en esas expresiones, y la
variable puede referirse a un objeto que sobrecarga al operador involucrado, la
evaluación debe retrasarse al tiempo de ejecución.
$ python3 -m dis dis_constant_folding.py
5 0 LOAD_CONST 11 (3)
2 STORE_NAME 0 (i)
6 4 LOAD_CONST 12 (19.04)
6 STORE_NAME 1 (f)
7 8 LOAD_CONST 13 ('Hello, World!')
10 STORE_NAME 2 (s)
10 12 LOAD_NAME 0 (i)
14 LOAD_CONST 6 (3)
16 BINARY_MULTIPLY
18 LOAD_CONST 7 (4)
20 BINARY_MULTIPLY
22 STORE_NAME 3 (I)
11 24 LOAD_NAME 1 (f)
26 LOAD_CONST 1 (2)
28 BINARY_TRUE_DIVIDE
30 LOAD_CONST 6 (3)
32 BINARY_TRUE_DIVIDE
34 STORE_NAME 4 (F)
12 36 LOAD_NAME 2 (s)
38 LOAD_CONST 8 ('\n')
40 BINARY_ADD
42 LOAD_CONST 9 ('Fantastic!')
44 BINARY_ADD
46 STORE_NAME 5 (S)
48 LOAD_CONST 10 (None)
50 RETURN_VALUE
Ver también
- Documentación de la biblioteca estándar para dis – Incluye la lista de instrucciones de bytecode.
Include/opcode.h
– El código fuente para el intérprete CPython define los bytecodes enopcode.h
.- Python Essential Reference, 4th Edition, David M. Beazley – http://www.informit.com/store/product.aspx?isbn=0672329786
- thomas.apestaart.org «Python Disassembly» – Una breve discusión sobre la diferencia entre almacenar valores en un diccionario entre Python 2.5 y 2.6.
- Why is looping over range() in Python faster than using a while loop? – Una discusión en StackOverflow.com comparando 2 ejemplos de bucle a través de sus códigos de byte desensamblados.
- Decorator for binding constants at compile time – Receta de Python Cookbook por Raymond Hettinger y Skip Montanaro con un decorador de funciones que reescribe los bytecodes para una función que inserte constantes globales para evitar búsquedas de nombres en tiempo de ejecución.