dis – Python バイトコードディスアセンブラ

目的:解析のためにコードオブジェクトを人間が読めるバイトコードに変換する
利用できるバージョン:1.4 以上

dis モジュールは Python バイトコードを “ディスアセンブル” して人間が読める形態にする機能を提供します。インタープリタが実行するバイトコードをレビューすることは、タイトループを手作業で調整したり、その他の最適化のために効率化したりする良い方法です。さらにスレッド制御が切り替わる位置を推定できるのでマルチスレッドアプリケーションの競合状態を見つけることにも役立ちます。

基本的なディスアセンブル処理

dis.dis() という関数はディスアセンブルされた Python ソースコード(モジュール、クラス、メソッド、関数やコードオブジェクト)を表示します。次のようなモジュールに対して、

1
2
3
4
#!/usr/bin/env python
# encoding: utf-8

my_dict = { 'a':1 }

コマンドラインから dis モジュールを実行することでディスアセンブルすることができます。その出力はオリジナルソースの行番号、コードオブジェクト内の命令 “アドレス”、オペコード名、オペコードに渡される引数でカラムが構成されます。

$ python -m dis dis_simple.py
  4           0 BUILD_MAP                1
              3 LOAD_CONST               0 (1)
              6 LOAD_CONST               1 ('a')
              9 STORE_MAP
             10 STORE_NAME               0 (my_dict)
             13 LOAD_CONST               2 (None)
             16 RETURN_VALUE

このケースでは、サンプルソースはディクショナリを作成して、そこで存在するために5つの異なるオペレーションに変換されます。それからローカル変数へその結果を保存します。Python インタープリタはスタックベースなので、最初のステップは LOAD_CONST を使用して正しい順番でスタックの中に定数を追加することです。それからディクショナリに追加される新たなキーと値をスタックから取り出すために STORE_MAP を使用します。その結果のオブジェクトは STORE_NAME で “my_dict” という名前に束縛されます。

関数をディスアセンブルする

不幸にも、モジュール全体をディスアセンブルしても自動的に関数の内部を取り出すことはできません。例えば、次のようなモジュールを実行します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env python
# encoding: utf-8

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

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

その出力結果はスタック上にコードオブジェクトが読み込まれることを表示して、そのコードオブジェクトが関数(LOAD_CONST, MAKE_FUNCTION)に変わりますが、それはその関数の内部では ありません

