Рубрики
Без рубрики

dis – дизассемблер байт-кода Python

Автор оригинала: 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

Смотрите также