更快的python -- EuroPython 2016讲座笔记

本文是对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 优化之道。

Don’t。

优化是需要成本的,需要花费程序员的成本。很可能优化还弄出bug来。或者可读性降低了。亦或速度提升了,内存却费了。所以,除非你有很好的理由优化,否则,不要优化。Don’t。

Don’t yet。

你必须满足

  • 程序完成
  • 有测试

然后你才能开始优化。

Profile

使用Profile,比如

  • cProfile
  • pstats
  • RunSnakeRun, SnakeViz

优化分层

优化分为6层:

  • 设计
  • 数据结构和算法
  • 源代码
  • Build
  • 编译
  • 运行时

当我们说到优化的时候,优化是有不同层次的。你可以在设计上优化,也可以在算法和数据结构上优化。这样的优化,可以最大限度地提升性能。当然,这也是最难的。

如果有的地方很慢,可以用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也有一些出入。

1 list 长度

优化前

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。然后他给出了这些函数列表:

更快的python -- EuroPython 2016讲座笔记_第1张图片

小知识

英文 中文 十进制
s 1
ms 毫秒 0.001
µs 微妙 0.000 001
ns 纳秒 0.000 000 001

2 Filter a List

版本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不同

更快的python -- EuroPython 2016讲座笔记_第2张图片

按照Sebastian的说法,List Comprehension应该是最快的。我平时也一直这样觉得,但是今天的实验为什么有不同的结果,我也很疑惑。

3 Permissions or Forgiveness 许可或原谅

方案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语句检查。

4 Membership 测试

方案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的实验结果倾向于,能排序尽量排序。

难道版本间的差距有这么大?

5 删除重复

方案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

6 List 排序

方案1:

%timeit sorted(MILLION_NUMBERS)

25 ms

方案2:

%timeit MILLION_NUMBERS.sort()

13 ms

快了一倍。Sebastian 快了6倍。

7 1000 Operations vs 1 Function

方案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%,和作者差不多。

8 检查是否是True

方案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

结论,最后一种最快。

9 Def vs Lambda

方案1:

def greet1(name):
    return f'Hello {name}'

138 ns

方案2:

greet2 = lambda name: f'Hello {
       name}'

132 ns

速度都一样。。。

10 list 和 []

方案1:

%timeit list()

76 ns

方案2:

%timeit []

27 ns

方案2快,是因为方案1是一个function,需要先resolve function,这里耽误了一点时间。

dict()和{}也使用这个规则。

11 Variable Assignment (得不偿失)

方案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

这里,虽然提高了性能,但是降低了可读性。得不偿失。

12 Variable Lookup (得不偿失)

方案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

你可能感兴趣的:(python)