dis - Disassemblatore di Bytecode Python

Scopo: Converte oggetti codice in una rappresentazione leggibile dall'umano dei bytecode a scopo di analisi.

Il modulo dis contiene funzioni per lavorare con bytecode Python per disassemblarlo in una forma più leggibile dall'umano. Riesaminare i bytecode che l'interprete sta eseguendo è un buon modo per realizzare una sintonizzazione manuale di tight loop (per tight loop si intende un ciclo che contiene poche istruzioni e che esegue iterazioni per molte volte, oppure un ciclo che usa pesantemente risorse in I/O oppure del processore, senza dividerle adeguatamente con altri programmi in esecuzione nel sistema operativo - n.d.t.) ed eseguire altri tipi di ottimizzazioni. E' anche utile per trovare race conditions in applicazioni multi thread, visto che è possibile stabilire il punto nel proprio codice nel quale il controllo del thread potrebbe trasferirsi.

L'uso di bytecode è un dettaglio di implementazione specifica alla versione dell'interprete CPython. Si faccia riferimento a Include/opcode.h nel codice sorgente per la versione dell'interprete che si sta usando per trovare la lista prefissata dei bytecode.

Disassemblaggio Basico

La funzione dis() stampa la rappresentazione disassemblata di un sorgente di codice Python (modulo, classe, metodo, funzione od oggetto codice). Un modulo come dis_simple.py può essere disassemblato eseguendo dis dalla riga di comando.

1
2
3
4
5
6
# dis_simple.py

#!/usr/binf/env python3
# encoding: utf-8

my_dict = { 'a':1 }

Il risultato è organizzata in colonne con il numero di riga originale del sorgente, l'indirizzo dell'istruzione all'interno dell'oggetto codice, il nome opcode e qualsivoglia argomento passato a opcode.

$ python3 -m dis dis_simple.py

  6           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

In questo caso, il sorgente si traduce in 4 diverse operazioni per creare e popolare il dizionario, quindi salvare i risultati in una variabile locale. Visto che l'interprete Python è basato sullo stack, i primi passi sono il porre le costanti nello stack nel corretto ordine con LOAD_CONST, quindi usare BUILD_MAP per estrarre la nuova chiave ed il valore da aggiungere al dizionario. L'oggetto dict risultante viene legato al nome my_dict con STORE_NAME.

Disassemblare Funzioni

Sfortunatamente, il disassemblaggio di un intero modulo non esegue una ricorsione all'interno delle funzioni automaticamente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# dis_function.py

#!/usr/bin/env python3
# encoding: utf-8


def f(*args):
    nargs = len(args)
    print(nargs, args)


if __name__ == '__main__':
    import dis
    dis.dis(f)

Il risultato del disassemblaggio di dis_function.py mostra le operazioni per il caricamento dell'oggetto codice della funzione dentro lo stack, quindi la trasformazione in una funzione (LOAD_CONST, MAKE_FUNCTION), seguito del corpo della funzione.

$ python3 -m dis dis_function.py

  7           0 LOAD_CONST               0 (<code object f at 0x7feb868383a0, file "dis_function.py", line 7>)
              2 LOAD_CONST               1 ('f')
              4 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

 12           8 LOAD_NAME                1 (__name__)
             10 LOAD_CONST               2 ('__main__')
             12 COMPARE_OP               2 (==)
             14 POP_JUMP_IF_FALSE       34

 13          16 LOAD_CONST               3 (0)
             18 LOAD_CONST               4 (None)
             20 IMPORT_NAME              2 (dis)
             22 STORE_NAME               2 (dis)

 14          24 LOAD_NAME                2 (dis)
             26 LOAD_METHOD              2 (dis)
             28 LOAD_NAME                0 (f)
             30 CALL_METHOD              1
             32 POP_TOP
        >>   34 LOAD_CONST               4 (None)
             36 RETURN_VALUE

Disassembly of <code object f at 0x7feb868383a0, file "dis_function.py", line 7>:
  8           0 LOAD_GLOBAL              0 (len)
              2 LOAD_FAST                0 (args)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (nargs)

  9           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

