Python高性能编程 读书笔记

主题

在当python的函数越短,通常意味着性能越高!

这不是笑话,在不改变算法的情况下,同一个功能是一定存在一个最短指令集的,当python代码变短意味着指令码变少,那多的指令码就在底层以CPU指令码的形式转移了。

这叫做同算法指令集数量守恒。 ——本人

第二章 通过性能分析找到瓶颈

python可用的分析工具:

  • cProfile:自带的运行分析工具,可以计算函数的执行次数和时间;
  • runsnakerun: 可视化cProfile的内容;
  • link_profile: 分析的更加细致,但是执行效率会变慢,要增加修饰符;
  • memory_profiler:诊断内存的用量
  • heapy:调查堆上的对象
  • dis模块:生成CPython字节码

heapy模块

  • guppy.hpy() – 获得heapy对象,作为操作接口。
  • heap() – 获得堆中的可见对象(reachable and visible),这些对象并不包含guppy库使用和创建的对象。
  • setref() – 设置一个参考点,下一次调用heap()函数会计算这个参考点之后创建的。
  • heapu() – 获得不可见(循环引用)的对象的状态和列表,heapu()返回一个状态(guppy.heapy.Part.Stat)标明不可见对象。

dis模块

获得python的字节码

def myfunc(alist):
    return len(alist)

>>> dis.dis(myfunc)
  2           0 LOAD_GLOBAL              0 (len)
              3 LOAD_FAST                0 (alist)
              6 CALL_FUNCTION            1
              9 RETURN_VALUE

>>> import math
>>> def myfunc(x):
...     return math.sin(x)
...
>>> import dis
>>> dis.dis(myfunc)
  2           0 LOAD_GLOBAL              0 (math)
              2 LOAD_ATTR                1 (sin)
              4 LOAD_FAST                0 (x)
              6 CALL_FUNCTION            1
              8 RETURN_VALUE
>>> from math import sin
>>> def myfunc(x):
...     return sin(x)
...
>>> dis.dis(myfunc)
  2           0 LOAD_GLOBAL              0 (sin)
              2 LOAD_FAST                0 (x)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

默认值不能用可变类型

>>> def B():
...   def A(x=[]):
...     x.append(2)
...     return x
...   return A
...
>>> import dis
>>> dis.dis(B)
  2           0 BUILD_LIST               0
              2 BUILD_TUPLE              1
              4 LOAD_CONST               1 (<code object A at 0x7febe7e5de40, file "<stdin>", line 2>)
              6 LOAD_CONST               2 ('B.<locals>.A')
              8 MAKE_FUNCTION            1
             10 STORE_FAST               0 (A)

  5          12 LOAD_FAST                0 (A)
             14 RETURN_VALUE


构建了一个函数,是B.<locals>.A,并且list作为默认值一开始就存进去了
>>> def X(x=[]):
...   return x
...
>>> X.__defaults__
([],)

第三章 列表和元祖

python使用魔法函数eqlt来进行对象的比较。

bisect模块是二分查找模块

通用代码会比某个特定问题设计的代码慢很多。

blist是比默认的list在存在大量修改的list场景下实现更快的数据结构。

第四章 字典和集合

python在va == vb的判断内部调用的是eq方法。

可以被散列(哈希)的对象是同时实现了hash的魔法函数以及eq或者cmp两者之一的类型。

这里一定要实现eq或者cmp为了防止两个对象的hash相同,那样就需要判断两个对象是否相等,如果相等则是同一个,不然则是不同的。

python对dict本质就是一个hash表,底层对hash冲突的处理方式是开放寻址法

同时在python的dict到达容量的2/3时会发生扩容,扩容的复杂度是O(n)的,需要将所有的对象都重新hash一次。

每当python访问一个变量、函数或模块时,首先会查找locals()数组,其内保存了所有本地变量的条目,如果不在本地变量里那么搜索globals()字典,最后如果对象也不在则搜索builtin对象。这里可能为了搜一个函数或者变量要搜好几个dict。

