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


Posted in Python onApril 13, 2015

 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 available
try:
  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 setup
from distutils.extension import Extension
from Pyrex.Distutils import build_ext
setup(
 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.3
import time, sys, optparse
hashcash = __import__(sys.argv[1])
start = time.time()
print hashcash.mint('mertz@gnosis.cx', bits=20)
timer = time.time()-start
sys.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 hashcash
1:20:041003:mertz@gnosis.cx::I+lyNUpV:167dca
13.7879 seconds (106904 hashes per second)
$ ./hashcash_test.py hashcash_pyx > /dev/null
6.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 < hex_digits:
      if digest[i] != c'0': break
    else:
      tries[0] = counter
      return hex(counter)[2:]

到现在为止一切都非常简单。我只声明了早已知道的一些变量类型,并使用最干净的 Pyrex counter 循环。一个小技巧是将 py_digest(一个 Python 字符串)赋值给 digest(一个 C/Pyrex 字符串),目的是确定其类型。经过实验,我还发现循环字符串比较操作速度都非常快。这些会带来什么好处呢?
清单 7. Pyrex 化 mint 函数的速度结果

$ ./hashcash_test.py hashcash_pyx2 >/dev/null
20.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.332
1000000 hex()[2:]s:   3.151
  just hex()s:     <2.471>
1000000 concatenations: 0.855
1000000 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 <= i < hex_digits:
                if digest[i] != c'0': break
              else:
                return suffix

虽然这个 Pyrex 函数看起来仍然比对应的 C 函数更加简单易读,但是它实际上最初的纯 Python 的版本更为复杂。通过这种方式,在纯 Python 中展开后缀生成与最初的版本相比会对总体速度有些负面的影响。在 Pyrex 中,正如您期望的一样,这些嵌套的循环都是很少花费时间的,因而我节省了转换和分时调度的代价:
清单 10. mint 函数 Pyrex 化优化后的速度结果

$ ./hashcash_test.py hashcash_pyx3 >/dev/null
13.2270 seconds (166125 hashes per second)

当然,这比我开始的时候好多了。但是速度提高也不过是两倍。大部分时间的问题是(此处也是)消耗了太多的时间在对 Python 库的调用上,而我并不能对这些调用编写代码来提高速度。
令人失望的比较

速度提高 50% 到 60% 似乎是值得的。达到这个目标我并没有编写 多少 代码。但是如果您认为是在原来的 Python 版本中添加 两条 语句 import psyco;psyco.bind(_mint),那么这种加速方法就不会给您多深的印象:
清单 11. mint 函数 Psyco 化的加速结果

$ ./hashcash_test.py hashcash_psyco >/dev/null
15.2300 seconds (157550 hashes per second)

换而言之,Psyco 之不过添加了两行通用的代码,就几乎能实现相同的目标。当然,Psyco 只能用于 x86 平台,而 Pyrex 可以在具有 C 编译器的所有环境上执行。但是对于这个特定的例子来说,os.popen('hashcash -m '+options) 的速度会比 Pyrex 和 Psyco 都快很多倍(当然,假设可以使用 C 工具 hashcash)。

 

Python 相关文章推荐
python和shell变量互相传递的几种方法
Nov 20 Python
python切换hosts文件代码示例
Dec 31 Python
Python中使用tarfile压缩、解压tar归档文件示例
Apr 05 Python
python实现的简单文本类游戏实例
Apr 28 Python
Python中set与frozenset方法和区别详解
May 23 Python
Python3结合Dlib实现人脸识别和剪切
Jan 24 Python
Django框架模板语言实例小结【变量,标签,过滤器,继承,html转义】
May 23 Python
使用virtualenv创建Python环境及PyQT5环境配置的方法
Sep 10 Python
python等差数列求和公式前 100 项的和实例
Feb 25 Python
pycharm sciview的图片另存为操作
Jun 01 Python
Django CBV模型源码运行流程详解
Aug 17 Python
解决PDF 转图片时丢文字的一种可能方式
Mar 04 Python
在Python中使用itertools模块中的组合函数的教程
Apr 13 #Python
Python中用Spark模块的使用教程
Apr 13 #Python
简单理解Python中基于生成器的状态机
Apr 13 #Python
Python中的高级函数map/reduce使用实例
Apr 13 #Python
Python遍历目录的4种方法实例介绍
Apr 13 #Python
用Python生成器实现微线程编程的教程
Apr 13 #Python
Python字符串处理函数简明总结
Apr 13 #Python
You might like
用Zend Encode编写开发PHP程序
2006/10/09 PHP
php中apc缓存使用示例
2013/12/25 PHP
ThinkPHP中ajax使用实例教程
2014/08/22 PHP
PHP实现通过中文字符比率来判断垃圾评论的方法
2014/10/20 PHP
ThinkPHP5框架实现简单的批量查询功能示例
2018/06/07 PHP
Django中通过定时任务触发页面静态化的处理方式
2018/08/29 PHP
服务端 VBScript 与 JScript 几个相同特性的写法 By shawl.qiu
2007/03/06 Javascript
jquery.idTabs 选项卡使用示例代码
2014/09/03 Javascript
JS替换字符串中空格方法
2015/04/17 Javascript
js获取页面及个元素高度、宽度的代码
2016/04/26 Javascript
jquery动态切换背景图片的简单实现方法
2016/05/14 Javascript
VUEJS实战之利用laypage插件实现分页(3)
2016/06/13 Javascript
Bootstrap字体图标无法正常显示的解决方法
2016/10/08 Javascript
Node.js复制文件的方法示例
2016/12/29 Javascript
jQuery实现倒计时重新发送短信验证码功能示例
2017/01/12 Javascript
解决在vue项目中webpack打包后字体不生效的问题
2018/09/01 Javascript
Vue多组件仓库开发与发布详解
2019/02/28 Javascript
JS数组及对象遍历方法代码汇总
2020/06/16 Javascript
Openlayers实现距离面积测量
2020/09/28 Javascript
[25:59]Newbee vs TNC 2018国际邀请赛小组赛BO2 第二场 8.16
2018/08/17 DOTA
Python类的基础入门知识
2008/11/24 Python
Python中struct模块对字节流/二进制流的操作教程
2017/01/21 Python
Flask框架各种常见装饰器示例
2018/07/17 Python
在python中使用xlrd获取合并单元格的方法
2018/12/26 Python
python selenium执行所有测试用例并生成报告的方法
2019/02/13 Python
Python实现通过解析域名获取ip地址的方法分析
2019/05/17 Python
JAKO-O德国野酷台湾站:德国首屈一指的婴幼童用品品牌
2019/01/14 全球购物
澳大利亚设计的优质鞋类和适合澳大利亚生活方式的服装:Rivers
2019/04/23 全球购物
学前教育教师求职自荐信
2013/09/22 职场文书
英语专业学生的自我评价
2013/12/30 职场文书
国窖1573广告词
2014/03/21 职场文书
向国旗敬礼活动总结
2014/09/27 职场文书
2014光棍节大学生联谊活动方案
2014/10/10 职场文书
企业开业庆典答谢词
2015/01/20 职场文书
学习《中小学教师职业道德规范》心得体会
2016/01/18 职场文书
智慧人生:永远不需要向任何人解释你自己
2019/08/20 职场文书