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

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)。

(0)

相关推荐

  • python基础教程之基本内置数据类型介绍

    Python基本内置数据类型有哪些 一些基本数据类型,比如:整型(数字).字符串.元组.列表.字典和布尔类型.随着学习进度的加深,大家还会接触到更多更有趣的数据类型,python初学者入门时先了解这几种类型就可以了. 基本内置数据类型对应符号 1)整型--int--数字python有5种数字类型,最常见的就是整型int.例如:1234.-12342)布尔型--bool--用符号==表示布尔型是一种比较特殊的python数字类型,它只有True和False两种值,它主要用来比较和判断,所得结果叫做

  • 详解Python中内置的NotImplemented类型的用法

    它是什么? >>> type(NotImplemented) <type 'NotImplementedType'> NotImplemented 是Python在内置命名空间中的六个常数之一.其他有False.True.None.Ellipsis 和 __debug__.和 Ellipsis很像,NotImplemented 能被重新赋值(覆盖).对它赋值,甚至改变属性名称, 不会产生 SyntaxError.所以它不是一个真正的"真"常数.当然,我们应

  • 详解Python的Django框架中manage命令的使用与扩展

    [简介] django-admin.py是Django的一个用于管理任务的命令行工具.本文将描述它的大概用法. 另外,在每一个Django project中都会有一个manage.py.manage.py是对django-admin.py的简单包装,它额外帮助我们做了两件事情: 它将你的project的包放到sys.path中 它将DJANGO_SETTINGS_MODULE环境变量设置为了你的project的setting.py文件的位置. 如果你是通过setup.py工具来安装Django的

  • Python中扩展包的安装方法详解

    前言 作为一个pythoner ,包的安装时必须懂的,这个语言跟matlab很类似,开源.共享,只要你有好的方法,都可以作为一个库,供大家下载使用,毕竟俗话说:"人生苦短,请用Python吗",下面话不多说,我们来一起看看详细的介绍吧. 方法如下: 1.单文件模块 将包拷贝到python安装目录下Lib下,eg:D:\py\Lib. 2.多文件模块 找到模块包(压缩文件zip或tar.gz)下载,进行解压,然后控制台中执行:python setup.py install xxx即可 3

  • Python中内置数据类型list,tuple,dict,set的区别和用法

    Python语言简洁明了,可以用较少的代码实现同样的功能.这其中Python的四个内置数据类型功不可没,他们即是list, tuple, dict, set.这里对他们进行一个简明的总结. List 字面意思就是一个集合,在Python中List中的元素用中括号[]来表示,可以这样定义一个List: L = [12, 'China', 19.998] 可以看到并不要求元素的类型都是一样的.当然也可以定义一个空的List: L = [] Python中的List是有序的,所以要访问List的话显然

  • vc6编写python扩展的方法分享

    系统环境:VC6 + Python-2.5.4 1.下载Python-2.5.4源码. 2.解压,打开D:\Python-2.5.4\PC\VC6\pcbuild.dsw,编译,D:\Python-2.5.4\PC\VC6\下得到python25.dll.python25_d.dll.python25.lib.python25_d.lib. 3.使用VC6建立一个动态链接库工程,拷贝D:\Python-2.5.4\PC\example_nt\example.c到工程目录下,并添加到工程中. 4.

  • Python实现扩展内置类型的方法分析

    本文实例讲述了Python实现扩展内置类型的方法.分享给大家供大家参考,具体如下: 简介 除了实现新的类型的对象方式外,有时我们也可以通过扩展Python内置类型,从而支持其它类型的数据结构,比如为列表增加队列的插入和删除的方法.本文针对此问题,结合实现集合功能的实例,介绍了扩展Python内置类型的两种方法:通过嵌入内置类型来扩展类型和通过子类方式扩展类型. 通过嵌入内置类型扩展 下面例子通过将list对象作为嵌入类型,实现集合对象,并增加了一下运算符重载.这个类知识包装了Python的列表,

  • 使用C语言扩展Python程序的简单入门指引

    一.简介 Python是一门功能强大的高级脚本语言,它的强大不仅表现在其自身的功能上,而且还表现在其良好的可扩展性上,正因如此,Python已经开始受到越来越多人的青睐,并且被屡屡成功地应用于各类大型软件系统的开发过程中. 与其它普通脚本语言有所不同,Python程序员可以借助Python语言提供的API,使用C或者C++来对Python进行功能性扩展,从而即可以利用Python方便灵活的语法和功能,又可以获得与C或者C++几乎相同的执行性能.执行速度慢是几乎所有脚本语言都具有的共性,也是倍受人

  • Python内置数据类型详解

    通常来说Python在编程语言中的定位为脚本语言--scripting language 高阶动态编程语言. Python是以数据为主,变量的值改变是指变量去指到一个地址. 即:Id(变量)->展示变量的地址. 因此一个具体的值,会有不同的变量名. Python的数据类型: 数字.字符串.列表.元组.字典 数字和字符串其实是很基本的数据类型,在Python中和其他语言相差不是很大的,在这里就不细讲了. Dictionary介绍: Dictionary是Python的内置数据类型之一,它定义了键和

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

    Pyrex 是一种专门设计用来编写 Python 扩展模块的语言.根据 Pyrex Web 站点的介绍,"它被设计用来在友好易用的高级 Python 世界和凌乱的低级 C 世界之间搭建一个桥梁."虽然几乎所有的 Python 代码都可以作为有效的 Pyrex 代码使用,但是您可以在 Pyrex 代码中添加可选的静态类型声明,从而使得这些声明过的对象以 C 语言的速度运行. 加速 Python 从某种意义上来说,Pyrex 只是不断发展的 Python 类语言系列的一个部分:Jython

  • 配置eAccelerator和XCache扩展来加速PHP程序的执行

    eaccelerator安装配置PHP加速 eAccelerator简介 eAccelerator是一个的免费.开源的PHP模块,它能够为提供PHP加速.优化.加码.和动态内容缓存功能.它通过存储PHP脚本编译后的状态而加快执行PHP脚本的速度,而不需要频繁的编译这个PHP脚本.而且它能优化PHP脚本,以提高执行PHP的速度.eAccelerator特色是减少了服务器负载.使PHP脚本加速1-10倍. 下载地址:http://sourceforge.net/projects/eaccelerat

  • 操作Windows注册表的简单的Python程序制作教程

    通过Python操作注册表有两种方式,第一种是通过Python的内置模块 _winreg:另一种方式就是Win32 Extension For Python 的win32api模块,但是需要进行额外的安装.这里主要给出一些_winreg和win32api的Demo代码. 1. _winrg 可以参考官方的参考文档: http://docs.python.org/library/_winreg.html http://www.python.org/doc/2.6.2/library/_winreg

  • 浅要分析Python程序与C程序的结合使用

    Python 是一种用于快速开发软件的编程语言,它的语法比较简单,易于掌握,但存在执行速度慢的问题,并且在处理某些问题时存在不足,如对计算机硬件系统的访问,对媒体文件的访问等.而作为软件开发的传统编程语言 C 语言,却能在这些问题上很好地弥补 Python 语言的不足.因此,本文通过实例研究如何在 Python 程序中整合既有的 C 语言模块,包括用 C 语言编写的源程序和动态链接库等,从而充分发挥 Python 语言和 C 语言各自的优势. 概览 背景知识介绍 Python 语言的特点 Pyt

  • 使用C语言来扩展Python程序和Zope服务器的教程

    有几个原因使您可能想用 C 扩展 Zope.最可能的是您有一个已能帮您做些事的现成的 C 库,但是您对把它转换成 Python 却不感兴趣.此外,由于 Python 是解释性语言,所以任何被大量调用的 Python 代码都将降低您的速度.因此,即使您已经用 Python 写了一些扩展,您仍然要考虑把其中最常被调用的部分改用 C 来写.不论哪种方式,扩展 Zope 都是从扩展 Python 开始.此外,扩展 Python 会给您带来其它的好处,因为您的代码将可以从任何 Python 脚本访问,而不

  • python程序文件扩展名知识点详解

    python程序文件的扩展名称是什么 python程序的扩展名有.py..pyc..pyo和.pyd..py是源文件,.pyc是源文件编译后的文件,.pyo是源文件优化编译后的文件,.pyd是其他语言写的python库. 扩展名 在写Python程序时我们常见的扩展名是py, pyc,其实还有其他几种扩展名.下面是几种扩展名的用法. py:py就是最基本的源码扩展名.windows下直接双击运行会调用python.exe执行. pyw:pyw是另一种源码扩展名,跟py唯一的区别是在windows

  • 提升Python程序运行效率的6个方法

    Python是一个很酷的语言,因为你可以在很短的时间内利用很少的代码做很多事情.不仅如此,它还能轻松地支持多任务,比如多进程等.Python批评者有时会说Python执行缓慢.本文将尝试介绍6个技巧,可加速你的Python应用程序. 1.让关键代码依赖于外部包 虽然Python让许多编程任务变得容易,但它可能并不总能为紧急的任务提供最佳性能.你可以为紧急的任务使用C.C++或机器语言编写的外部包,这样可以提高应用程序的性能.这些包都是不能跨平台的,这意味着你需要根据你正在使用的平台,寻找合适的包

  • 如何让python程序正确高效地并发

    目录 python线程何时需要拥有GIL? 认知模型1:同一时刻只有一个线程运行python代码 模型2:不保证每 5 毫秒释放一次 GIL 模型3:非 Python 代码可以显式释放 GIL 模型4:调用 Python C API 需要 GIL 什么场景适合利用python的并发? 使用Python C API的低级代码 前言: 如今,大多数计算机都带有多个内核,允许多个线程并行运行计算.即使处理器只有单核,也可以通过并发编程来提升程序的运行效率,比如在一个线程等待网络数据的同时,允许另一个线

  • 使用Protocol Buffers的C语言拓展提速Python程序的示例

    Protocol Buffers (类似XML的一种数据描述语言)最新版本2.3里,protoc-py_out命令只生成原生的Python代码. 尽管PB(Protocol Buffers)可以为C++语言生成快速解析和序列化代码,但是这种方式对于Python不适用,并且手动生成的已包装的代码需要非常大的维护工作.在讨论组里,这是一个常见的功能要求,由于一个必备的客户端组件-AppEngine(根据团队介绍名称为AppEngine),生成原生的Python代码有更高的优先级. 幸运的是, PB

随机推荐