18.3.6 循环的性能分析
除了调试错误,dis还有助于发现性能问题。检查反汇编的代码对于紧密循环尤其有用,在这些循环中,Python指令很少,但是这些指令会转换为一组效率很低的字节码。可以通过查看一个类Dictionary的不同实现来了解反汇编提供的帮助,这个类会读取一个单词列表,然后按其首字母分组。
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_name=module_name)
)
iterations = 10
print('TIME: {:0.4f}'.format(t.timeit(iterations) / iterations))
可以用测试驱动应用dis_test_loop.py来运行Dictionary类的各个实现,首先是一个简单但很慢的实现。
#!/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]
用这个版本运行测试程序时,会显示反汇编的程序,以及运行所花费的时间。
前面的输出显示,dis_slow_loop.py花费了0.0108秒来加载单词,这个性能不算太坏,不过相应的反汇编结果显示出循环做了很多不必要的工作。它在操作码16处进入循环时,程序建立了一个异常上下文(SETUP_EXCEPT)。然后在将word追加到列表之前,使用了6个操作码来查找self.by_letter [word[0]]。如果由于word[0]还不在字典中而生成一个异常,那么异常处理器会做完全相同的工作来确定word0,并把self.by_letter[word[0]]设置为包含这个单词的一个新列表。要避免建立这个异常,一种技术是对应字母表中的各个字母分布用一个列表来预填充字典self.by_letter。这意味着总会找到新单词相应的列表,可以在查找之后保存值。
将self.by_letter的查找移到循环之外(毕竟值没有改变),可以进一步提高性能。
#!/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.0089秒。
Brandon Rhodes还建议了进一步的优化,可以完全消除Python版本的for循环。如果使用itertools.groupby()来整理输入,那么将把迭代处理移至C。这个转移很安全,因为输入已经是有序的。如果并非如此,则程序需要先进行排序。
#!/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,
key=operator.itemgetter(0),
)
# Save arranged sets of words.
self.by_letter = {
group[0][0]: group
for group in grouped
}
这个itertools版本运行只需要0.0048秒,仅为原程序运行时间的约40%。