$ python -m dis dis_function.py
  4           0 LOAD_CONST               0 (<code object f at 0x10046fb30, file "dis_function.py", line 4>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (f)

  8           9 LOAD_NAME                1 (__name__)
             12 LOAD_CONST               1 ('__main__')
             15 COMPARE_OP               2 (==)
             18 POP_JUMP_IF_FALSE       49

  9          21 LOAD_CONST               2 (-1)
             24 LOAD_CONST               3 (None)
             27 IMPORT_NAME              2 (dis)
             30 STORE_NAME               2 (dis)

 10          33 LOAD_NAME                2 (dis)
             36 LOAD_ATTR                2 (dis)
             39 LOAD_NAME                0 (f)
             42 CALL_FUNCTION            1
             45 POP_TOP
             46 JUMP_FORWARD             0 (to 49)
        >>   49 LOAD_CONST               3 (None)
             52 RETURN_VALUE

関数の内部を見るためには dis.dis() にその関数を渡す必要があります。

$ python dis_function.py
  5           0 LOAD_GLOBAL              0 (len)
              3 LOAD_FAST                0 (args)
              6 CALL_FUNCTION            1
              9 STORE_FAST               1 (nargs)

  6          12 LOAD_FAST                1 (nargs)
             15 PRINT_ITEM
             16 LOAD_FAST                0 (args)
             19 PRINT_ITEM
             20 PRINT_NEWLINE
             21 LOAD_CONST               0 (None)
             24 RETURN_VALUE

クラス

同様にコードオブジェクトに変わる全てのメソッドがディスアセンブルされるように dis へクラスを渡すこともできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
# encoding: utf-8

import dis

class MyObject(object):
    """Example for dis."""
    
    CLASS_ATTRIBUTE = 'some value'
    
    def __init__(self, name):
        self.name = name
    
    def __str__(self):
        return 'MyObject(%s)' % self.name

dis.dis(MyObject)
$ python dis_class.py
Disassembly of __init__:
 12           0 LOAD_FAST                1 (name)
              3 LOAD_FAST                0 (self)
              6 STORE_ATTR               0 (name)
              9 LOAD_CONST               0 (None)
             12 RETURN_VALUE

Disassembly of __str__:
 15           0 LOAD_CONST               1 ('MyObject(%s)')
              3 LOAD_FAST                0 (self)
              6 LOAD_ATTR                0 (name)
              9 BINARY_MODULO
             10 RETURN_VALUE

デバッグのためにディスアセンブルする

例外をデバッグするとき、バイトコードが問題を引き起こすことを確認するために役に立つときがあります。エラーが発生する付近のコードをディスアセンブルする方法があります。

1つ目は最後に発生した例外に関するレポートのためにインタープリタで dis.distb() を使用します。 dis.distb() に引数が渡されなかったら最後に発生した例外を探して、その例外を引き起こしたスタックをディスアセンブルして表示します。

$ python
Python 2.6.2 (r262:71600, Apr 16 2009, 09:17:39)
[GCC 4.0.1 (Apple Computer, Inc. build 5250)] 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.distb()
  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 という変数が定義されていないので、その名前で関連付けられた値をスタック上にロードすることができません。

コード内で dis.distb() に直接アクティブなトレースバックを渡すことでそれに関する情報を表示することもできます。このサンプルでは DivideByZero 例外が発生しますが、コード内の数式は2つの除算があるので、どちらがゼロかは分かりません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/usr/bin/env python
# encoding: utf-8

i = 1
j = 0
k = 3

# ... 多くの行が削除される ...

try:
    result = k * (i / j) + (i / k)
except:
    import dis
    import sys
    exc_type, exc_value, exc_tb = sys.exc_info()
    dis.distb(exc_tb)

ディスアセンブルされたソースでスタック上に問題のある値がロードされるとき、その値を見つけることは簡単です。問題のあるオペレーションは --> でハイライトされます。そして j に格納されている 0 という値がスタック上に追加された場所を見つけるためにほんの数行前を探すだけです。

$ python dis_traceback.py
  4           0 LOAD_CONST               0 (1)
              3 STORE_NAME               0 (i)

  5           6 LOAD_CONST               1 (0)
              9 STORE_NAME               1 (j)

  6          12 LOAD_CONST               2 (3)
             15 STORE_NAME               2 (k)

 10          18 SETUP_EXCEPT            26 (to 47)

 11          21 LOAD_NAME                2 (k)
             24 LOAD_NAME                0 (i)
             27 LOAD_NAME                1 (j)
    -->      30 BINARY_DIVIDE
             31 BINARY_MULTIPLY
             32 LOAD_NAME                0 (i)
             35 LOAD_NAME                2 (k)
             38 BINARY_DIVIDE
             39 BINARY_ADD
             40 STORE_NAME               3 (result)
             43 POP_BLOCK
             44 JUMP_FORWARD            65 (to 112)

 12     >>   47 POP_TOP
             48 POP_TOP
             49 POP_TOP

 13          50 LOAD_CONST               3 (-1)
             53 LOAD_CONST               4 (None)
             56 IMPORT_NAME              4 (dis)
             59 STORE_NAME               4 (dis)

 14          62 LOAD_CONST               3 (-1)
             65 LOAD_CONST               4 (None)
             68 IMPORT_NAME              5 (sys)
             71 STORE_NAME               5 (sys)

 15          74 LOAD_NAME                5 (sys)
             77 LOAD_ATTR                6 (exc_info)
             80 CALL_FUNCTION            0
             83 UNPACK_SEQUENCE          3
             86 STORE_NAME               7 (exc_type)
             89 STORE_NAME               8 (exc_value)
             92 STORE_NAME               9 (exc_tb)

 16          95 LOAD_NAME                4 (dis)
             98 LOAD_ATTR               10 (distb)
            101 LOAD_NAME                9 (exc_tb)
            104 CALL_FUNCTION            1
            107 POP_TOP
            108 JUMP_FORWARD             1 (to 112)
            111 END_FINALLY
        >>  112 LOAD_CONST               4 (None)
            115 RETURN_VALUE

ループのパフォーマンス解析

エラーをデバッグすることとは別に dis はパフォーマンスの問題を特定することにも役立ちます。ディスアセンブルされたコードを調べることは、数少ない Python のバイトコード命令が非効率なバイトコードセットへ変換するタイトループで特に役に立ちます。単語のリストを読み込み、その最初の文字で単語をグループ分けするクラス Dictionary の異なる実装を数個調べることで、ディスアセンブルすることがどのように役に立つかを理解できます。

先ずはテストドライバアプリケーションです。

import dis
import sys
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)', 
    """from %(module_name)s import Dictionary
words = [l.strip() for l in open('/usr/share/dict/words', 'rt')]
    """ % locals()
    )
iterations = 10
print 'TIME: %0.4f' % (t.timeit(iterations)/iterations)

Dictionary クラスの個々の実体化を処理するために dis_test_loop.py を使用します。