第五章 迭代器和生成器

代码每次运行到yield该函数就会“发射”出一个值,然后外部代码需求另一个值时该函数才会继续运行(上下文会保存)。

使用python的内置函数iter来创建迭代器,iter函数会返回对象的iter属性:

# python的循环
for i in object:
    do_work(i)

# 等价于
object_iterator = iter(object)
while 1:
    try:
        i = object_iterator.next()
        do_work(i)
    except StopIteration:
        break

如果只是计算满足某个条件的列表数量,这样可以节省一笔内存

# 不要写:
[i for i in xrange(n) if i % 3 == 0]
# 要用:
sum((1 for i in xrange(n) if i % 3 == 0))

----------------------
项目里一些代码的鞭尸:
level_aver = float(sum([member.rank_level for member in self.member_dict.values()])) / self.size

total_num = sum([abs(value) for value in pop_result.itervalues()])

sum([mgr.busy_count for mgr in self.combat_mgr.itervalues()])

# 这里每次+可能会增加一次内存分配和复制
# 用list(chain()) 在1000*1000的对象上操作要快396倍
task_list = reduce(lambda x, y: x + y, task_level_list)

# 而且原场景其实不需要这个list,拼接了之后只是一次性使用直接返回迭代器即可

python里一些表达式:

  • 列表表达式: [f(xx) for xx in rr if xx else f(xx)]
  • 生成器表达式: (f(xx) for xx in rr if xx else f(xx))
  • 字典表达式: { key_expr: value_expr for value in collection if condition }
  • 集合生成器: {f(xx) for xx in rr if xx else f(xx)}

在这里记录一下。

标准库中的itertools库提供了内建函数map, reduce, filter和zip的生成器版本(分别是imap, ireduce, ifilter和izip),以及其他很多有用的函数,特别是:

  • islice 允许对一个无穷生成器进行切片
  • chain 讲多个生成器链接到一起
  • takewhile 给生成器添加一个终止条件
  • cycle 通过不断重复将一个有限生成器变成无穷

顺便记录一下上面几个函数的功能:

  • map(function, iterable, …): 将参数以函数执行的生成器
  • reduce(function, iterable[, initializer]): 跟map类似,不过f的参数是两个,先对迭代器中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
  • filter(function, iterable): 对迭代器的对象执行f来进行过滤,为True才会返回
  • zip([iterable, …]): 将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
  • any(iterable), 如果 iterable 的任一元素为真值则返回 True。
  • all(iterable), 如果 iterable 的所有元素都为真值则返回 True。

第六章 矩阵和矢量计算

原生python并不支持矢量操作,主要的核心原因是python的列结构存储的内容是指向对象的指针,其次python的字节码没有对矢量操作做优化。

result = 0
for i xrange(num_iterations):
   result += i * sin(num_iterations)
return result

# 下面这个快过上面那个,虽然是废话,但是很实用
result = 0
for i xrange(num_iterations):
   result += i
return result * sin(num_iterations)

# -------------------------------------
# damage_system
for ratio in reduce_ratio_list:
    damage *= ratio

# 比上面要快16%
damage = reduce(operator.mul, reduce_ratio_list, damage)

# -------------------------------------

书中用numpy去优化一段代码,带来了40倍的效率提升,但是神奇的事情是,禁用矢量指令集优化,性能依然有极大的提高,用perf分析发现矢量操作近带来了40倍中的15%,剩下的是内存问题导致的效率低下。

perf是一个进程级别的分析工具。

numexpr

将python能够分析执行顺序,非常大程度的优化矩阵,可以自动地利用多CPU的优势,并且考虑到CPU缓存的问题,会特意移动数据让各级CPU缓存拥有正确的数据。

如果numpy可以将速度快几倍,那使用numexpr可能可以将速度提升至几十上百倍。