Versioni precedenti di Python non includono il corpo delle funzioni nel modulo disassemblato automaticamente. Per vedere la versione disassemblata di una funzione, si passi la funzione direttamente a dis().

$ python3  dis_function.py

  8           0 LOAD_GLOBAL              0 (len)
              2 LOAD_FAST                0 (args)
              4 CALL_FUNCTION            1
              6 STORE_FAST               1 (nargs)

  9           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

Per stampare un sommario della funzione, incluse informazioni circa gli argomenti ed i nomi che usa, si chiami show_code(), passando la funzione come primo argomento.

# dis_show_code.py

#!/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)

L'argomento per show_code() viene passato a code_info(), che ritorna un sommario ben formattato della funzione, metodo, stringa di codice od altro oggetto codice, pronto per la stampa.

$ python3 dis_show_code.py

Name:              f
Filename:          dis_show_code.py
Argument count:    0
Positional-only arguments: 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

Classi

Anche le classi possono essere passate a dis, nel qual caso tutti i metodi sono di volta in volta disassemblati.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# dis_class.py

#!/usr/bin/env python
# encoding: utf-8

import dis


class MyObject:
    """Esempio per dis."""

    CLASS_ATTRIBUTE = 'un qualche valore'

    def __str__(self):
        return 'MyObject({})'.format(self.name)

    def __init__(self, name):
        self.name = name


dis.dis(MyObject)

I metodi sono elencati in ordine alfabetico, non nell'ordine nel quale appaiono nel file.

$ python3  dis_class.py

Disassembly of __init__:
 18           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__:
 15           0 LOAD_CONST               1 ('MyObject({})')
              2 LOAD_METHOD              0 (format)
              4 LOAD_FAST                0 (self)
              6 LOAD_ATTR                1 (name)
              8 CALL_METHOD              1
             10 RETURN_VALUE

Codice Sorgente

E' spesso più conveniente lavorare con il codice sorgente di un programma, piuttosto che con i corrispondenti oggetti codice. Le funzioni in dis accettano argomenti stringa che contengono codice sorgente, quindi li convertono in oggetti codice prima di produrre il disassemblaggio od altro risultato.

# dis_string.py

import dis

code = """
my_dict = {'a': 1}
"""

print('Disassemblato:\n')
dis.dis(code)

print('\nDettagli codice:\n')
dis.show_code(code)

Il passare una stringa consente di evitare di occuparsi del passaggio della compilazione del codice e del mantenere un riferimento ai risultati, che è molto conveniente nei casi nei quali si stanno esaminando istruzioni al di fuori di una funzione.

$ python3 dis_string.py

Disassemblato:

  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

Dettagli codice:

Name:              <module>
Filename:          <disassembly>
Argument count:    0
Positional-only arguments: 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

Usare il Disassemblaggio per Debug

Talvolta quando si sta eseguendo il debug di una eccezione, può essere utile vedere quale bytecode ha causato il problema. Ci sono un paio di metodi per disassemblare il codice intorno ad un errore. Il primo è usare dis() nell'interprete interattivo per ottenere informazioni circa l'ultima eccezione. Se non viene passato a dis() alcun argomento, allora viene cercata una eccezione e viene mostrato il disassemblaggio dell'inizio dello stack che l'ha causata.

$ python3

Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
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)
              2 LOAD_CONST               0 (4)
              4 BINARY_ADD
              6 STORE_NAME               0 (i)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
>>>

Il simbolo --> dopo il numero di riga indica l'opcode che ha causato l'errore. Non esiste nessuna variabile i definita, quindi il valore associato a quel nome non può essere caricato nello stack.

Un programma può anche stampare le informazioni sul traceback attivo passandolo direttamente a distb(). In questo esempio, c'è una eccezione DivideByZero, tuttavia, visto che la formula contiene due divisioni, potrebbe non essere chiaro quale parte è zero.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# dis_traceback.py

