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 クックブックのレシピで実行時に名前を検索しないようにグローバル定数を追加する関数のバイトコードを書き直すデコレータ