但是如果矩阵特别小,会有负优化的效果。

第七章 编译成C

Cython、Shed Skin和Pythran是纯粹的基于C的编译方式,凭借Numba的基于LLVM的编译方式,还有替代虚拟机的pypy,内置了一个即时编译器(JIT)。

无论使用哪条路线,都要在适用性和团队效率上作出权衡,

image

优化是永无止尽的,但是对一个模块的优化是有限的。

顺便一提,pyc只是py编译成的python机器码,并不包含优化。

对python的优化

Cython

可以直接把python代码编译成so,可以先用Cython在服务端优化一轮,顺便提前把模块化做好。

Shed Skin

将python翻译成C++的编译器,会自动去猜测类型,可以直接被import,不像Cython需要显式指定类型。

Shed Skin会将python的list拷贝成Shed Skin环境的变量,这个是会花时间的,

numba

llvm编译器的python脚本编译,只需要在需要编译的函数增加@jit()修饰符即可,执行速度非常快

Pythran

有点像Numba和Cython——我注解函数的参数,接着它接管进一步的类型注解和代码特化,它会利用OpenMP的并行化可能性。

pypy

底层换了虚拟机,提供了JIT,执行时编译,所以第一次会很慢,编译好之后再执行非常快!

pypy和CPython的垃圾回收机制不同。

任何纯python的库pypy都能直接安装运行,但是有C扩展库的就可能无法工作。

内存消耗比cpython多。

写C/C++程序的优化

ctypes

C++只写一些函数,然后通过ctypes来”import”进python,需要在python层将所有的调用对象都转化成ctypes对应的C类型,然后执行。

这里在使用list或者dict的时候,会非常非常消耗。

cffi

相比ctypes方便很多,并且支持一些很便利的语法糖。

CPython模块

我们项目里目前是用的模块,会很繁琐,很简单的代码也要写几十行来兼容,但是好处是控制颗粒度非常高,从无视GIL到引用计数,一切都可以直接去修改,但是对程序的要求也会高一点,因为需要了解python的一些细节。

在大部分情况下,书中的建议是不使用这种方式。

第八章 并发

时间循环有两种方式:回调或者future。

回调会带来很长的调用链,future+asyncio是一个很好的,不过我们用python2是永远没机会用了。

分别使用了gevent,tomado和asyncio

第九章 multiprocessing模块

可以让程序基于进程和线程的并行处理,multiprocessing.Pool的功能感觉很强!

对于使用这个模块,需要解决的问题:

  • 把工作拆分成独立的工作单元
  • 考虑是否能随机化工作序列
  • 尽量让同时在处理的任务数量和CPU数量相同

第十章 集群和工作队列

一个容易调适的系统可能胜过一个更快的系统。

  • 启动组件:cron任务,Circus或supervisord
  • 测试容灾的组件:ChaosMonkey
  • 部署系统:Fabric、Salt、Chef或Puppet

下面几个都是集群化的方案

Parallel Python模块

  • 几乎没有依赖性,接口简单;
  • 不是很强大,缺少通信机制

Ipython Parallel

IPython作为shell做并行任务控制,依赖ZeroMQ。

NSQ

是一个可随时投入生产的队列系统,有持久性和扩展性。

其他集群化的工具

  • Celery
  • Gearman
  • PyRes
  • 亚马逊的简单队列服务(SQS)

第十一章 使用更少的RAM

基础对象开销高:100000000项的list要消耗760MB的内存,可以替换成array,array是连续的内存块,而且array可以直接传递给C++层不需要额外的转换。

python3对字符串的存储效率相比python2提高了很多。

image

高效地存储文本,trie树和有向无环图 (DAWG)(双trie树结构)单词图:

通过允许概率误差来减少数据集需要的内存空间:

  • Morris计数器
  • 布隆过滤器
  • loglog计数器

End.