- 内容提要
- 前言
- 作者简介
- 封面简介
- 第1章 理解高性能 Python
- 第2章 通过性能分析找到瓶颈
- 2.1 高效地分析性能
- 2.2 Julia 集合的介绍
- 2.3 计算完整的 Julia 集合
- 2.4 计时的简单方法——打印和修饰
- 2.5 用 UNIX 的 time 命令进行简单的计时
- 2.6 使用 cProfile 模块
- 2.7 用 runsnakerun 对 cProfile 的输出进行可视化
- 2.8 用 line_profiler 进行逐行分析
- 2.9 用 memory_profiler 诊断内存的用量
- 2.10 用 heapy 调查堆上的对象
- 2.11 用 dowser 实时画出变量的实例
- 2.12 用 dis 模块检查 CPython 字节码
- 2.13 在优化期间进行单元测试保持代码的正确性
- 2.14 确保性能分析成功的策略
- 2.15 小结
- 第3章 列表和元组
- 第4章 字典和集合
- 第5章 迭代器和生成器
- 第6章 矩阵和矢量计算
- 第7章 编译成 C
- 第8章 并发
- 第9章 multiprocessing 模块
- 第10章 集群和工作队列
- 第11章 使用更少的 RAM
- 第12章 现场教训
7.7 Shed Skin
Shed Skin是一个和Python2.4~2.7一起使用的实验性的把Python转为C++的编译器。它使用类型引用来自动检查Python程序来注解每一个变量。被注解的代码接着就会被翻译成C代码,可以被g++之类的标准编译器编译。自动反射是Shed Skin的一个很有趣的特征,用户只要求提供一个关于怎样用正确类型的数据来调用函数的例子,Shed Skin会把剩下的都给做了。
类型引用的好处是程序员不需要显式地手动声明类型。付出的开销就是分析器需要能够推导出程序中的每个变量的类型。在当前版本中,上千行Python代码能被自动转换成C。它使用了Boehm的标记-清除垃圾收集器来自动管理内存。Boehm的垃圾收集器也用于Mono和Java的GNU编译器。Shed Skin的缺点就是它使用了外部实现的标准库。任何没有被实现的库(包含numpy)将不受支持。
整个项目有超过75个例子,包括了许多集中于数学方面的纯Python模块,甚至有一个能够完整工作的Commodore 64模拟器。每一个模块在用Shed Skin编译后,相比本地运行的CPython,速度得到明显的提升。
Shed Skin能构建独立的可执行程序,不依赖于已安装的Python安装包或者在常规Python代码中用import导入的扩展模块。
编译模块管理它们自己的内存。这意味着来自Python进程的内存被拷贝进来并且结果被拷贝出去——没有显式的共享内存。对于大块内存(例如,一个大矩阵)执行拷贝的开销很显著,我们在本节末尾会简略看一下。
Shed Skin提供了和PyPy一样的很多好处(请看7.11节)。因此,PyPy可能更容易使用,因为它不需要任何编译步骤。Shed Skin自动增加类型注解的方式可能让一些用户很感兴趣,并且生成的C代码比Cython生成的可读性高,如果你希望修改生成的C代码的话。我们肯定地猜测自动类型反射代码会让社区中的其他编译器编写者很感兴趣。
7.7.1 构建扩展模块
在这个例子中,我们会构建一个扩展模块。我们会导入生成的模块,就如在Cython的示例中做的那样。我们也能够把这个模块编译成一个独立的可执行文件。
在例7-8中,我们有一个在独立模块中的代码示例,它包含了无法注解的普通Python代码。也要注意我们已经增加了一个__main__测试——这使得这个模块可以被独立运行来做类型分析。Shed Skin能够使用这个__main__块来提供示例中的参数,从而去推导出传入calculate_z的参数类型,进而推导出在CPU密集型函数内部所使用的数据类型。
例7-8 把我们的CPU密集型函数挪到一个分离的模块去(就如我们用Cython做的那样)来让Shed Skin的自动类型推导系统运行
# shedskinfn.py def calculate_z(maxiter, zs, cs): """Calculate output list using Julia update rule""" output = [0] * len(zs) for i in range(len(zs)): n = 0 z = zs[i] c = cs[i] while n < maxiter and abs(z) < 2: z = z * z + c n += 1 output[i] = n return output if __name__ == "__main__": # make a trivial example using the correct types to enable type inference # call the function so Shed Skin can analyze the types output = calculate_z(1, [0j], [0j])
我们能够像例7-9中那样,既可以在这个模块被编译之前导入它,也可以在这个模块被编译之后再导入它,就像通常所做的那样。既然代码没有被改动过(不像使用Cython那样),我们就能在编译前调用Python的原生模块。如果你还未编译过你的代码,你不会得到速度提升,但是你能够使用常规的Python工具以一种轻量级的方式来调试。
例7-9 导入外部模块以便让Shed Skin编译它
... import shedskinfn ... def calc_pure_python(desired_width, max_iterations): #... start_time = time.time() output = shedskinfn.calculate_z(max_iterations, zs, cs) end_time = time.time() secs = end_time - start_time print "Took", secs, "seconds" ...
就如在例7-10中所见,我们能使用Shedskin -ann shedskin.py让Shed Skin提供一个它所分析的注解输出,这会生成shedskinfn.ss.py。如果我们要编译一个扩展模块,我们只需要用空的__main__函数来做分析“种子”。
例7-10 检查Shed Skin的注解输出看下它推导出了哪些类型
# shedskinfn.ss.py def calculate_z(maxiter, zs, cs): # maxiter: [int], # zs: [list(complex)], # cs: [list(complex)] """Calculate output list using Julia update rule""" output = [0] * len(zs) # [list(int)] for i in range(len(zs)): # [__iter(int)] n = 0 # [int] z = zs[i] # [complex] c = cs[i] # [complex] while n < maxiter and abs(z) < 2: # [complex] z = z * z + c # [complex] n += 1 # [int] output[i] = n # [int] return output # [list(int)] if __name__ == "__main__": # [] # make a trivial example using the correct types to enable type inference # call the function so Shed Skin can analyze the types output = calculate_z(1, [0j], [0j]) # [list(int)]
__main__的类型被分析之后,接着在calculate_z的内部,变量z和c的类型能够从与它们有互动的对象中推导出来。
我们使用shedskin --extmod shedskinfn.py来编译这个模块,会生成下列文件:
shedskinfn.hpp(C++头文件)。
shedskinfn.cpp(C++源文件)。
Makefile。
通过运行make,我们就生成了shedskinfn.so。我们能够通过import shedskinfn在Python中使用。用shedskinfn.so来执行julia1.py的时间只有0.4秒——相比未编译的版本,只做了很小的工作就取得了巨大的胜利。
我们也能展开abs函数,就如我们用Cython在例7-7中做的那样。在运行这个版本(只改了abs那一行)并使用一些额外的标志位--nobounds --nowrap之后,我们得到了一个0.3秒的最终执行时间。这比Cython版本稍慢一点(慢0.05秒),但是我们不需要声明所有的类型信息。这使得用Shed Skin来做实验很容易。PyPy以相近的速度运行了这个代码的相同版本。
备忘
仅仅是因为Cython、PyPy和Shed Skin在这个例子中跑出了相近的速度,但这并不意味着这是普遍性的结果。为了让你的项目得到最佳提速,你必须调查这些不同的工具并运行你自己的实验。
Shed Skin允许你声明额外的编译期选项,比如-ffast-math或-o3,并且你能够在两遍扫描(第一遍收集执行统计信息,第二遍在这些统计信息基础上优化生成的代码)中增加剖析导向优化(PGO)来设法做出进一步的速度提升。然而剖析导向优化没有让Julia的例子跑得更快,在实践中它通常很少或没有真实效果。
你应该注意到默认整数是32比特的,如果你想要更大的64比特整数范围,那么就声明--long标志。你也应该避免在紧凑的内循环中分配小对象(例如,new tuples),因为垃圾收集器并不能高效地处理它们。
7.7.2 内存拷贝的开销
在我们的示例中,Shed Skin通过把数据扁平化成基本C类型的方式把Python的list对象拷贝进Shed Skin的环境中来,接着在执行函数的末尾把结果从C函数转换为Python的list。这些转换和拷贝花费时间。这大概就占了我们在前面的结果中看到的多出来的0.05秒(相比Cython的结果多了0.05秒)吧?
我们能够修改Shedskinfn.py文件来移除实际的工作量,这样我们就能计算经由Shed Skin把数据拷进拷出的开销。下面的calculate_z的变体是我们所需要的:
def calculate_z(maxiter, zs, cs): """Calculate output list using Julia update rule""" output = [0] * len(zs) return output
当我们用这个框架函数执行julia1.py时,执行时间接近于0.05秒(显然它不计算正确结果!)。这个时间是把2000000个复数拷贝进calculate_z并把1000000个整数再次拷贝出来的开销。实质上,Shed Skin和Cython生成了相同的机器码,执行速度的差异归结于Shed Skin在一个独立的内存空间运行,还有就是所需的拷进拷出数据的开销。硬币的另一面就是,使用Shed Skin,你不必预先做注解工作,节省了相当多的时间。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论