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