您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息

使用Pyrex来扩展和加速Python程序的教程

2025/2/5 6:38:30发布20次查看
pyrex 是一种专门设计用来编写 python 扩展模块的语言。根据 pyrex web 站点的介绍,“它被设计用来在友好易用的高级 python 世界和凌乱的低级 c 世界之间搭建一个桥梁。”虽然几乎所有的 python 代码都可以作为有效的 pyrex 代码使用,但是您可以在 pyrex 代码中添加可选的静态类型声明,从而使得这些声明过的对象以 c 语言的速度运行。
加速 python
从某种意义上来说,pyrex 只是不断发展的 python 类语言系列的一个部分:jython、ironpython、prothon、boo、vyper(现在没人用了)、stackless python(以一种方式)或 parrot runtime(以另外一种方式)。按照语言的术语来说,pyrex 本质上是在 python 中添加了类型声明。它的另外几个变化没有这么重要(不过对 for 循环的扩展很漂亮)。
然而,您真正希望使用 pyrex 的原因是它编写的模块比纯 python 运行得更快,可能会快很多。
实际上,pyrex 会从 pyrex 代码生成一个 c 程序。中间文件 module.c 依然可以用于手工处理。然而对于“普通的” pyrex 用户来说,没有什么理由需要修改所生成的 c 模块。pyrex 本身可以让您访问那些对速度至关重要的 c 级代码,而节省了编写内存分配、回收、指针运算、函数原型等的工作。pyrex 还可以无缝地处理 python 级对象的所有接口;通常它都是通过在必要的地方将变量声明为 pyobject 结构并使用 python c-api 调用进行内存处理和类型转换而实现的。
对于大部分情况来说,pyrex 不需要不断对简单数据类型变量进行装箱(box) 和 拆箱(unbox) 操作,因此速度比 python 更快。例如,python 中的 int 类型是一个具有很多方法的对象。它有一个继承树,自己有一个计算好的“方法解析顺序(mothod resolution order,mro)”。它有分配和回收方法可以用于内存处理。它知道何时将自己转换为一个 long 类型,以及如何对其他类型的值进行数值运算。所有这些额外的功能都意味着在使用 int 对象进行处理时需要经过更多级的间接处理或条件检查。另外一方面,c 或 pyrex 的 int 变量只是内存中各个位设置为 1 或 0 的一个区域。使用 c/pyrex 的 int 类型进行处理不需要涉及 任何 间接操作或条件检查。一个 cpu “加”操作在硅芯片中就可以执行完了。
在仔细选择的情况中,pyrex 模块的速度可以比 python 版本的相同模块的运行速度快 40 到 50 倍。但是与使用 c 本身 编写的模块相比,pyrex 版本的模块几乎都不会比 python 版本的模块更长,代码更类似于 python,而不是 c。
当然,当您开始谈论加速(类)python 模块时,pyrex 并不是惟一可用的工具。在 python 开发者的选择中,也可以使用 psyco。psyco 可以保持代码非常简短;它是(x86)机器代码中的一个 jit python 代码编译器。与 pyrex 不同,psyco 并不会精确地限定变量的类型,而是根据数据 可能 是哪种类型的每种假设为每个 python 代码块创建几种可能的机器代码。如果在一个给定的代码段中数据是是简单类型,例如 int,那么这段代码(如果是一个循环,这种情况就更为突出)就可以很快地运行。例如,x 在一个执行一百万次的循环中可以是 int 类型,但是在循环结束时可以依然是一个 float 类型的值。psyco 可以使用与在 pyrex 中显式指定的类型相同的类型来加速循环。
虽然 pyrex 也并不难,但是 psyco 更加简单易用。使用 psyco 不过是在模块的末尾加上几行;实际上,如果加上正确的代码,那么即使在 psyco 不可用时,模块也可以同样运行(只是速度较慢)。
清单 1. 只有在 psyco 可用时才使用 psyco
# import psyco if availabletry: import psyco psyco.full()except importerror: pass
要使用 pyrex,需要对代码进行的修改会更多(但也不过是多一点而已),系统中还需要安装一个 c 编译器,并正确对生成 pyrex 模块的系统进行配置。虽然您 可以 分发二进制的 pyrex 模块,但是为了能使您的模块在其他地方也可以运行,python 的版本、架构和终端用户需要的优化选项必须匹配。
速度初体验
我最近为 developerworks 的文章 beat spam using hashcash 创建了一个纯 python 的 hashcash 实现,但是基本上来说,hashcash 是一种使用 sha-1 提供 cpu 工作的技术。python 有一个标准的模块 sha,这使得编写 hashcash 非常简单。
与我编写的 95% 的 python 程序不同,hashcash 模块缓慢的速度让我心烦,至少有那么一点点心烦。按照设计,这个协议就是要吃光所有的 cpu 周期,因此运行效率非常关键。hashcash.c 的 ansi c 二进制文件运行的速度是这个 hashcash.py 脚本的 10 倍。而且启用了 ppc/altivec 的优化后的 hashcash.c 二进制文件的速度是普通的 ansi c 版本的 4 倍(1ghz 的 g4/altivec 在处理 hashcash/sha 操作时的速度相当于 3ghz 的 pentium4?/mmx;g5 的速度会更快)。因此在我的 tipowerbook 上的测试显示,这个模块的速度比优化后的 c 版本速度慢 40 倍(不过在 x86 上的差距没有这么大)。
由于这个模块的运行速度很慢,可能 pyrex 会是一个比较好的加速方法。至少我认为是如此。“pyrex 化” hashcash.py 的第一件事情(当然是在安装 pyrex 之后)是简单地将其拷贝为 hashcash_pyx.pyx,并试图这样处理:
$ pyrexc hashcash_pyx.pyx
创建二进制模块
运行这个命令会生成一个 hashcash.c 文件(这会对源文件进行一些微小的改动)。不幸的是,调整 gcc 开关刚好适合我的平台需要点技巧,因此我决定采用推荐的捷径,让 distutils 为我做一些工作。标准的 python 安装知道如何在模块安装过程中使用本地的 c 编译器,以及如何使用 distutils 来简化 pyrex 模块的共享。我创建了一个 setup_hashcash.py 脚本,如下所示:
清单 2. setup_hashcash.py 脚本
from distutils.core import setupfrom distutils.extension import extensionfrom pyrex.distutils import build_extsetup( name = hashcash_pyx, ext_modules=[ extension(hashcash_pyx, [hashcash_pyx.pyx], libraries = []) ], cmdclass = {'build_ext': build_ext})
运行下面的命令,完整地编译一个基于 c 的扩展模块 hashcash:
$ python2.3 prime_setup.py build_ext --inplace
代码修改
我把从 hashcash.pyx 生成基于 c 的模块的工作有些简化了。实际上,我需要对源代码进行两处修改;通过查找 pyrexc 抱怨的位置来找到要修改的位置。在代码中,我使用了一个不支持的列表,将其放入一个普通的 for 循环。这非常简单。我还将增量赋值从 counter+=1 修改为 counter=counter+1。
就这么多了。这就是我的第一个 pyrex 模块。
测试速度
为了可以简单地测试要开发的模块的速度提高情况,我编写了一个简单的测试程序来运行不同版本的模块:
清单 3. 测试程序 hashcash_test.py
#!/usr/bin/env python2.3import time, sys, optparsehashcash = __import__(sys.argv[1])start = time.time()print hashcash.mint('mertz@gnosis.cx', bits=20)timer = time.time()-startsys.stderr.write(%0.4f seconds (%d hashes per second)\n % (timer, hashcash.tries[0]/timer))
令人兴奋的是,我决定来看一下只通过 pyrex 编译可以怎样提高速度。注意在下面所有的例子中,真实的时间变化很大,都是随机的。我们要看的内容是“hashes per second”,它可以精确可靠地测量速度。因此比较一下纯粹的 python 和 pyrex:
清单 4. 纯 python 和 “纯 pyrex”的比较
$ ./hashcash_test.py hashcash1:20:041003:mertz@gnosis.cx::i+lynupv:167dca13.7879 seconds (106904 hashes per second)$ ./hashcash_test.py hashcash_pyx > /dev/null6.0695 seconds (89239 hashes per second)
噢!使用 pyrex 几乎慢了 20%。这并不是我期望的。现在应该来分析一下代码可能加速的地方了。下面这个简短的函数会试图消耗所有的时间:
清单 5. hashcash.py 中的函数
def _mint(challenge, bits): answer a 'generalized hashcash' challenge' counter = 0 hex_digits = int(ceil(bits/4.)) zeros = '0'*hex_digits hash = sha while 1: digest = hash(challenge+hex(counter)[2:]).hexdigest() if digest[:hex_digits] == zeros: tries[0] = counter return hex(counter)[2:] counter += 1
我需要利用 pyrex 变量声明的优点来进行加速。有些变量显然是整数,另外一些变量显然是字符串 —— 我们可以指定这些类型。在进行修改时,我将使用 pyrex 的经过改进的 for 循环:
清单 6. 经过最低限度 pyrex 改进的 mint 函数
cdef _mint(challenge, int bits): # answer a 'generalized hashcash' challenge' cdef int counter, hex_digits, i cdef char *digest hex_digits = int(ceil(bits/4.)) hash = sha for counter from 0 <= counter < sys.maxint: py_digest = hash(challenge+hex(counter)[2:]).hexdigest() digest = py_digest for i from 0 <= i /dev/null20.3749 seconds (116636 hashes per second)
这下好多了。我已经对原有的 python 进行了一些细微的改进,这可以稍微提高最初的 pyrex 模块的速度。不过效果还不明显,仅仅提高了很少的百分比。
剖析
有些东西似乎不对。速度提高几个百分比和 pyrex 主页(以及很多 pyrex 用户)那样提高 40 倍有很大的差距。现在应该来看一下 这个 python _mint() 函数中 哪些 地方真正消耗了时间。有一个 quick 脚本(此处没有给出)可以分解复杂操作 sha(challenge+hex(counter)[2:]).hexdigest():
清单 8. hashcash 的 mint 函数的时间消耗
1000000 empty loops: 0.559------------------------------1000000 sha()s: 2.3321000000 hex()[2:]s: 3.151 just hex()s: 1000000 concatenations: 0.8551000000 hexdigest()s: 3.742------------------------------total: 10.079
显然,我并不能将这个循环从 _mint() 函数中删除。虽然 pyrex 改进后的 for 循环可能有一点加速,但是整个函数主要是一个循环。我也不能删除对 sha() 的调用,除非要使用 pyrex 重新实现 sha-1(即使我要这样做,也没有自信自己可以比 python 标准的 sha 模块的作者做得更好)。而且,如果我希望得到一个 sha.sha 对象的 hash 值,就只能调用 .hexdigest() 或 .digest();前者的速度更快。
现在真正要解决的是 hex() 对 counter 变量的转换,以及结果中时间片的消耗情况。我可能需要使用 pyrex/c 的字符串连接操作,而不是 python 的字符串对象。然而,我见过的惟一一种避免 hex() 转换的方法是手工在嵌套循环之外构建一个后缀。虽然这样做可以避免 int 到 char 类型的转换,但是需要生成更多代码:
清单 9. 完全 pyrex 优化过的 mint 函数
cdef _mint(char *challenge, int bits): cdef int hex_digits, i0, i1, i2, i3, i4, i5 cdef char *ab, *digest, *trial, *suffix suffix = '******' ab = alphabet hex_digits = int(ceil(bits/4.)) hash = sha for i0 from 0 <= i0 < 55: suffix[0] = ab[i0] for i1 from 0 <= i1 < 55: suffix[1] = ab[i1] for i2 from 0 <= i2 < 55: suffix[2] = ab[i2] for i3 from 0 <= i3 < 55: suffix[3] = ab[i3] for i4 from 0 <= i4 < 55: suffix[4] = ab[i4] for i5 from 0 <= i5 < 55: suffix[5] = ab[i5] py_digest = hash(challenge+suffix).hexdigest() digest = py_digest for i from 0 /dev/null15.2300 seconds (157550 hashes per second)
换而言之,psyco 之不过添加了两行通用的代码,就几乎能实现相同的目标。当然,psyco 只能用于 x86 平台,而 pyrex 可以在具有 c 编译器的所有环境上执行。但是对于这个特定的例子来说,os.popen('hashcash -m '+options) 的速度会比 pyrex 和 psyco 都快很多倍(当然,假设可以使用 c 工具 hashcash)。
该用户其它信息

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录 Product