性能建议

使用NumPy的代码的性能一般都很不错,因为数组运算一般都比纯Python循环快得多。下面大致列出了一些需要注意的事项:

·将Python循环和条件逻辑转换为数组运算和布尔数组运算。

·尽量使用广播。

·避免复制数据,尽量使用数组视图(即切片)。

·利用ufunc及其各种方法。

如果单用NumPy无论如何都达不到所需的性能指标,就可以考虑一下用C、Fortran或Cython(等下会稍微介绍一下)来编写代码。我自己在工作中经常会用到Cython(http://cython.org),因为它不用花费我太多精力就能得到C语言那样的性能。

连续内存的重要性

虽然这个话题有点超出本书的范围,但还是要提一下,因为在某些应用场景中,数组的内存布局可以对计算速度造成极大的影响。这是因为性能差别在一定程度上跟CPU的高速缓存(cache)体系有关。运算过程中访问连续内存块(例如,对以C顺序存储的数组的行求和)一般是最快的,因为内存子系统会将适当的内存块缓存到超高速的L1或L2CPU Cache中译注7。此外,NumPy的C语言基础代码(某些)对连续存储的情况进行了优化处理,这样就能避免一些跨越式的内存访问。

一个数组的内存布局是连续的,就是说元素是以它们在数组中出现的顺序(即Fortran型(列优先)或C型(行优先))存储在内存中的。默认情况下,NumPy数组是以C型连续的方式创建的。列优先的数组(比如C型连续数组的转置)也被称为Fortran型连续。通过ndarray的flags属性即可查看这些信息:

  1. In [227]: arr_c = np.ones((1000, 1000), order='C')
  2.  
  3. In [228]: arr_f = np.ones((1000, 1000), order='F')
  4.  
  5. In [229]: arr_c.flags In [230]: arr_f.flags
  6. Out[229]: Out[230]:
  7. C_CONTIGUOUS : True C_CONTIGUOUS : False
  8. F_CONTIGUOUS : False F_CONTIGUOUS : True
  9. OWNDATA : True OWNDATA : True
  10. WRITEABLE : True WRITEABLE : True
  11. ALIGNED : True ALIGNED : True
  12. UPDATEIFCOPY : False UPDATEIFCOPY : False
  13.  
  14. In [231]: arr_f.flags.f_contiguous
  15. Out[231]: True

在这个例子中,对两个数组的行进行求和计算,理论上说,arr_c会比arr_f快,因为arr_c的行在内存中是连续的。我们可以在IPython中用%timeit来确认一下:

  1. In [232]: %timeit arr_c.sum(1)
  2. 1000 loops, best of 3: 1.33 ms per loop
  3.  
  4. In [233]: %timeit arr_f.sum(1)
  5. 100 loops, best of 3: 8.75 ms per loop

如果想从NumPy中提升性能,这里就应该是下手的地方。如果数组的内存顺序不符合你的要求,使用copy并传入'C'或'F'即可解决该问题:

  1. In [234]: arr_f.copy('C').flags
  2. Out[234]:
  3. C_CONTIGUOUS : True
  4. F_CONTIGUOUS : False
  5. OWNDATA : True
  6. WRITEABLE : True
  7. ALIGNED : True
  8. UPDATEIFCOPY : False

注意,在构造数组的视图时,其结果不一定是连续的:

  1. In [235]: arr_c[:50].flags.contiguous In [236]: arr_c[:, :50].flags
  2. Out[235]: True Out[236]:
  3. C_CONTIGUOUS : False
  4. F_CONTIGUOUS : False
  5. OWNDATA : False
  6. WRITEABLE : True
  7. ALIGNED : True
  8. UPDATEIFCOPY : False

其他加速手段:Cython、f2py、C

近年来,Cython项目(http://cython.org)已经受到了许多Python程序员的认可,用它实现的代码运行速度很快(可能需要与C或C++库交互,但无需编写纯粹的C代码)。你可以将Cython看成是带有静态类型并能嵌入C函数的Python。下面这个简单的Cython函数用于对一个一维数组的所有元素求和:

  1. from numpy cimport ndarray, float64_t
  2.  
  3. def sum_elements(ndarray[float64_t] arr):
  4. cdef Py_ssize_t i, n = len(arr)
  5. cdef float64_t result = 0
  6.  
  7. for i in range(n):
  8. result += arr[i]
  9.  
  10. return result

Cython处理这段代码时,先将其翻译为C代码,然后编译这些C代码并创建一个Python扩展。Cython是一种诱人的高性能计算方式,因为编写Cython代码只比编写纯Python代码多花一点时间而已,而且还能跟NumPy紧密结合。一般的工作流程是:得到能在Python中运行的算法,然后再将其翻译为Cython(只需添加类型定义并完成一些其他必要的工作即可)。更多信息请参考该项目的文档。

其他有关NumPy的高性能代码编写手段还有f2py(FORTRAN 77和90的包装器生成器)以及利用纯C语言编写Python扩展。

译注7:这里主要考虑的是预读机制以及缓存块失效率。由于这个存储层次是纯硬件实现的,谁的程序都控制不了,所以数据最好连续存储。