花下猫语:性能优化是每个程序员的必修课,但你是否想过,除了更换算法,还有哪些“大招”?这篇文章堪称典范,它将一个普通的函数,通过四套组合拳,硬生生把性能提升了 330 倍!作者不仅展示了“术”,更传授了“道”。让我们一起跟随作者的思路,体验一次酣畅淋漓的优化之旅。
PS.本文选自最新一期Python 潮流周刊,如果你对优质文章感兴趣,诚心推荐你订阅我们的专栏。
作者:Itamar Turner-Trauring
译者:豌豆花下猫@Python猫
英文:330× faster: Four different ways to speed up your code
声明:本翻译是出于交流学习的目的,为便于阅读,部分内容略有改动。转载请保留作者信息。
温馨提示: 本文原始版本与当前略有不同,比如曾经提到过500倍加速;本文已根据实际情况重新梳理,使论证更清晰。
当你的 Python 代码慢如蜗牛,而你渴望它快如闪电时,其实有很多种提速方式,从并行化到编译扩展应有尽有。如果只盯着一种方法,往往会错失良机,最终的代码也难以达到极致性能。
为了不错过任何潜在的提速机会,我们可以从“实践”的角度来思考。每种实践:
- 以独特方式加速你的代码
- 涉及不同的技能和知识
- 可以单独应用
- 也可以组合应用,获得更大提升
为了让这一点更具体,本文将通过一个案例演示多种实践的应用,具体包括:
- 效率(Efficiency): 消除浪费或重复的计算。
- 编译(Compilation): 利用编译型语言,并巧妙绕开编译器限制。
- 并行化(Parallelism): 充分发挥多核CPU的威力。
- 流程(Process): 采用能产出更快代码的开发流程。
我们将看到:
- 仅用效率实践,就能带来近 2倍 提速。
- 仅用编译实践,可实现 10倍 提速。
- 两者结合,速度更上一层楼。
- 最后加上并行化实践,最终实现 330倍 惊人加速。
我们的例子:统计字母频率
我们有一本英文书,简·奥斯汀的《诺桑觉寺》:- with open("northanger_abbey.txt") as f:
- TEXT = f.read()
复制代码 我们的目标是分析书中字母的相对频率。元音比辅音更常见吗?哪个元音最常见?
下面是最初的实现:- from collections import defaultdict
- def frequency_1(text):
- # 一个当键不存在时默认值为0的字典
- counts = defaultdict(lambda: 0)
- for character in text:
- if character.isalpha():
- counts[character.lower()] += 1
- return counts
复制代码 运行结果如下:- sorted(
- (count, letter) for (letter, count)
- in frequency_1(TEXT).items()
- )
复制代码- [(1, 'à'),
- (2, 'é'),
- (3, 'ê'),
- (111, 'z'),
- (419, 'q'),
- (471, 'j'),
- (561, 'x'),
- (2016, 'k'),
- (3530, 'v'),
- (5297, 'b'),
- (5404, 'p'),
- (6606, 'g'),
- (7639, 'w'),
- (7746, 'f'),
- (7806, 'y'),
- (8106, 'c'),
- (8628, 'm'),
- (9690, 'u'),
- (13431, 'l'),
- (14164, 'd'),
- (20675, 's'),
- (21107, 'r'),
- (21474, 'h'),
- (22862, 'i'),
- (24670, 'n'),
- (26385, 'a'),
- (26412, 'o'),
- (30003, 't'),
- (44251, 'e')]
复制代码 毫无意外,出现频率最高的字母是 "e"。
那我们如何让这个函数更快?
流程实践:测量与测试
软件开发不仅依赖于源代码、库、解释器、编译器这些“产物”,更离不开你的工作“流程”——也就是你做事的方法。性能优化同样如此。本文将介绍两种在优化过程中必不可少的流程实践:
- 通过基准测试和性能分析来测量代码速度。
- 测试优化后的代码,确保其行为与原始版本一致。
我们可以先用 line_profiler 工具分析函数,找出最耗时的代码行:- Line # Hits % Time Line Contents
- ========================================
- 3 def frequency_1(text):
- 4 # 一个当键不存在时默认值为0的字典
- 5 # available:
- 6 1 0.0 counts = defaultdict(lambda: 0)
- 7 433070 30.4 for character in text:
- 8 433069 27.3 if character.isalpha():
- 9 339470 42.2 counts[character.lower()] += 1
- 10 1 0.0 return counts
复制代码 效率实践:减少无用功
效率实践的核心,是用更少的工作量获得同样的结果。这类优化通常在较高的抽象层面进行,无需关心底层CPU细节,因此适用于大多数编程语言。其本质是通过改变计算逻辑来减少浪费。
减少内循环的工作量
从上面的性能分析可以看出,函数大部分时间都花在 counts[character.lower()] += 1 这行。显然,对每个字母都调用 character.lower() 是种浪费。我们一遍遍地把 "I" 转成 "i",甚至还把 "i" 转成 "i"。
优化思路:我们可以先分别统计大写和小写字母的数量,最后再合并,而不是每次都做小写转换。- def frequency_2(text):
- split_counts = defaultdict(lambda: 0)
- for character in text:
- if character.isalpha():
- split_counts[character] += 1
- counts = defaultdict(lambda: 0)
- for character, num in split_counts.items():
- counts[character.lower()] += num
- return counts
- # 确保新函数结果与旧函数完全一致
- assert frequency_1(TEXT) == frequency_2(TEXT)
复制代码说明:这里的 assert 就是流程实践的一部分。一个更快但结果错误的函数毫无意义。虽然你在最终文章里看不到这些断言,但它们在开发时帮我抓出了不少bug。
基准测试(也是流程实践的一环)显示,这个优化确实让代码更快了:
| frequency_1(TEXT) | 34,592.5 µs |
| frequency_2(TEXT) | 25,798.6 µs |
针对特定数据和目标进行优化
我们继续用效率实践,这次针对具体目标和数据进一步优化。来看下最新代码的性能分析:- Line # Hits % Time Line Contents
- ========================================
- 3 def frequency_2(text):
- 4 1 0.0 split_counts = defaultdict(lambda: 0)
- 5 433070 33.6 for character in text:
- 6 433069 32.7 if character.isalpha():
- 7 339470 33.7 split_counts[character] += 1
- 8
- 9 1 0.0 counts = defaultdict(lambda: 0)
- 10 53 0.0 for character, num in split_counts.items():
- 11 52 0.0 counts[character.lower()] += num
- 12 1 0.0 return counts
复制代码 可以看到,split_counts[character] += 1 依然是耗时大户。怎么加速?答案是用 list 替换 defaultdict(本质上是 dict)。list 的索引速度远快于 dict:
- list 存储条目只需一次数组索引
- dict 需要计算哈希、可能多次比较,还要内部数组索引
但 list 的索引必须是整数,不能像 dict 那样用字符串,所以我们要把字符转成数字。幸运的是,每个字符都能用 ord() 查到数值:- ord('a'), ord('z'), ord('A'), ord('Z')
- # (97, 122, 65, 90)
复制代码 用 chr() 还能把数值转回字符:- chr(97), chr(122)
- # ('a', 'z')
复制代码 所以可以用 my_list[ord(character)] += 1 计数。但前提是我们得提前知道 list 的大小。如果处理任意字母字符,list 可能会很大:
[code]ideograph = '
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |