返回介绍

7.8 Cython 和 numpy

发布于 2024-01-25 21:44:08 字数 4319 浏览 0 评论 0 收藏 0

list对象(作为背景,请看第3章)的每一个解引用都有开销,因为它们所引用的对象可以存在于内存中任意处。而相反,array对象在RAM的连续块中存储原生类型,能够被快速寻址。

Python具有array模块,为基础原生类型(包括整数、浮点数、字符串和Unicode字串)提供了一维存储。Numpy的numpy.array模块允许多维存储和更多样的原生类型,包括复数。

当以可预测的方式去迭代访问一个array对象时,如果要移动到序列中的下一个原生项,编译器会被指导去直接访问该项的内存地址,而不是让Python来计算出一个合适的地址。既然数据布局到了连续块中,在C中用偏移量来计算下一项的地址就是轻而易举的,而不要让CPython去计算来得到相同的结果,因为这会涉及慢速的虚拟机回调。

你应该注意到了如果运行接下来的numpy版本而不用任何Cython注解(例如,只是作为一个普通Python脚本运行),大概要71秒跑完——远超出普通的Python list脚本,它大概花费11秒。拖慢运行速度的原因是在numpy lists中解引用每一个元素的开销——它从来不是为这种用法而设计的,即使对初学者来说,这种用法看上去直观。通过编译代码,我们移除了这个开销。

Cython为此有两种特殊的语法形式。更老的Cython版本有一种特殊的访问numpy array的类型,但最近更一般化的缓存接口协议通过memoryview引入了进来——允许对实现了缓存接口的任意对象进行相同的低级访问,包括numpy arrays和Python arrays。

缓存接口的一个附加优势是内存块能够很容易地在其他C库中共享,而不需要把它们从Python对象转换成其他形式。

例7-11中的代码块看上去有一点像原来的实现,除了我们增加的memoryview注解外。函数的第2个参数是double complex[:] zs,意味着我们有一个使用缓存协议(用[]声明)的双精度复数对象,包含了一个一维的数据块(由一个冒号:声明)。

例7-11 Julia计算函数的注解numpy版本

# cython_np.pyx
import numpy as np
cimport numpy as np

def calculate_z(int maxiter, double complex[:] zs, double complex[:] cs):
  """Calculate output list using Julia update rule"""
  cdef unsigned int i, n
  cdef double complex z, c
  cdef int[:] output = np.empty(len(zs), dtype=np.int32)
  for i in range(len(zs)):
    n = 0
    z = zs[i]
    c = cs[i]
    while n < maxiter and (z.real * z.real + z.imag * z.imag) < 4:
      z = z * z + c
      n += 1
    output[i] = n
  return output

除了使用缓存注解语法声明输入参数外,我们也注解了输出变量,由empty给它分配了一个一维的numpy array。调用empty会分配一块内存,但是不会用完整的数值初始化内存,所以它不包含任何东西。我们在内循环中会覆写这个array的内容,所以我们不需要重复给它赋默认值。这要比分配并给array的内容赋默认值要稍快一点。

我们也使用更快、更显式的数学版本来展开abs的调用。这个版本用了0.23秒跑完——结果比原来在例7-7中的纯Python的Julia例子的Cython化版本要稍微快一点。纯Python版本有每次解引用一个Python复数对象的开销,但是这些解引用发生于外循环中,所以不占用多少执行时间。在外循环之后,我们做了这些变量的本地版本,它们以“C的速度”运行。这个numpy的例子和之前的纯Python例子中的内循环都对相同的数据做了相同的处理,所以执行时间的差异归因于外循环中的解引用和输出队列的创建。

在一台机器上使用OpenMP来做并行解决方案

作为这版代码演进的最后步骤,让我们看一下使用OpenMP C++扩展来并行化处理让我们为难的并行问题。如果你的问题适合这个模式,那么你就能很快发挥你的计算机多核的优势。

OpenMP(Open Multi-Processing)是一个定义良好的跨平台API,支持并行执行,以及与C、C++和Fortran的内存共享。它被构建入了大多数的现代C编译器,如果C代码编写合适的话,并行化就会在编译器级别上发生,所以它就给开发者使用Cython带来了相对小的工作量。

与Cython一起,OpenMP能够通过使用prange(并行range)操作符和给setup.py增加-fopenmp编译指令的方式加入进来。在一个prange循环中的工作就能够做到并行运行,因为我们禁止了全局解释器锁(GIL)。

一个修改过的支持prange的代码版本显示在例7-12中。用nogil:来声明禁止GIL的代码块,在这个代码块内部,我们使用prange为循环开启OpenMP并行模式来独立计算每一个i。

 警告 

当禁止GIL时,我们一定不能在常规Python对象(例如,lists)上操作,必须要在原生对象和支持memoryview接口的对象上去操作。如果并行操作了常规的Python对象,我们不得不去解决随之而来的内存管理问题,而这是GIL意图避免的。Cython不阻止我们去操控Python对象,但是如果你这样做,只会招来痛苦和困扰。

例7-12 增加prange来启用OpenMP并行化

# cython_np.pyx
from cython.parallel import prange
import numpy as np
cimport numpy as np

def calculate_z(int maxiter, double complex[:] zs, double complex[:] cs):
  """Calculate output list using Julia update rule"""
  cdef unsigned int i, length
  cdef double complex z, c
  cdef int[:] output = np.empty(len(zs), dtype=np.int32)
  length = len(zs)
  with nogil:
    for i in prange(length, schedule="guided"):
      z = zs[i]
      c = cs[i]
      output[i] = 0
      while output[i] < maxiter and (z.real * z.real + z.imag * z.imag) < 4:
        z = z * z + c
        output[i] += 1
  return output
 

为了编译cython_np.pyx,我们不得不修改setup.py脚本,就如在例7-13中显示的那样。我们让它通知C编译器在编译期间使用-fopenmp作为参数来启用OpenMP以及和OpenMP库去链接。

例7-13 为Cython在setup.py中增加OpenMP编译器和和链接器标志

#setup.py
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

setup(
    cmdclass = {'build_ext': build_ext},
    ext_modules = [Extension("calculate",
                 ["cython_np.pyx"],
                  extra_compile_args=['-fopenmp'],
                  extra_link_args=['-fopenmp'])]
    )

使用Cython的prange,我们能够选择不同的调度方式。使用static,工作负载可以均匀地在可用的CPU之间分布。我们的一些计算区域在时间上是开销很大的,另一些则开销很小。如果我们用static让Cython在CPU之间平等地调度工作块,那么一些区域要比另外一些完成得快,那些线程就会处于空闲状态。

D``ynamic和guided调度选项都企图缓解这个问题,可以通过在运行时动态地把工作分配给更小的块,这样当工作负载的运算时间可变时,CPU会更均匀地得到分布。对你的代码来说,正确的选择将会是根据你的工作负载的本质而做改变。

通过引入OpenMP和使用schedule=”guided’,我们把执行时间降低到了接近0.07秒——guided调度会动态地分配工作,所以更少的线程在等待新的工作。

我们也可能为这个例子使用#cython:boundscheck=False来禁止边界检查,但是这不会改进我们的运行时间。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文