2.4.4 写更快的数值代码

关于numpy的高级使用的讨论可以在高级numpy那章,或者由van der Walt等所写的文章NumPy数组: 一种高效数值计算结构。这里只是一些经常会遇到的让代码更快的小技巧。

  • 循环向量化

找到一些技巧来用numpy数组避免循环。对于这一点,掩蔽和索引通常很有用。

  • 广播

在数组合并前,在尽可能小的数组上使用广播。

  • 原地操作

In [30]:

  1. a = np.zeros(1e7)
  2. %timeit global a ; a = 0*a
  1. 10 loops, best of 3: 33.5 ms per loop
  1. /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ipykernel/__main__.py:1: DeprecationWarning: using a non-integer number instead of an integer will result in an error in the future
  2. if __name__ == '__main__':

In [31]:

  1. %timeit global a ; a *= 0
  1. 100 loops, best of 3: 8.98 ms per loop

注意: 我们需要在timeit中global a,以便正常工作,因为,向a赋值,会被认为是一个本地变量。

  • 对内存好一点:使用视图而不是副本

复制一个大数组和在上面进行简单的数值运算一样代价昂贵:

In [32]:

  1. a = np.zeros(1e7)
  1. /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ipykernel/__main__.py:1: DeprecationWarning: using a non-integer number instead of an integer will result in an error in the future
  2. if __name__ == '__main__':

In [33]:

  1. %timeit a.copy()
  1. 10 loops, best of 3: 28.2 ms per loop

In [34]:

  1. %timeit a + 1
  1. 10 loops, best of 3: 33.4 ms per loop
  • 注意缓存作用

分组后内存访问代价很低:用连续的方式访问一个大数组比随机访问快很多。这意味着在其他方式中小步幅会更快(见CPU缓存作用):

In [35]:

  1. c = np.zeros((1e4, 1e4), order='C')
  1. /Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages/ipykernel/__main__.py:1: DeprecationWarning: using a non-integer number instead of an integer will result in an error in the future
  2. if __name__ == '__main__':

In [36]:

  1. %timeit c.sum(axis=0)
  1. The slowest run took 5.66 times longer than the fastest. This could mean that an intermediate result is being cached
  2. 1 loops, best of 3: 80.9 ms per loop

In [37]:

  1. %timeit c.sum(axis=1)
  1. 10 loops, best of 3: 79.7 ms per loop

In [38]:

  1. c.strides

Out[38]:

  1. (80000, 8)

这就是为什么Fortran顺序或者C顺序会在操作上有很大的不同:

In [39]:

  1. a = np.random.rand(20, 2**18)

In [40]:

  1. b = np.random.rand(20, 2**18)

In [41]:

  1. %timeit np.dot(b, a.T)
  1. 10 loops, best of 3: 23.8 ms per loop

In [42]:

  1. c = np.ascontiguousarray(a.T)

In [43]:

  1. %timeit np.dot(b, c)
  1. 10 loops, best of 3: 22.2 ms per loop

注意,通过复制数据来绕过这个效果是不值得的:

In [44]:

  1. %timeit c = np.ascontiguousarray(a.T)
  1. 10 loops, best of 3: 42.2 ms per loop

使用numexpr可以帮助自动优化代码的这种效果。

  • 使用编译的代码

一旦你确定所有的高级优化都试过了,那么最后一招就是转移热点,即将最花费时间的几行或函数编译代码。要编译代码,优先选项是用使用Cython:它可以简单的将Python代码转化为编译代码,并且很好的使用numpy支持来以numpy数据产出高效代码,例如通过展开循环。

警告:对于以上的技巧,剖析并计时你的选择。不要基于理论思考来优化。

2.4.4.1 其他的链接

  • 如果你需要剖析内存使用,你要应该试试memory_profiler
  • 如果你需要剖析C扩展程序,你应该用yep从Python中试着使用一下gperftools
  • 如果你想要持续跟踪代码的效率,比如随着你不断向代码库提交,你应该试一下:vbench
  • 如果你需要一些交互的可视化为什么不试一下RunSnakeRun