#!/usr/bin/env python
# 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)

E' facile identificare il valore errato quando esso viene caricato nello stack nella versione disassemblata. L'operazione errata viene evidenziata con -->, e la riga precedente spinge il valore di i nello stack.

$ python3  dis_traceback.py

  6           0 LOAD_CONST               0 (1)
              2 STORE_NAME               0 (i)

  7           4 LOAD_CONST               1 (0)
              6 STORE_NAME               1 (j)

  8           8 LOAD_CONST               2 (3)
             10 STORE_NAME               2 (k)

 11          12 SETUP_FINALLY           24 (to 38)

 12          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)
             34 POP_BLOCK
             36 JUMP_FORWARD            60 (to 98)

 13     >>   38 DUP_TOP
             40 LOAD_NAME                4 (Exception)
             42 COMPARE_OP              10 (exception match)
             44 POP_JUMP_IF_FALSE       96
             46 POP_TOP
             48 POP_TOP
             50 POP_TOP

 14          52 LOAD_CONST               1 (0)
             54 LOAD_CONST               3 (None)
             56 IMPORT_NAME              5 (dis)
             58 STORE_NAME               5 (dis)

 15          60 LOAD_CONST               1 (0)
             62 LOAD_CONST               3 (None)
             64 IMPORT_NAME              6 (sys)
             66 STORE_NAME               6 (sys)

 16          68 LOAD_NAME                6 (sys)
             70 LOAD_METHOD              7 (exc_info)
             72 CALL_METHOD              0
             74 UNPACK_SEQUENCE          3
             76 STORE_NAME               8 (exc_type)
             78 STORE_NAME               9 (exc_value)
             80 STORE_NAME              10 (exc_tb)

 17          82 LOAD_NAME                5 (dis)
             84 LOAD_METHOD             11 (distb)
             86 LOAD_NAME               10 (exc_tb)
             88 CALL_METHOD              1
             90 POP_TOP
             92 POP_EXCEPT
             94 JUMP_FORWARD             2 (to 98)
        >>   96 END_FINALLY
        >>   98 LOAD_CONST               3 (None)
            100 RETURN_VALUE

Analisi delle Prestazioni dei Cicli

