本文是对EuroPython 2016年Sebastian Witowski的演讲《Writing Faster Python》的笔记。
首先,Sebastian 指出
PYTHON WAS NOT MADE TO BE FAST… …BUT TO MAKE DEVELOPERS FAST.
Python不是为了快而创造的,Python是为了程序员快而创造的。
然后他比较了Java和Python的Hello World
Java:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
Python:
print("Hello, world!")
如果你用bing搜索Optimization,你会在前几项找到 Rules Of Optimization 优化之道。
优化是需要成本的,需要花费程序员的成本。很可能优化还弄出bug来。或者可读性降低了。亦或速度提升了,内存却费了。所以,除非你有很好的理由优化,否则,不要优化。Don’t。
你必须满足
然后你才能开始优化。
使用Profile,比如
优化分为6层:
当我们说到优化的时候,优化是有不同层次的。你可以在设计上优化,也可以在算法和数据结构上优化。这样的优化,可以最大限度地提升性能。当然,这也是最难的。
如果有的地方很慢,可以用C/C++重写。
Sebastian举的算法的例子:如果要从1加到n:
优化前:
sum = 0
for x in range(1, n+1):
sum += x
print sum
优化后:
print n * (1+n) / 2
以下示例以我自己的测试为准,当然,代码还是使用Sebastian的。
代码地址:
https://github.com/EricWebsmith/articles/blob/main/writing_faster_python.ipynb
大陆地区请访问:
https://nbviewer.jupyter.org/github/EricWebsmith/articles/blob/main/writing_faster_python.ipynb
我使用的版本是 3.8.8,有些实验结果和作者使用的3.5也有一些出入。
优化前
def count():
how_many = 0
for element in ONE_MILLION_ELEMENTS:
how_many += 1
return how_many
39.5 ms
优化后
len(ONE_MILLION_ELEMENTS)
68.4 ns
速度提高577485倍!!!
这里的重点是使用python的build-in function。然后他给出了这些函数列表:
英文 | 中文 | 十进制 |
---|---|---|
s | 妙 | 1 |
ms | 毫秒 | 0.001 |
µs | 微妙 | 0.000 001 |
ns | 纳秒 | 0.000 000 001 |
版本1:
def filter1():
output = []
for element in MILLION_NUMBERS:
if element % 2:
output.append(element)
return output
265 ms
版本2:
list(filter(lambda x: x % 2, MILLION_NUMBERS))
331 ms
版本3:
[x for x in MILLION_NUMBERS if x % 2]
299 ms
这里的实验和Sebastian不同
按照Sebastian的说法,List Comprehension应该是最快的。我平时也一直这样觉得,但是今天的实验为什么有不同的结果,我也很疑惑。
方案1:
if hasattr(foo, 'hello'):
foo.hello
131 ns
方案2:
try:
foo.hello
except AttributeError:
pass
104 ns
这里快了1/3,和作者说的快了3倍差了不少。
第一个方案叫做Permission 许可,因为他先检查属性是否存在,如果存在,才调用属性。
第二个方案叫做Forgiveness 原谅,因为他不管属性是否存在,先调用了再说。
实验表明Forgiveness 比 Permission 快。
这是正常情况,但是如果发生了异常,反而会变慢。见方案3和4.
方案3:
if hasattr(foo, 'world'):
foo.world
119 ns
方案4:
def example3_4():
try:
foo.world
except AttributeError:
pass
448 ns
原因是异常处理是比较耗费资源的。
结论,如果你的程序大多数情况都正常,可以用try…except。如果大多数情况都找不到属性,可以用if语句检查。
方案1
def check_number(number):
for item in MILLION_NUMBERS:
if item == number:
return True
return False
90 ms
方案2:
500000 in MILLION_NUMBERS
66.3 ms
我们看到,使用python的built in的in,比写for循环快多了。
当然,这里的速度还和成员在list中的出现位置有关。
方案3:
%timeit 100 in MILLION_NUMBERS
13.6 µs
方案4:
999999 in MILLION_NUMBERS
130 ms
我们可以把list转成set,这样就有快多了。我觉得应该是log2吧。
方案5:
MILLION_SET = set(MILLION_NUMBERS)
%timeit 100 in MILLION_SET
195 ns
方案6:
%timeit 999999 in MILLION_SET
188 ns
方案6比方案4快了两个数量级 1000,000倍。
当然,把list转成set也是要花费时间的。
方案7:
%timeit MILLION_SET = set(MILLION_NUMBERS)
47 ms
我这里转换一下还是挺快的。当然,我这个MILLION_NUMBERS本来就是排序好的。
如果是逆序的呢?
方案8:
MILLION_NUMBERS_REVERSE = MILLION_NUMBERS[::-1]
%timeit MILLION_SET_REVERSE = set(MILLION_NUMBERS_REVERSE)
44.2 ms
看来 python 3.8.8的排序消耗并不大。这和Sebastian的试验结果大相径庭。他的实验结果,排序的消耗,是一次查找的好几倍。所以,他认为如果只是少量的查找,不必排序。而3.8.8的实验结果倾向于,能排序尽量排序。
难道版本间的差距有这么大?
方案1:
def exmple5_1():
unique = []
for e in MILLION_NUMBERS_WITH_DUP:
if e not in unique:
unique.append(e)
return unique
1分钟过去了,还在跑。。。我取消了。
方案2:
set(MILLION_NUMBERS_WITH_DUP)
51 ms
方案1:
%timeit sorted(MILLION_NUMBERS)
25 ms
方案2:
%timeit MILLION_NUMBERS.sort()
13 ms
快了一倍。Sebastian 快了6倍。
方案1:
def square(number):
return number ** 2
%timeit [square(i) for i in range(1000)]
253 µs
方案2:
%timeit [i**2 for i in range(1000)]
211 µs
快20%,和作者差不多。
方案1:
variable = True
%timeit if variable == True: pass
37 ns
方案2:
%timeit if variable is True: pass
37 ns
方案3:
%timeit if variable: pass
24 ns
结论,最后一种最快。
方案1:
def greet1(name):
return f'Hello {name}'
138 ns
方案2:
greet2 = lambda name: f'Hello {
name}'
132 ns
速度都一样。。。
方案1:
%timeit list()
76 ns
方案2:
%timeit []
27 ns
方案2快,是因为方案1是一个function,需要先resolve function,这里耽误了一点时间。
dict()和{}也使用这个规则。
方案1:
a=1
b=2
c=3
d=4
e=5
f=6
g=7
114 ns
方案2:
a,b,c,d,e,f,g = 1,2,3,4,5,6,7
60 ns
这里,虽然提高了性能,但是降低了可读性。得不偿失。
方案1:
def example12_1():
output = []
for e in MILLION_NUMBERS:
output.append(e)
return output
68 ms
方案2:
def example12_2():
output = []
append = output.append
for e in MILLION_NUMBERS:
append(e)
return output
56 ms
这里把append注册为了本地函数,所以程序快了一点点。但是这样也降低了程序的可读性。得不偿失。
优化有不同目的和层级。代码优化积少成多,容易实现。要善于使用profile。
[1] Writing Faster Python, Sebastian Witonski, EuroPython 2016
[2] Rules Of Optimization