率直に考えた Dictionary の実装は次のように単語を探すかもしれません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: utf-8

class Dictionary(object):

    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]

このソースの出力結果は Mac OS X で /usr/share/dict/words のコピーである 234936 単語を読み込むために 0.1074 秒かかりました。それはあまり悪い結果ではありませんが、次のようにディスアセンブルして、必要ならもっと効率的に動作することを理解できます。オペコード13でループに入ると、例外コンテキスト(SETUP_EXCEPT)をセットアップします。それからリストへ word を追加する前に self.by_letter[word[0]] を見つけるために6個のオペコードを取ります。ディクショナリにまだ word[0] が存在しないために例外が発生するなら、その例外ハンドラは word[0] (3 オペコード) を調べるために全て同じように動作します。そして、その単語を含む新たなリストに対して self.by_letter[word[0]] をセットします。

$ python dis_test_loop.py dis_slow_loop
 11           0 SETUP_LOOP              84 (to 87)
              3 LOAD_FAST                1 (words)
              6 GET_ITER
        >>    7 FOR_ITER                76 (to 86)
             10 STORE_FAST               2 (word)

 12          13 SETUP_EXCEPT            28 (to 44)

 13          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
             39 POP_TOP
             40 POP_BLOCK
             41 JUMP_ABSOLUTE            7

 14     >>   44 DUP_TOP
             45 LOAD_GLOBAL              2 (KeyError)
             48 COMPARE_OP              10 (exception match)
             51 JUMP_IF_FALSE           27 (to 81)
             54 POP_TOP
             55 POP_TOP
             56 POP_TOP
             57 POP_TOP

 15          58 LOAD_FAST                2 (word)
             61 BUILD_LIST               1
             64 LOAD_FAST                0 (self)
             67 LOAD_ATTR                0 (by_letter)
             70 LOAD_FAST                2 (word)
             73 LOAD_CONST               1 (0)
             76 BINARY_SUBSCR
             77 STORE_SUBSCR
             78 JUMP_ABSOLUTE            7
        >>   81 POP_TOP
             82 END_FINALLY
             83 JUMP_ABSOLUTE            7
        >>   86 POP_BLOCK
        >>   87 LOAD_CONST               0 (None)
             90 RETURN_VALUE

TIME: 0.1074

例外のセットアップを取り除くための1つのテクニックは全てのアルファベット self.by_letter を事前に1つのリスト内に存在させることです。それはつまり、新たな単語はいつもそのリストで見つかるので、最初の文字の値の探索と単語の保存処理だけになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: utf-8

import string

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = dict( (letter, []) 
                                for letter in string.letters)
        self.load_data(words)

    def load_data(self, words):
        for word in words:
            self.by_letter[word[0]].append(word)

その変更はオペコード数を半分に減らしますが、たった 0.0984 秒に実行時間を短縮できただけです。例外の扱いは明らかにオーバヘッドになりますが、そう大きなものではありません。

$ python dis_test_loop.py dis_faster_loop
 14           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)

 15          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
             36 POP_TOP
             37 JUMP_ABSOLUTE            7
        >>   40 POP_BLOCK
        >>   41 LOAD_CONST               0 (None)
             44 RETURN_VALUE

TIME: 0.0984

self.by_letter の名前のルックアップをループの外側に移動することで、そのパフォーマンスを大幅に改善することができます(そうしても、その値は変更しません)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/usr/bin/env python
# encoding: utf-8

import collections

class Dictionary(object):

    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 の値を見つけます。そして self.by_letter の値をローカル変数 by_letter に保存します。 2つのオペコードの代わりに、ローカル変数を使用することで1つのオペコードのみを取ります(ステートメント22はそのディクショナリをスタック上に追加するために LOAD_FAST を使用する)。この変更後に実行時間は 0.0842 秒へ減少します。

$ python dis_test_loop.py dis_fastest_loop
 13           0 LOAD_FAST                0 (self)
              3 LOAD_ATTR                0 (by_letter)
              6 STORE_FAST               2 (by_letter)

 14           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)

 15          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
             42 POP_TOP
             43 JUMP_ABSOLUTE           16
        >>   46 POP_BLOCK
        >>   47 LOAD_CONST               0 (None)
             50 RETURN_VALUE

TIME: 0.0842

Brandon Rhodes が提案したさらなる最適化は Python のソースから for ループを完全に排除することです。もし入力の単語を配置するために itertools.groupby() を使用するなら、その繰り返し処理は C 言語側へ移動されます。このサンプルではその入力の単語が既にソートされていることが分かっているので安全に実行することができます。もし入力の単語がソートされているか分からなかったら、先に入力の単語をソートする必要があります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
# encoding: utf-8

