Автор оригинала: Doug Hellmann.
Цель:
Преобразуйте объекты кода в удобочитаемое представление байт-коды для анализа.
Модуль dis
включает функции для работы с байт-кодом Python путем дизассемблирования его в более удобочитаемую форму. Просмотр байт-кодов, выполняемых интерпретатором, – хороший способ вручную настроить жесткие циклы и выполнить другие виды оптимизации. Это также полезно для поиска условий гонки в многопоточных приложениях, поскольку его можно использовать для оценки точки в коде, где может переключиться управление потоком.
Предупреждение
Использование байт-кодов – это специфическая для версии деталь реализации интерпретатора CPython. Обратитесь к Include/opcode.h
в исходном коде, чтобы узнать версию интерпретатора, которую вы используете, чтобы найти канонический список байт-кодов.
Базовая разборка
Функция dis ()
печатает дизассемблированное представление источника кода Python (модуля, класса, метода, функции или объекта кода). Модуль, такой как dis_simple.py
, можно разобрать, запустив dis
из командной строки.
dis_simple.py
1 2 3 4
#!/usr/bin/env python3 # encoding: utf-8 my_dict {'a': 1}
Вывод организован в столбцы с исходным номером строки исходного кода, адресом инструкции в объекте кода, именем кода операции и любыми аргументами, переданными коду операции.
$ 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
В этом случае источник преобразуется в четыре различные операции для создания и заполнения словаря, а затем сохраняет результаты в локальную переменную. Поскольку интерпретатор Python основан на стеке, первым делом нужно поместить константы в стек в правильном порядке с помощью LOAD_CONST
, а затем использовать BUILD_MAP
, чтобы извлечь новый ключ и значение для добавления в словарь. Результирующий объект dict
привязывается к имени my_dict
с помощью STORE_NAME
.
Функции разборки
К сожалению, дизассемблирование всего модуля не приводит к автоматическому преобразованию в функции.
dis_function.py
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)
Результаты дизассемблирования dis_function.py
показывают операции по загрузке объекта кода функции в стек и последующему преобразованию его в функцию ( LOAD_CONST
, MAKE_FUNCTION
), за которым следует тело функции.
$ python3 -m dis dis_function.py
5 0 LOAD_CONST 0 ()
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_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 :
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
Более ранние версии Python не включали тела функций в дизассемблирование модулей автоматически. Чтобы увидеть дизассемблированную версию функции, передайте функцию непосредственно в 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
Чтобы распечатать сводку функции, включая информацию об используемых ею аргументах и именах, вызовите show_code ()
, передав функцию в качестве первого аргумента.
#!/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)
Аргумент для show_code ()
передается в code_info ()
, который возвращает хорошо отформатированную сводку функции, метода, строки кода или другого объекта кода, готового к работе. напечатан.
$ 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
Классы
Классы можно передавать в dis ()
, и в этом случае все методы дизассемблируются по очереди.
dis_class.py
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)
Методы перечислены в алфавитном порядке, а не в том порядке, в котором они появляются в файле.
$ 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_METHOD 0 (format) 4 LOAD_FAST 0 (self) 6 LOAD_ATTR 1 (name) 8 CALL_METHOD 1 10 RETURN_VALUE
Исходный код
Зачастую работать с исходным кодом программы удобнее, чем с самими объектами кода. Функции в dis
принимают строковые аргументы, содержащие исходный код, и преобразуют их в объекты кода перед выполнением дизассемблирования или другого вывода.
dis_string.py
import dis code """ my_dict = {'a': 1} """ print('Disassembly:\n') dis.dis(code) print('\nCode details:\n') dis.show_code(code)
Передача строки позволяет вам сохранить этап компиляции кода и сохранить ссылку на результаты самостоятельно, что более удобно в случаях, когда проверяются операторы вне функции.
$ 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:Filename: 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
Использование дизассемблирования для отладки
Иногда при отладке исключения может быть полезно посмотреть, какой байт-код вызвал проблему. Есть несколько способов дизассемблировать код вокруг ошибки. Первый заключается в использовании dis ()
в интерактивном интерпретаторе для сообщения о последнем исключении. Если в dis ()
не передан аргумент, он ищет исключение и показывает разборку вершины стека, которая его вызвала.
$ 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 "", line 1, in 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 >>>
-->
после номера строки указывает код операции, вызвавший ошибку. Переменная i
не определена, поэтому значение, связанное с именем, не может быть загружено в стек.
Программа также может распечатать информацию об активной трассировке, передав ее напрямую в distb ()
. В этом примере есть исключение DivideByZero
, но поскольку формула имеет два деления, может быть неясно, какая часть равна нулю.
dis_traceback.py
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)
Ошибка легко заметить, если она загружена в стек в разобранной версии. Неправильная операция выделяется -->
, а предыдущая строка помещает значение для j
в стек.
$ 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) ...trimmed...
Анализ производительности петель
Помимо ошибок отладки, dis
также может помочь выявить проблемы с производительностью. Изучение дизассемблированного кода особенно полезно при работе с жесткими циклами, когда количество инструкций Python невелико, но они переводятся в неэффективный набор байт-кодов. Полезность дизассемблирования можно увидеть, изучив несколько различных реализаций класса Dictionary
, который читает список слов и группирует их по первой букве.
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/words', 'rt') ] """).format(module_namemodule_name) ) iterations 10 print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))
Приложение тестового драйвера dis_test_loop.py
можно использовать для запуска каждого воплощения класса Dictionary
, начиная с простой, но медленной реализации.
dis_slow_loop.py
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]
Запуск тестовой программы с этой версией показывает дизассемблированную программу и время, необходимое для ее выполнения.
$ 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
Предыдущий вывод показывает, что dis_slow_loop.py
занимает 0,0568 секунды для загрузки 235886 слов в копию /usr/share/dict/words
в OS X. Это не так уж плохо , но сопутствующая разборка показывает, что цикл выполняет больше работы, чем нужно. Когда он входит в цикл в коде операции 13, он устанавливает контекст исключения ( SETUP_EXCEPT
). Затем требуется шесть кодов операций, чтобы найти self.by_letter [word [0]]
перед добавлением word
в список. Если возникло исключение из-за того, что word [0]
еще нет в словаре, обработчик исключения выполняет ту же работу, чтобы определить word [0]
(три кода операции) и устанавливает self.by_letter [word [0]]
в новый список, содержащий слово.
Одним из способов устранения настройки исключения является предварительное заполнение self.by_letter
одним списком для каждой буквы алфавита. Это означает, что список для нового слова всегда должен быть найден, и значение может быть сохранено после поиска.
dis_faster_loop.py
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)
Это изменение вдвое сокращает количество кодов операций, но сокращает время только до 0,0567 секунды. Очевидно, что обработка исключений имела некоторые накладные расходы, но не значительные.
$ 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
Производительность можно дополнительно улучшить, переместив поиск для self.by_letter
за пределы цикла (значение в конце концов не меняется).
dis_fastest_loop.py
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)
Коды операций 0–6 теперь находят значение self.by_letter
и сохраняют его как локальную переменную by_letter
. Использование локальной переменной требует только одного кода операции вместо двух (в инструкции 22 используется LOAD_FAST
для помещения словаря в стек). После этого изменения время работы уменьшилось до 0,0473 секунды.
$ 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
Дальнейшая оптимизация, предложенная Брэндоном Роудсом, заключается в полном исключении версии Python цикла for
. Если для упорядочивания ввода используется itertools.groupby ()
, итерация перемещается в C. Это безопасно, поскольку известно, что входные данные отсортированы. Если бы это было не так, программе нужно было бы сначала их отсортировать.
dis_eliminate_loop.py
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, keyoperator.itemgetter(0), ) # Save arranged sets of words self.by_letter { group[0][0]: group for group in grouped }
Версия itertools
запускается всего за 0,0332 секунды, что составляет около 60% времени выполнения оригинала.
$ 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 ( at 0x101517930, file ".../dis_eliminate_loop.py",
line 21>)
33 LOAD_CONST 4
('Dictionary.load_data..')
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
Оптимизация компилятора
Дизассемблирование скомпилированного исходного кода также раскрывает некоторые оптимизации, сделанные компилятором. Например, буквальные выражения сворачиваются во время компиляции, когда это возможно.
dis_constant_folding.py
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!'
Ни одно из значений в выражениях в строках 5-7 не может изменить способ выполнения операции, поэтому результат выражений можно вычислить во время компиляции и свернуть в отдельные инструкции LOAD_CONST
. Это не относится к строкам 10–12. Поскольку в этих выражениях участвует переменная, и эта переменная может ссылаться на объект, который перегружает задействованный оператор, оценка должна быть отложена до времени выполнения.
$ python3 -m dis dis_constant_folding.py 5 0 LOAD_CONST 0 (3) 2 STORE_NAME 0 (i) 6 4 LOAD_CONST 1 (19.04) 6 STORE_NAME 1 (f) 7 8 LOAD_CONST 2 ('Hello, World!') 10 STORE_NAME 2 (s) 10 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) 11 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) 12 36 LOAD_NAME 2 (s) 38 LOAD_CONST 5 ('\n') 40 BINARY_ADD 42 LOAD_CONST 6 ('Fantastic!') 44 BINARY_ADD 46 STORE_NAME 5 (S) 48 LOAD_CONST 7 (None) 50 RETURN_VALUE
Смотрите также
- Стандартная библиотечная документация для DIS – включает список инструкции по байт-коду .
Include/opcode.h
– исходный код интерпретатора CPython определяет байтовые коды вopcode.h
.- Важный справочник по Python , 4-е издание, Дэвид М. Бизли – http://www .informit.com/store/product.aspx? isbn = 0672329786 .
- thomas.apestaart.org «Разборка Python» – краткое обсуждение разницы между хранением значений в словаре между Python 2.5 и 2.6.
- Почему цикл превышает диапазон ( ) в Python быстрее, чем при использовании цикла while? – обсуждение на StackOverflow.com двух примеров циклов с использованием их дизассемблированных байт-кодов.
- Декоратор для привязки констант во время компиляции – рецепт Python Cookbook от Раймонда Хеттингера и Скипа Монтанаро с декоратором функций, который перезаписывает байт-коды для функции вставки глобальных констант, чтобы избежать поиска имени во время выполнения.