Oltre ad eseguire il debug degli errori, dis può anche aiutare ad identificare problemi di prestazioni. Esaminare il codice disassemblato è particolarmente utile con i tight loop dove il numero di istruzioni Python è basso ma esse si traducono in un insieme di bytecode inefficiente. Si può vedere come il disassemblaggio viene in aiuto esaminando qualche diversa implementazione di una classe, Dictionary, che legge una lista di parole e le raggruppa in base alla loro prima lettera.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# dis_test_loop.py

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/mywords', 'rt')
    ]
    """).format(module_name=module_name)
    )
iterations = 10
print('TEMPO: {:0.4f}'.format(t.timeit(iterations) / iterations))

Si può usare dis_test_loop.py per eseguire ogni versione della classe Dictionary, a partire con una implementazione lineare, ma lenta.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# dis_slow_loop.py

#!/usr/bin/env python
# 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]

Eseguendo il programma di test con questa versione viene mostrato il programma disassemblato ed il tempo impiegato per l'esecuzione.

$ python3   dis_test_loop.py dis_slow_loop

 14           0 LOAD_FAST                1 (words)
              2 GET_ITER
        >>    4 FOR_ITER                66 (to 72)
              6 STORE_FAST               2 (word)

 15           8 SETUP_FINALLY           24 (to 34)

 16          10 LOAD_FAST                0 (self)
             12 LOAD_ATTR                0 (by_letter)
             14 LOAD_FAST                2 (word)
             16 LOAD_CONST               1 (0)
             18 BINARY_SUBSCR
             20 BINARY_SUBSCR
             22 LOAD_METHOD              1 (append)
             24 LOAD_FAST                2 (word)
             26 CALL_METHOD              1
             28 POP_TOP
             30 POP_BLOCK
             32 JUMP_ABSOLUTE            4

 17     >>   34 DUP_TOP
             36 LOAD_GLOBAL              2 (KeyError)
             38 COMPARE_OP              10 (exception match)
             40 POP_JUMP_IF_FALSE       68
             42 POP_TOP
             44 POP_TOP
             46 POP_TOP

 18          48 LOAD_FAST                2 (word)
             50 BUILD_LIST               1
             52 LOAD_FAST                0 (self)
             54 LOAD_ATTR                0 (by_letter)
             56 LOAD_FAST                2 (word)
             58 LOAD_CONST               1 (0)
             60 BINARY_SUBSCR
             62 STORE_SUBSCR
             64 POP_EXCEPT
             66 JUMP_ABSOLUTE            4
        >>   68 END_FINALLY
             70 JUMP_ABSOLUTE            4
        >>   72 LOAD_CONST               0 (None)
             74 RETURN_VALUE

Traceback (most recent call last):
  File "dis_test_loop.py", line 26, in <module>
    print('TEMPO: {:0.4f}'.format(t.timeit(iterations) / iterations))
  File "/usr/lib/python3.8/timeit.py", line 177, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 7, in inner
FileNotFoundError: [Errno 2] No such file or directory: '/usr/share/dict/mywords'

Il risultato mostra che dis_slow_loop.py ha impiegato 0.0130 secondi circa per caricare 102305 parole nella copia di /usr/share/dict/words (questi dati sono rilevati dall'esecuzione sul mio computer su di un s.o. Linux a 64 bit - n.d.t.). Non è male, tuttavia come si può rilevare dal codice disassemblato il ciclo sta eseguendo più lavoro di quello che serve. Quando entra nel ciclo, nell'opcode 15, imposta un contesto di eccezione (SETUP_EXCEPT). Poi gli occorrono 6 opcode per trovare self.by_letter[word[0]] prima di aggiungere word alla lista. Se si verifica una eccezione in quanto la chiave word[0] non si trova ancora nel dizionario, il gestore di eccezione esegue tutto lo stesso lavoro per determinare word[0] (3 opcode) ed impostare self.by_letter[word[0]] ad una nuova lista che contiene la parola.

Una tecnica per eliminare l'impostazione dell'eccezione è di popolare precedentemente self.by_letter con una lista per ognuna delle lettere dell'alfabeto. In questo modo si dovrebbe sempre trovare la lista alla quale si vuole assegnare la nuova parola, ed il valore può essere salvato dopo la ricerca .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# dis_faster_loop.py

#!/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)

La modifica riduce il numero di opcode di circa la metà, ma riduce il tempo di esecuzione solo fino a 0.0121 circa (sul mio computer - n.d.t.). Ovviamente la gestione dell'eccezione generava qualche appesantimento, ma non così tanto.

$ python3 dis_test_loop.py dis_faster_loop

 19           0 LOAD_FAST                1 (words)
              2 GET_ITER
        >>    4 FOR_ITER                24 (to 30)
              6 STORE_FAST               2 (word)

 20           8 LOAD_FAST                0 (self)
             10 LOAD_ATTR                0 (by_letter)
             12 LOAD_FAST                2 (word)
             14 LOAD_CONST               1 (0)
             16 BINARY_SUBSCR
             18 BINARY_SUBSCR
             20 LOAD_METHOD              1 (append)
             22 LOAD_FAST                2 (word)
             24 CALL_METHOD              1
             26 POP_TOP
             28 JUMP_ABSOLUTE            4
        >>   30 LOAD_CONST               0 (None)
             32 RETURN_VALUE

Traceback (most recent call last):
  File "dis_test_loop.py", line 26, in <module>
    print('TEMPO: {:0.4f}'.format(t.timeit(iterations) / iterations))
  File "/usr/lib/python3.8/timeit.py", line 177, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 7, in inner
FileNotFoundError: [Errno 2] No such file or directory: '/usr/share/dict/mywords'

E' possibile migliorare ulteriormente le prestazioni spostando la ricerca della lettera self.by_letter all'esterno del ciclo (il valore, dopo tutto, non cambia).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# dis_fastest_loop.py

#!/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)

Gli opcode 0-4 adesso cercano il valore di self.by_letter e lo salvano come variabile locale by_letter. L'uso di una variabile locale richiede un solo opcode, in luogo dei 2 (l'istruzione 16 utilizza LOAD_FAST per piazzare il dizionario nello stack). Dopo questa modifica, il tempo di esecuzione si è ridotto a 0.010 secondi circa (sul mio computer - n.d.t.).

$ python3  dis_test_loop.py dis_fastest_loop

 16           0 LOAD_FAST                0 (self)
              2 LOAD_ATTR                0 (by_letter)
              4 STORE_FAST               2 (by_letter)

 17           6 LOAD_FAST                1 (words)
              8 GET_ITER
        >>   10 FOR_ITER                22 (to 34)
             12 STORE_FAST               3 (word)

 18          14 LOAD_FAST                2 (by_letter)
             16 LOAD_FAST                3 (word)
             18 LOAD_CONST               1 (0)
             20 BINARY_SUBSCR
             22 BINARY_SUBSCR
             24 LOAD_METHOD              1 (append)
             26 LOAD_FAST                3 (word)
             28 CALL_METHOD              1
             30 POP_TOP
             32 JUMP_ABSOLUTE           10
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

Traceback (most recent call last):
  File "dis_test_loop.py", line 26, in <module>
    print('TEMPO: {:0.4f}'.format(t.timeit(iterations) / iterations))
  File "/usr/lib/python3.8/timeit.py", line 177, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 7, in inner
FileNotFoundError: [Errno 2] No such file or directory: '/usr/share/dict/mywords'

Una ulteriore ottimizzazione, suggerita da Brandon Rhodes è di eliminare interamente la versione Python del ciclo for. Se si utilizza itertools.groupby() per sistemare l'input, l'iterazione viene spostata in C. E' possibile farlo in sicurezza visto che si sa che i dati in input sono già ordinati, viceversa avrebbero dovuto essere ordinati in precedenza.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# dis_eliminate_loop.py

#!/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):
        # Disposti per lettera
        grouped = itertools.groupby(
            words,
            key=operator.itemgetter(0),
        )
        # Salva gli insiemi di parole disposti
        self.by_letter = {
            group[0][0]: group
            for group in grouped
        }

La versione che utilizza itertools impiega solamente 0.0056 secondi circa per essere eseguita (meno della metà del tempo della versione di partenza sul mio computer - n.d.t.).

$ python3   dis_test_loop.py dis_eliminate_loop

 18           0 LOAD_GLOBAL              0 (itertools)
              2 LOAD_ATTR                1 (groupby)

 19           4 LOAD_FAST                1 (words)

 20           6 LOAD_GLOBAL              2 (operator)
              8 LOAD_METHOD              3 (itemgetter)
             10 LOAD_CONST               1 (0)
             12 CALL_METHOD              1

 18          14 LOAD_CONST               2 (('key',))
             16 CALL_FUNCTION_KW         2
             18 STORE_FAST               2 (grouped)

 23          20 LOAD_CONST               3 (<code object <dictcomp> at 0x7f1da9c0fa80, file "/dati/dev/python/pymotw3restyling/dumpscripts/dis_eliminate_loop.py", line 23>)
             22 LOAD_CONST               4 ('Dictionary.load_data.<locals>.<dictcomp>')
             24 MAKE_FUNCTION            0

 25          26 LOAD_FAST                2 (grouped)

 23          28 GET_ITER
             30 CALL_FUNCTION            1
             32 LOAD_FAST                0 (self)
             34 STORE_ATTR               4 (by_letter)
             36 LOAD_CONST               0 (None)
             38 RETURN_VALUE

Disassembly of <code object <dictcomp> at 0x7f1da9c0fa80, file "/dati/dev/python/pymotw3restyling/dumpscripts/dis_eliminate_loop.py", line 23>:
 23           0 BUILD_MAP                0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                18 (to 24)

 25           6 STORE_FAST               1 (group)

 24           8 LOAD_FAST                1 (group)
             10 LOAD_CONST               0 (0)
             12 BINARY_SUBSCR
             14 LOAD_CONST               0 (0)
             16 BINARY_SUBSCR
             18 LOAD_FAST                1 (group)
             20 MAP_ADD                  2
             22 JUMP_ABSOLUTE            4
        >>   24 RETURN_VALUE

Traceback (most recent call last):
  File "dis_test_loop.py", line 26, in <module>
    print('TEMPO: {:0.4f}'.format(t.timeit(iterations) / iterations))
  File "/usr/lib/python3.8/timeit.py", line 177, in timeit
    timing = self.inner(it, self.timer)
  File "<timeit-src>", line 7, in inner
FileNotFoundError: [Errno 2] No such file or directory: '/usr/share/dict/mywords'

Ottimizzazioni del Compilatore

Il disassemblare sorgente compilato rivela anche alcune delle ottimizzazioni eseguite dal compilatore. Ad esempio espressioni letterali sono unite durante la compilazione, dove possibile.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# dis_constant_folding.py

#!/usr/bin/env python3
# encoding: utf-8

# Uniti
i = 1 + 2
f = 3.4 * 5.6
s = 'Ciao,' + ' Mondo!'

# Non uniti
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastico!'

Nessun valore nelle espressioni nelle righe da 7 a 9 possono modificare il modo in cui sono eseguite le operazioni, quindi il risultato delle espressioni può essere calcolato in fase di compilazione ed unite in singole istruzioni LOAD_CONST. Il che non è vero per le istruzioni nelle righe da 12 a 14. Visto che in quelle espressioni è coinvolta una variabile, ed una variabile potrebbe fare riferimento ad un oggetto che sovrascrive l'operatore coinvolto, la valutazione deve essere differita al tempo dell'esecuzione.

$ python3 -m dis dis_constant_folding.py

  7           0 LOAD_CONST               0 (3)
              2 STORE_NAME               0 (i)

  8           4 LOAD_CONST               1 (19.04)
              6 STORE_NAME               1 (f)

  9           8 LOAD_CONST               2 ('Ciao, Mondo!')
             10 STORE_NAME               2 (s)

 12          12 LOAD_NAME                0 (i)
             14 LOAD_CONST               0 (3)
             16 BINARY_MULTIPLY
             18 LOAD_CONST               3 (4)
             20 BINARY_MULTIPLY
             22 STORE_NAME               3 (I)

 13          24 LOAD_NAME                1 (f)
             26 LOAD_CONST               4 (2)
             28 BINARY_TRUE_DIVIDE
             30 LOAD_CONST               0 (3)
             32 BINARY_TRUE_DIVIDE
             34 STORE_NAME               4 (F)

 14          36 LOAD_NAME                2 (s)
             38 LOAD_CONST               5 ('\n')
             40 BINARY_ADD
             42 LOAD_CONST               6 ('Fantastico!')
             44 BINARY_ADD
             46 STORE_NAME               5 (S)
             48 LOAD_CONST               7 (None)
             50 RETURN_VALUE

Vedere anche:

dis
La documentazione della libreria standard per questo modulo, compreso l'elenco delle istruzioni bytecode
Python Essential Reference, 4th Edition, David M. Beazley
thomas.apestart.org "Python Disassembly"
Una breve discussione circa la differenza tra l'immissione di valori in un dizionario tra Python 2.5 e 2.6
Why is looping over range() in Python faster than using a while loop?
Una discussione su StackOverflow.com confrontando due esempi di iterazione visti attraverso i loro bytecode disassemblati.
Decorator for binding constants at compile time
Una ricetta di Python Cookbook di Raymond Hettinger e Skip Montanaro con un decoratore di funzione che riscrive i bytecode per una funzione per inserire costanti globali per evitare ricerche di nomi in fase di esecuzione.