Python 与 NumPy 库相结合为用户提供了编写高度复杂的函数和分析的工具。 随着代码的大小和复杂性的增长,代码库中的低效率问题开始蔓延。一旦项目进入完成阶段,开发人员就应开始关注代码的性能并分析瓶颈。 Python 提供了许多工具和库来创建优化且性能更快的代码。
在本章中,我们将研究一种名为 Cython 的工具。 Cython 是 Python 和“Cython”语言的静态编译器,在从事科学库/数值计算的开发人员中特别流行。 许多用 Python 编写的著名分析库都大量使用 Cython(Pandas,SciPy,scikit-learn 等)。
Cython 编程语言是 Python 的超集,用户仍然喜欢 Python 所提供的所有功能和更高层次的结构。 在本章中,我们将研究 Cython 起作用的许多原因,并且您将学习如何将 Python 代码转换为 Cython。 但是,本章不是 Cython 的完整指南。
在本章中,我们将介绍以下主题:
- 在我们的计算机上安装 Cython
- 将少量 Python 代码重写为 Cython 版本并进行分析
- 学习在 Cython 中使用 NumPy
每个开发人员在优化其代码时应注意的问题如下:
- 您的代码执行多少个函数调用?
- 有多余的调用吗?
- 该代码使用了多少内存?
- 是否存在内存泄漏?
- 瓶颈在哪里?
前四个问题主要由分析器工具回答。 建议您至少学习一种分析工具。 分析工具将不在本章中介绍。 在大多数情况下,建议先尝试优化函数调用和内存使用,然后再使用低级方法,例如 Cython 或汇编语言(使用 C 衍生语言)。
一旦确定了瓶颈,并且解决了算法和逻辑的所有问题,Python 开发人员便可以进入 Cython 的世界,以提高应用的速度。
Cython 是一个将类型定义的 Python 代码转换为 C 代码的编译器,该代码仍在 Python 环境中运行。 最终输出是本机代码,其运行速度比 Python 生成的字节码快得多。 在大量使用循环的代码中,Python 代码加速的幅度更加明显。 为了编译 C 代码,首要条件是在计算机上安装 C/C++ 编译器,例如gcc
(Linux)或mingw
(Windows)。
第二步是安装 Cython。 Cython 与其他带有 Python 模块的库一样,可以使用任何首选的方法(PIP,EasyInstall 等)进行安装。 完成这两个步骤后,您可以通过尝试从 Shell 调用 Cython 来测试设置。 如果收到错误消息,则说明您错过了第二步,需要重新安装 Cython 或从 Cython 官方网站下载 TAR 归档文件,然后从这次下载的root
文件夹中运行以下命令:
python setup.py install
正确完成所有操作后,您可以继续使用 Cython 编写第一个程序。
Cython 程序看起来与 Python 程序非常相似,但大多带有附加的类型信息。 让我们看一个简单的程序,该程序计算给定n
的第n
个斐波那契数:
defcompute_fibonacchi(n):
"""
Computes fibonacchi sequence
"""
a = 1
b = 1
intermediate = 0
for x in xrange(n):
intermediate = a
a = a + b
b = intermediate
return a
让我们研究一下该程序,以了解在调用带有某些数字输出的函数时幕后的情况。 假设compute_fibonacchi(3)
。
众所周知,Python 是一种解释性和动态语言,这意味着您无需在使用变量之前声明变量。 这意味着在函数调用开始时,Python 解释器无法确定n
将保留的值的类型。 当您使用某个整数值调用函数时,Python 会通过名为装箱和拆箱的过程自动为您进行类型推断。
在 Python 中,一切都是对象。 因此,当您输入1
或hello
时,Python 解释器将在内部将其转换为对象。 在许多在线材料中,此过程也称为拳击。 该过程可以可视化为:
那么当您将函数应用于对象时会发生什么呢? Python 解释器必须做一些额外的工作来推断类型并应用函数。 在一般意义上,下图说明了add
函数在 Python 中的应用。 Python 是一种解释型语言,它在优化函数调用方面做得并不出色,但是可以使用 C 或 Cython 很好地优化它们:
这种装箱和拆箱不是免费的,需要花费宝贵的计算时间。 当这样的操作被循环执行多次时,效果变得更加显着。
在n = 20
上运行时,以下程序在 IPython 笔记本上每个循环大约需要 1.8 微秒:
现在让我们将该程序重写为 Cython:
defcompute_fibonacchi_cython(int n):
cdefint a, b, intermediate, x
a, b= 1, 1
intermediate, x = 0, 0
for x in xrange(n):
intermediate = a
a = a+b
b = intermediate
return a
该程序每个循环花费64.5
纳秒:
尽管在此示例代码中提高速度非常重要,但这不是您将遇到的实际代码,因此您应始终记住首先在代码上运行分析器并确定需要优化的部分。 同样,在使用 Cython 时,开发人员应考虑在使用静态类型和灵活性之间进行权衡。 使用类型会降低灵活性,有时甚至会降低可读性。
通过删除xrange
并改用for
循环,可以进一步改进此代码。 当您对模块的所有组件/功能都满意并且没有错误后,用户可以将这些函数/过程存储在扩展名为.pyx
的文件中。 这是 Cython 使用的扩展名。 将此代码与您的应用集成的下一步是在安装文件中添加信息。
在这里,出于说明目的,我们将代码存储在名为fib.pyx
的文件中,并创建了一个构建该模块的安装文件:
from distutils.core import setup, Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
setup(
ext_modules=[Extension('first', ['first.pyx'])],
cmdclass={'build_ext': build_ext}
)
在这里,请注意扩展名first
的名称与模块的名称完全匹配。 如果您无法保持相同的名称,则将收到一个神秘的错误:
您的应用可能会使用多线程代码。 由于全局解释器锁(GIL),Python 不适合多线程代码。 好消息是,在 Cython 中,您可以显式解锁 GIL,并使您的代码真正成为多线程。 只需在您的代码中放置一个with nogil:
语句即可。 您以后可以在代码中使用with gil
获取 GIL:
with nogil:
<The code block here>
function_name(args) with gil:
<function body>
Cython 具有内置支持,可提供对 NumPy 数组的更快访问。 这些功能使 Cython 成为优化 NumPy 代码的理想人选。 在本节中,我们将研究用于计算欧式期权价格的代码,欧式期权是一种使用蒙特卡洛技术的金融工具。 不期望有金融知识; 但是,我们假设您对蒙特卡洛模拟有基本的了解:
defprice_european(strike = 100, S0 = 100, time = 1.0,
rate = 0.5, mu = 0.2, steps = 50,
N = 10000, option = "call"):
dt = time / steps
rand = np.random.standard_normal((steps + 1, N))
S = np.zeros((steps+1, N));
S[0] = S0
for t in range(1,steps+1):
S[t] = S[t-1] * np.exp((rate-0.5 * mu ** 2) * dt
+ mu * np.sqrt(dt) * rand[t])
price_call = (np.exp(-rate * time)
* np.sum(np.maximum(S[-1] - strike, 0))/N)
price_put = (np.exp(-rate * time)
* np.sum(np.maximum(strike - S[-1], 0))/N)
returnprice_call if option.upper() == "CALL" else price_put
以下是前面示例的 Cython 化代码:
import numpy as np
def price_european_cython(double strike = 100,doubleS0 = 100,
double time = 1.0, double rate = 0.5,
double mu = 0.2, int steps = 50,
long N = 10000, char* option = "call"):
cdef double dt = time / steps
cdefnp.ndarray rand = np.random.standard_normal((steps + 1, N))
cdefnp.ndarray S = np.zeros([steps+1, N], dtype=np.float)
#cdefnp.ndarrayprice_call = np.zeroes([steps+1,N], dtype=np.float)
S[0] = S0
for t in xrange(1,steps+1):
S[t] = S[t-1] * np.exp((rate-0.5 * mu ** 2) * dt
+ mu * np.sqrt(dt) * rand[t])
price_call = (np.exp(-rate * time)
* np.sum(np.maximum(S[-1] - strike, 0))/N)
price_put = (np.exp(-rate * time)
* np.sum(np.maximum(strike - S[-1], 0))/N)
return price_call if option.upper() == "CALL" else price_put
与此相关的安装文件如下所示:
from distutils.core import setup, Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import numpy.distutils.misc_util
include_dirs = numpy.distutils.misc_util.get_numpy_include_dirs()
setup(
name="numpy_first",
version="0.1",
ext_modules=[Extension('dynamic_BS_MC',
['dynamic_BS_MC.pyx'],
include_dirs = include_dirs)],
cmdclass={'build_ext': build_ext}
)
虽然通过 Cython 代码获得的加速效果非常好,并且您可能会倾向于在 Cython 中编写大多数代码,但建议仅将性能至关重要的部分转换为 Cython。 NumPy 在优化对数组的访问和执行更快的计算方面做得非常出色。 该代码可以视为描述该代码的理想候选者。 前面的代码有很多“松散的结果”,可以当作练习来解决 Python 中的性能问题,并在采用 Cython 方式之前先最佳地使用 NumPy。 由于 Python 的动态特性,盲目地对 NumPy 代码进行 Cython 化的速度提升可能不如具有真正问题的最优编写代码那样快。
最后,我们介绍在 Cython 中开发模块时应遵循的以下内容:
- 用纯 Python 编写代码并进行测试。
- 运行分析器并确定要关注的关键区域。
- 创建一个新模块以保存 Cython 代码(
<module_name>.pyx
)。 - 将这些区域中的所有变量和循环索引转换为它们的 C 对应物。
- 使用以前的测试设置进行测试。
- 将扩展添加到安装文件中。
在本章中,我们了解了如何将 Python 代码隐蔽到 Cython 中。 我们还研究了一些涉及 NumPy 数组的示例 Python 代码。 我们简要介绍了 Python 语言中装箱和拆箱的概念以及它们如何影响代码性能。 我们还说明了如何显式解锁臭名昭著的 GIL。 为了进一步深入研究 Cython,我们建议《Cython 编程学习手册》,Philip Herron,Packt Publishing。 在下一章中,您将了解 NumPy C API 以及如何使用它。