最近读了《Python性能分析与优化》,前面大部分章节都是介绍性能分析工具以及一些比较简单的优化方式(重复运算查表之类的常识),可能是我第一次阅读,挖掘的干活不算很多,不过还想来总结分享一下。
首先介绍一些关于Python特性的优化利用。
一、循环、列表综合表达式、生成器表达式
书中有列举循环和列表的运行时间对比,由于我主要用的是Python3,所以我改成了Python3的代码:
#coding:utf-8
'''
@DateTime: 2017-12-18 09:56:15
@Version: 1.0
@Author: Unname_Bao
'''
import dis
import inspect
import timeit
programs = dict(
loop = '''
multiples_of_two = []
for x in range(100):
if x%2 == 0:
multiples_of_two.append(x)
''', #loop是循环的代码
comprehension = 'multiples_of_two = [x for x in range(100) if x % 2 == 0]', #comprehension是列表综合生成式代码
)
for name , text in programs.items():
print(name,timeit.Timer(stmt = text).timeit()) #打印运行时间
code = compile(text,'','exec') #编译
dis.disassemble(code) #输出生成的机器码
loop 10.350648703367305
2 0 BUILD_LIST 0
2 STORE_NAME 0 (multiples_of_two)
3 4 SETUP_LOOP 38 (to 44)
6 LOAD_NAME 1 (range)
8 LOAD_CONST 0 (100)
10 CALL_FUNCTION 1
12 GET_ITER
>> 14 FOR_ITER 26 (to 42)
16 STORE_NAME 2 (x)
4 18 LOAD_NAME 2 (x)
20 LOAD_CONST 1 (2)
22 BINARY_MODULO
24 LOAD_CONST 2 (0)
26 COMPARE_OP 2 (==)
28 POP_JUMP_IF_FALSE 14
5 30 LOAD_NAME 0 (multiples_of_two)
32 LOAD_ATTR 3 (append)
34 LOAD_NAME 2 (x)
36 CALL_FUNCTION 1
38 POP_TOP
40 JUMP_ABSOLUTE 14
>> 42 POP_BLOCK
>> 44 LOAD_CONST 3 (None)
46 RETURN_VALUE
comprehension 8.135235990133049
1 0 LOAD_CONST 0 ( at 0x00000258440AA5D0, file "", line 1>)
2 LOAD_CONST 1 ('')
4 MAKE_FUNCTION 0
6 LOAD_NAME 0 (range)
8 LOAD_CONST 2 (100)
10 CALL_FUNCTION 1
12 GET_ITER
14 CALL_FUNCTION 1
16 STORE_NAME 1 (multiples_of_two)
18 LOAD_CONST 3 (None)
20 RETURN_VALUE
***Repl Closed***
关于生成器表达式,使用方法和列表表达式是类似的,就是把中括号换成小括号就可以了,例如:
my_list = (i**2 for i in range(100))
但生成器有个缺点是不能随机接入,即只可以遍历使用:
>>> my_list[1] #这样使用会报错
Traceback (most recent call last):
File "", line 1, in
TypeError: 'generator' object has no attribute '__getitem__'
>>> for i in my_list:
... print(i)
...
0
1
...
生成器和列表表达式在不同的数据量中表现不同,数据量越小使用列表表达式创建列表越快,数据量越大使用生成器创建列表越快。
二、Ctypes
这个特性只存在于CPython中,ctypes可以使开发者借助C直接进行底层开发,实现C语言的功能,也可以通过这个库调用共享链接库(so、dll),并且可以借此绕过GIL(总所周知Python是伪多线程,而GIL是Python在设计中限制Python多线程的机制,就是说因为GIL的存在Python无法实现真正的多线程,但是在调用机器码时可以绕过该机制。)
下面是一个简单的生成随机数对比代码:
#coding:utf-8
'''
@DateTime: 2018-01-14 14:06:34
@Version: 1.0
@Author: Unname_Bao
'''
import time
import random
from ctypes import cdll
libc = cdll.msvcrt
#libc = cdll.LoadLibrary('libc.so.6') #Linux系统
init = time.time()
randoms = [random.randint(1,100) for i in range(1000000)]
print('Pure python: %s seconds'%(time.time() - init))
init = time.time()
randoms = [(libc.rand()%100+1) for i in range(1000000)]
print('C version: %s seconds'%(time.time() - init))
输出结果:
Pure python: 1.5020687580108643 seconds
C version: 0.5097446441650391 seconds
***Repl Closed***
三、字符串连接
这个涉及到我之前写的一篇文章,当时其实写错了,但我不修改了,大家看到知道就行了:关于python3中整数数组转bytes的效率问题
这个大家可以就当成例子看,不过原理写错了,实际上真正的原因是Python的字符串特性。
字符串在Python在内存中是静态值,也就是说Python的字符串变量只不过是指向了内存中的静态值,这一点跟Java类似,看下面的输出结果大家就懂了:
>>> str1 = 'Unname_Bao'
>>> str2 = 'Unname_Bao'
>>> id(str1)
2020390278064
>>> id(str2)
2020390278064
>>> str1 = str1+str2
>>> id(str2)
2020390278064
>>> id(str1)
2020390263736
str1和str2的值相同,他们内存中指向的静态值就相同,而一旦对str1进行修改,就会申请新的内存地址,然后让str1和str2连接的运算结果存在新申请的内存地址中,然后让str1指向新申请的内存地址。也就是说任何字符串修改都会让python重新申请内存地址,所以在我之前的文章中才会遇到跑十几分钟跑不出来的问题,但是改成列表运算就可以几秒钟内跑出来了。
四、多线程与多进程
之前也提到了,由于GIL的存在,python的多线程实际上是伪多线程,但是可以通过调用dll绕过GIL,纯python代码的多线程仅适用于IO密集型操作中,否则反而会使效率降低,这些都是老生常谈的话题了。
python的多进程是真的多进程,不过我对多进程的使用的还比较少,这篇文章主要是想帮大家了解一下提高代码效率的方法,所以想要了解并使用多进程的话,可以百度一下。
五、JIT
不知道JIT(just in time)的可以了解一下,Python是一个解释型语言,就是边运行边翻译,JIT技术是指在第一次运行的时候进行编译,例如第一次运行sum的时候,先进行编译再运行,下次再调用sum的时候,由于是直接调用的机器码,就可以减少很多时间。Java就是通过JIT技术成为解释型语言中的性能怪兽的。
当然这也带来一个问题就是第一次调用函数的时候反而会降低效率,接下来给大家举个例子:
#coding:utf-8
'''
@DateTime: 2018-01-14 12:50:35
@Version: 1.0
@Author: Unname_Bao
'''
from numba import jit
import random
import time
# import numpy
@jit
def sum1(a): #sum1使用jit技术
s = 0
for i in a:
s = s +i
return s
def sum2(a): #sum2没有使用jit技术
s = 0
for i in a:
s = s + i
return s
a = [random.randint(0,1000) for i in range(1000000)]
init = time.time()
print(sum1(a))
print(time.time()-init)
init = time.time()
print(sum2(a))
print(time.time()-init)
init = time.time()
print(sum1(a))
print(time.time()-init)
numba是提高Python的一个第三方库之一,是解决Python效率的方案之一,提供JIT、GIT绕过和调用GPU,但它JIT的使用适用范围有限,可能会不支持第三方库的数据类型。大家了解一下便好,接下来是运行结果:
500335531
0.18614816665649414
500335531
0.04512643814086914
500335531
0.0170440673828125
***Repl Closed***
可见第一次调用sum1比第二次调用sum1慢的多,但一旦编译好,sum1的速度就比sum2要快了。
以上算是我阅读完这本书后的小结了,第一次阅读感觉干货不算很多,勉强可以总结成一篇文章,大部分优化方法都了解过或者算是常识了,不过也让我了解到了一些我对Python的误解,也算是有收获吧。