import operator
import itertools

class Dictionary(object):

    def __init__(self, words):
        self.by_letter = {}
        self.load_data(words)

    def load_data(self, words):
        # 文字で配置する
        grouped = itertools.groupby(words, key=operator.itemgetter(0))
        # 配置された単語セットを保存する
        self.by_letter = dict((group[0][0], group) for group in grouped)
        

itertools を使用すると実行時間はたった 0.0543 秒です。オリジナルの実行時間のちょうど半分です。

$ python dis_test_loop.py dis_eliminate_loop
 15           0 LOAD_GLOBAL              0 (itertools)
              3 LOAD_ATTR                1 (groupby)
              6 LOAD_FAST                1 (words)
              9 LOAD_CONST               1 ('key')
             12 LOAD_GLOBAL              2 (operator)
             15 LOAD_ATTR                3 (itemgetter)
             18 LOAD_CONST               2 (0)
             21 CALL_FUNCTION            1
             24 CALL_FUNCTION          257
             27 STORE_FAST               2 (grouped)

 17          30 LOAD_GLOBAL              4 (dict)
             33 LOAD_CONST               3 (<code object <genexpr> at 0x7e7b8, file "/Users/dhellmann/Documents/PyMOTW/dis/PyMOTW/dis/dis_eliminate_loop.py", line 17>)
             36 MAKE_FUNCTION            0
             39 LOAD_FAST                2 (grouped)
             42 GET_ITER
             43 CALL_FUNCTION            1
             46 CALL_FUNCTION            1
             49 LOAD_FAST                0 (self)
             52 STORE_ATTR               5 (by_letter)
             55 LOAD_CONST               0 (None)
             58 RETURN_VALUE

TIME: 0.0543

コンパイラの最適化

コンパイルされたソースをディスアセンブルすることはコンパイラによって最適化が行われたことを表します。例えば、リテラル表記はコンパイル中にできるだけ折り畳まれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/usr/bin/env python
# encoding: utf-8

# 折り畳む
i = 1 + 2
f = 3.4 * 5.6
s = 'Hello,' + ' World!'

# 折り畳まれない
I = i * 3 * 4
F = f / 2 / 3
S = s + '\n' + 'Fantastic!'

5-7行目のリテラル表記はオペレーションが実行されている途中で変更されないのでコンパイル時に1つの LOAD_CONST 命令に折り畳まれて算出されます。10-12行目に関してはそうではありません。というのは、変数は評価されて、その変数が実行したオペレータをオーバーロードするオブジェクトを参照する可能性があるからです。そのため、変数の評価は実行時に遅延させる必要があります。

$ python -m dis dis_constant_folding.py
  5           0 LOAD_CONST              11 (3)
              3 STORE_NAME               0 (i)

  6           6 LOAD_CONST              12 (19.04)
              9 STORE_NAME               1 (f)

  7          12 LOAD_CONST              13 ('Hello, World!')
             15 STORE_NAME               2 (s)

 10          18 LOAD_NAME                0 (i)
             21 LOAD_CONST               6 (3)
             24 BINARY_MULTIPLY
             25 LOAD_CONST               7 (4)
             28 BINARY_MULTIPLY
             29 STORE_NAME               3 (I)

 11          32 LOAD_NAME                1 (f)
             35 LOAD_CONST               1 (2)
             38 BINARY_DIVIDE
             39 LOAD_CONST               6 (3)
             42 BINARY_DIVIDE
             43 STORE_NAME               4 (F)

 12          46 LOAD_NAME                2 (s)
             49 LOAD_CONST               8 ('\n')
             52 BINARY_ADD
             53 LOAD_CONST               9 ('Fantastic!')
             56 BINARY_ADD
             57 STORE_NAME               5 (S)
             60 LOAD_CONST              10 (None)
             63 RETURN_VALUE

See also

dis
バイトコード命令 を含む本モジュールの標準ライブラリドキュメント
Python Essential Reference, 4th Edition, David M. Beazley
http://www.informit.com/store/product.aspx?isbn=0672329786
thomas.apestaart.org “Python Disassembly”
Python 2.5 と 2.6 でディクショナリへ値を格納することの違いに関する短い議論
Python の range() 上のループは while ループを使用するよりなぜ速いか?
ディスアセンブルされたバイトコードを通して2つのループのサンプルを比較する StackOverflow.com の議論
コンパイル時に定数を束縛するためのデコレータ
Raymond Hettinger と Skip Montanaro による Python クックブックのレシピで実行時に名前を検索しないようにグローバル定数を追加する関数のバイトコードを書き直すデコレータ
Bookmark and Share