Python高性能编程

978-7-115-45489-8
作者: 【美】 戈雷利克 (Micha Gorelick) 欧日沃尔德(Ian Ozsvald)
译者: 胡世杰徐旭彬
编辑: 陈冀康
分类: Python

图书目录:

详情

本书针对有一定基础的Python程序员,将指导读者实现代码优化的各种方法。读者将学习如何使用智能的算法,以及使用各种相关的技术,例如numpy、cython、cpython等,以及各种多线程和多节点策略。市面上一致缺乏学习用Pyhton完成高度计算性任务的教程,而本书正是这方面不可多得的一本好书。

图书摘要

版权信息

书名:Python高性能编程

ISBN:978-7-115-45489-8

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 著    [美] Micha Gorelick Ian Ozsvald

  译    胡世杰 徐旭彬

  责任编辑 陈冀康

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright © 2014 by O’Reilly Media, Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2017. Authorized translation of the English edition, 2015 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体版由O’Reilly Media, Inc. 授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


Python语言是一种脚本语言,其应用领域非常广泛,包括数据分析、自然语言处理、机器学习、科学计算、推荐系统构建等。

本书共有12章,围绕如何进行代码优化和加快实际应用的运行速度进行详细讲解。本书主要包含以下主题:计算机内部结构的背景知识、列表和元组、字典和集合、迭代器和生成器、矩阵和矢量计算、并发、集群和工作队列等。最后,通过一系列真实案例展现了在应用场景中需要注意的问题。

本书适合初级和中级Python程序员、有一定Python语言基础想要得到进阶和提高的读者阅读。


Python很容易学。你之所以阅读本书可能是因为你的代码现在能够正确运行,而你希望它能跑得更快。你可以很轻松地修改代码,反复地实现你的想法,你对这一点很满意。但能够轻松实现代码跑得够快之间的取舍却是一个世人皆知且令人惋惜的现象。而这个问题其实是可以解决的。

有些人想要让顺序执行的过程跑得更快。有些人需要利用多核架构、集群,或者图形处理单元的优势来解决他们的问题。有些人需要可伸缩系统在保证可靠性的前提下酌情或根据资金多少处理更多或更少的工作。有些人意识到他们的编程技巧,通常是来自其他语言,可能不如别人的自然。

我们会在本书中覆盖所有这些主题,给出明智的指导去了解瓶颈并提出效率更高、伸缩性更好的解决方案。我们也会在本书中包含那些来自前人的战场故事,让你可以避免重蹈覆辙。

Python很适合快速开发、生产环境部署,以及可伸缩系统。Python的生态系统里到处都是帮你解决伸缩性的人,让你有更多时间处理那些更有挑战性的工作。

你使用Python的时间已经足够长,了解为什么某些代码会跑得慢,也见过以本书讨论的Cython、numpy以及PyPy等技术作为解决方案。你可能还有其他语言的编程经验,因此知道解决性能问题的路不止一条。

本书主要的目标读者是那些需要解决CPU密集型问题的人,同时我们也会关注数据传输以及内存密集型问题。科学家、工程师、数据分析专家、学者通常会面临这些问题。

我们还会关注网页开发者可能面临的问题,包括数据的移动以及为了快速提升性能而使用PyPy这样的即时(JIT)编译器。

如果你有一个C(或C++,或Java)的背景可能会有帮助,但这不是必须的。Python最常用的解释器(CPython——你在命令行输入python时启动的标准解释器)是用C写的,所以各种钩子和库全都血淋淋地暴露了内部的C机制。但我们也会谈到对C一无所知的人也能使用的许多其他技术。

你可能还具有CPU、内存架构和数据总线的底层知识,还是那句话,这些也不完全是必须的。

本书适用于中高级Python程序员。积极的Python初学者可能可以跟上,但我们建议要具有坚实的Python基础。

我们不会探讨存储系统优化。如果你有一个SQL或NoSQL瓶颈,本书可能帮不了你。

我们两位作者在业界和学术界工作了很多年,专门应对大数据应用、处理我需要更快得到答案!之类的请求、可伸缩架构等需求。我们会将自己经历千辛万苦获得的经验传授于你,让你免于重蹈覆辙。

在每一章开头,我们会列出问题,并在后续的文字中回答(如果没有回答,告诉我们,我们会在下一个版本中修正!)。

我们会覆盖下面这些主题:

Python 2.7在科学和工程计算中是占主导地位的Python版本。在*nix环境(通常是Linux或Mac)下,64位的版本占了主导地位。64位让你能够拥有更宽广的RAM寻址范围。*nix让你构建出的应用程序的行为、部署和配置方法都可以很容易地被别人所理解。

如果你是一个Windows用户,那么你就要系好安全带了。我们展示的大多数代码都可以正常工作,但有些东西是针对特定操作系统的,你将不得不研究Windows下的解决方案。Windows用户可能面临的最大的困难是模块的安装:搜索StackOverflow等站点应该可以帮助你找到你需要的答案。如果你正在使用Windows,那么使用一台安装了Linux的虚拟机(比如VirtualBox)可能可以帮助你更自由地进行实验。

Windows用户绝对应该看看那些通过Anaconda、Canopy、Python(x,y)或Sage等Python发行版提供的打包的解决方案。这些发行版也会让Linux和Mac用户的生活简单许多。

Python 3是Python的未来,每一个人都在迁移过去。尽管Python 2.7还将在接下来的很多年里面继续跟我们做伴(有些安装版仍在使用2004年的Python 2.4),它的退役日期已经被定在2020年了。

升级到Python 3.3+让Python库的开发者伤透了脑筋,人们移植代码的速度一直都很慢(这是有原因的),所以人们转用Python 3的速度也很慢。这主要是因为要把一个混用了Python 2的string和Unicode数据类型的应用程序切换成Python 3的Unicode和byte实在太过复杂了。

通常来说,当你需要重现基于一批值得信任的库的结果时,你不会想要站在危险的技术前沿。高性能Python的开发者更有可能在接下来的几年里使用和信任Python 2.7。

本书的大多数代码只需要稍做修改就能运行于Python 3.3+(最明显的修改是print从一个语句变成了一个函数)。在一些地方,我们将特地关注Python 3.3+带来的性能提升。一个可能需要你关注的地方是在Python 2.7中 / 表示integer的除法,而在Python 3中它变成了float的除法。当然,作为一个好的开发者,你精心编写的单元测试应该已经在测试你的关键代码路径了,所以如果你的代码需要关注这点,那么你应该已经收到来自你单元测试的警告了。

scipynumpy从2010年开始就已经兼容Python 3了。matplotlib从2012年开始兼容,scikit-learn是2013,NLTK是2014,Django是2013。这些库的迁移备忘录可以在它们各自的代码库和新闻组里查看。如果你也有旧代码需要移植到Python 3,那么就值得回顾一下这些库移植的过程。

我们鼓励你用Python 3.3+进行新项目的开发,但你要当心那些最近刚刚移植还没有多少用户的库——追踪bug将会更困难。比较明智的做法是让你的代码可以兼容Python 3.3+(学习一下__future__模块的导入),这样未来的升级就会更简单。

有两本参考手册不错:《把Python 2的代码移植到Python 3》和《移植到Python 3:深度指南》。Anaconda或Canopy这样的发行版让你可以同时运行Python 2和Python 3——这会让你的移植变得简单一些。

本书版权符合知识共享协议“署名-非商业性使用-禁止演绎3.0”。

欢迎以非商业性目的使用本书,包括非商业性教育。本书许可完整转载,如果你需要部分转载,请联系O’Reilly(见后面“联系我们”部分)。请根据下面的提示进行署名。

我们经过协商认为本书应该使用知识共享许可证,让其内容在世界上更广泛传播。如果这个决定帮到了你,我们将十分高兴收到你的啤酒。我们估计O’Reilly的员工对啤酒的看法跟我们相同。

如果你需要使用本书,知识共享许可证要求你署名。署名意味着你需要写一些东西让其他人能够找到本书。下面是一个不错的例子:“High Performance Python by Micha Gorelick and Ian Ozsvald (O’Reilly). Copyright 2014 Micha Gorelick and Ian Ozsvald, 978-1-449-36159-4.”

我们鼓励你在Amazon这样的公开网站上评论本书——请帮助其他人了解他们是否能从本书中受益!你也可以发E-mail给我们:feedback@highperformancepython.com。

我们特别希望听到您指出本书的错误,本书帮到你的成功案例,以及我们应该在下一版本加上的高性能技术。你可以通过O’Reilly官网给我们留言。

至于抱怨,欢迎你使用即时抱怨传输服务> /dev/null

本书采用下列排版约定:

斜体

表示新词、E-mail地址、文件名,以及文件扩展名。

等宽

用于程序列印,以及在文字中表示命令、模块和程序元素,如变量或函数名、数据库、数据类型、环境变量、语句和关键字。

等宽加粗

表示命令或其他需要用户原封不动输入的文字。

等宽斜体

表示需要被替换成用户指定的值或根据上下文决定的值。

 问题 

这个记号表示一个问题或练习。

 备忘 

这个记号表示一个备忘。

 警告 

这个记号表示一个警告或注意。

补充材料(示例代码、练习等)可以通过GitHub下载。

本书是为了帮你搞定你的问题。通常来说,只要是本书提供的示例代码,你就可以在你的程序和文档中使用。你不需要联系我们获得许可,除非你需要对很大一部分代码进行转载。比如,写一个使用了好几段本书代码的程序不需要许可。以CD-ROM的形式销售或分发O’Reilly图书中的示例需要许可。引用本书文字和示例代码回答问题不需要许可。在你的产品文档中合并大量本书示例代码需要许可。

如果你觉得你对示例代码的使用超出了上述的许可范围,请通过permissions@oreilly.com联系我们。

Safari在线图书是一个应需数字图书馆,它以书和视频的形式提供来自全球顶尖作者的技术和商业内容。

技术专家、软件开发者、网页设计者,以及商业和创新人员将Safari在线图书当成他们研究、解决问题、学习和资格认证训练的主要资源。

Safari在线图书为企业,政府,教育和个人提供了各种收费标准。

其成员可以通过全文搜索数据库访问成千上万的图书,训练视频,以及还未正式出版的手稿。它们来自O’Reilly Media、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology,以及其他几百个出版社。更多信息请在线访问https://www.safaribooksonline.com/

请将关于本书的评论和问题发给本书出版社:

O’Reilly Media, Inc.

1005 Gravenstein Highway North

Sebastopol, CA 95472

800-998-9938 (in the United States or Canada)

707-829-0515 (international or local)

707-829-0104 (fax)

你也可以发送E-mail到bookquestions@oreilly.com对本书进行评论或询问技术问题。

关于我们的图书、课程、会议和新闻等更多信息请访问我们的网站。

我们的Facebook:http://facebook.com/oreilly

Twitter:http://twitter.com/oreillymedia

YouTube:http://www.youtube.com/oreillymedia

感谢来自Jake Vanderplas、Brian Granger、Dan Foreman-Mackey、Kyran Dale、John Montgomery、Jamie Matthews、Calvin Giles、William Winter、Christian Schou Oxvig、Balthazar Rouberol、Matt “snakes” Reiferson、Patrick Cooper和Michael Skirpan的反馈和贡献。Ian感谢他的妻子Emily让他消失10个月之久来完成本书(她真的太善解人意了)。Micha感谢Elaine及其他朋友和他的家庭能够耐心等待他学习写书。O’Reilly也是很好的合作伙伴。

第12章的提供者非常亲切地共享了他们的时间和宝贵的经验。我们感谢Ben Jackson、Radim Řehůřek、Sebastjan Trebca、Alex Kelly、Marko Tasic和Andrew Godwin花费的时间和精力。


Micha Gorelick在bitly公司从事与数据打交道的工作,并以负责建立了快速前进实验室(Fast Forward Labs),研究从机器学习到高性能流算法领域的问题。

Ian Ozsvald是一个数据科学家,并且在ModelInsight.io担任Python老师,具有超过10年的Python经验。他已经在PyCon和PyData大会上讲课超过10年,并且在伦敦从事人工智能和高性能计算领域的咨询工作超过10年时间。Ian的背景涉及Python和C++,结合了Linux和Windows开发、存储系统、许多自然语言处理和文本处理,机器学习以及数据可视化。他在许多年前也共同创建了专注于Python的视频学习网站ShowMeDo.com。


本书封面上的动物是矛头蝮蛇。在法语字面上是“钢矛”的意思,该名字为一些主要发现于马提尼克岛上的蛇的种类所保留。它也可能用来指其他的矛头蝮蛇种类,比如圣卢西亚矛头蝮蛇、普通矛头蝮蛇,以及三色矛头蝮蛇。所有这些种类都属于蝮蛇,所以就以两个位于眼睛和鼻孔之间的看起来像斑点的热感应器官来命名。

由于大部分三色矛头蝮蛇和普通矛头蝮蛇能导致咬伤后致死,所以在美洲矛头蝮属种的蛇比任何其他的属种要为更多人的死亡而负责。在南美咖啡和香蕉种植场的工人惧怕被意图捕获啮齿类动物当点心的普通矛头蝮咬一口。当你在中美洲的河流岸边沐浴在阳光下而不堪忍受寂寞的生活时,要当心据说脾气更暴躁的三色矛头蝮蛇,它是一种危险。

O’Reilly封面上的许多动物是濒危物种,它们对这个世界是重要的。为了了解到更多关于你该如何去帮助它们的信息,请去animals.oreilly.com。

封面图片来自于Wood的动画创造。封面字体是URW打字机字体和Guardian Sans字体,文本字体是Adobe Minion Pro字体,标题是Adobe Myriad Condensed字体,代码字体是Dalton Maag的Ubuntu Mono字体。


读完本章之后你将能够回答下列问题

计算机编程可以被认为是以特定的方式进行数据的移动和转换来得到某种结果。然而这些操作有时间上的开销。因此,高性能编程可以被认为是通过降低开销(比如撰写更高效的代码)或改变操作方式(比如寻找一种更合适的算法)来让这些操作的代价最小化。

数据的移动发生在实际的硬件上,我们可以通过降低代码开销的方式来了解更多硬件方面的细节。这样的练习看上去可能没什么用,因为Python做了很多工作将我们对硬件的直接操作抽象出来。然而,通过理解数据在硬件层面的移动方式以及Python在抽象层面移动数据的方式,你会学到一些编写高性能Python程序的知识。

一台计算机的底层组件可被分为三大基本部分:计算单元,存储单元,以及两者之间的连接。除此之外,这些单元还具有多种属性帮助我们了解它们。计算单元有一个属性告诉我们它每秒能够进行多少次计算,存储单元有一个属性告诉我们它能保存多少数据,还有一个属性告诉我们能以多快的速度对它进行读写,而连接则有一个属性告诉我们它们能以多快的速度将数据从一个地方移动到另一个地方。

通过这些基本单元,我们就可以在各种不同的复杂度级别上描述一个标准工作站。例如,一个标准工作站可以被看作是具有一个中央处理单元(CPU)作为其计算单元,两个独立的存储单元,分别是随机访问内存(RAM)和硬盘(各自有不同的容量和读写速度),最后还有一个总线将所有这些连接在一起。然而,我们还可以深入CPU并发现其内部也有多个存储单元:L1、L2,有时甚至有L3和L4缓存,它们的容量较小(从几KB到十几MB)但速度非常快。这些额外的存储单元通过一个被称为后端总线的特殊总线连接CPU。另外,新的计算机架构通常会有新的配置(比如Intel的Nehalem架构的CPU用英特尔快速通道互联技术替换了前端总线并重新构建了很多连接)。最后,在上述案例的讨论中,我们还忽略了网络连接,这是一种慢速的连接,用于连接其他许多潜在的计算单元和存储单元。

为了帮助理清这些错综复杂的结构,让我们去浏览一下这些基本单元的简要描述。

一台计算机的计算单元是其中央部件——它具有将接收到的任意输入转换成输出的能力以及改变当前处理状态的能力。CPU是最常见的计算单元;然而,最初被设计用于加速计算机图像处理的图形处理单元(GPU)现在变得更加适用于数值计算了,这是因为其自身的并行模式使得大量计算能并发进行。无论哪种类型,一个计算单元都会接收一系列比特(比如代表数字的比特)并输出另外一堆比特(比如代表这些数字之和的比特)。除了实数的基本算数操作和二进制的比特操作以外,一些计算单元还提供了非常特殊的操作,比如“乘加混合计算”,接收三个数字A、B、C并返回A * B + C的值。

计算单元的主要属性是其每个周期能进行的操作数量以及每秒能完成多少个周期。第一个属性通过每周期完成的指令数(IPC)[1]来衡量,而第二个属性则是通过其时钟速度衡量。当新的计算单元被制造出来时,它们的这两个参数总是互相竞争。比如Intel的Core系列具有非常高的IPC但时钟速度较低,而Pentium 4的芯片则完全相反。不过话又说回来,GPU的IPC和时钟速度都很高,但它们有别的问题,我们后面会提到。

另外,当时钟速度提高时,能够立即提高该计算单元上所有的程序运行速度(因为它们每秒能进行更多运算),而提高IPC则在矢量计算能力上有相当程度的影响。矢量计算是指一次提供多个数据给一个CPU并能同时被操作。这种类型的CPU指令被称为SIMD(单指令多数据)。

总之,计算单元在过去十年的进展颇为有限(图1-1)。时钟速度和IPC的提升都限于停滞,因为晶体管已经小到了物理的极限。结果就是芯片制造商开始依靠其他手段来获得更高的速度,包括超线程技术,更聪明的乱序执行和多核架构。

图1-1 CPU的时钟速度随时间的变化(数据来自CPU DB)

超线程技术为主机的操作系统(OS)虚拟了第二个CPU,而聪明的硬件逻辑则试图将两个指令线程交错地插入单个CPU的执行单元。如果成功插入,能比单线程提升30%。一般来讲,当两个线程的工作分布在不同的执行单元上时,这样做效果不错——比如一个操作浮点而另一个操作整数时。

乱序执行允许编译器检测出一个线性程序中某部分可以不依赖于之前的工作,也就是说两个工作能够以各种顺序执行或同时执行。只要两个工作的成果都能够在正确的时间点上依次得到,哪怕它们的计算次序跟程序设计不同,程序也能继续正确运行。这使得当一些指令被阻塞时(比如等待一次内存访问),另一些指令得以执行,以此来提升资源的利用率。

最后也是对于高级程序员来说最重要的是多核架构的普及。这些架构在同一个计算单元中包含了多个CPU,提高了总体计算能力,而且无须等待内存屏障,让单个核心可以跑得更快。这就是为什么现在已经很难找到少于双核的计算机了——双核意味着计算机有两个互连的物理计算单元。虽然这增加了每秒可以进行的操作总数,但是想要让两个计算单元都达到最大利用率的话还需要考虑很多错综复杂的因素。

给CPU增加更多的核心并不一定能提升程序运行的速度。这是由阿姆达尔定律决定的。简单地说,阿姆达尔定律认为如果一个可以运行在多核上的程序有某些执行路径必须运行在单核上,那么这些路径就会成为瓶颈导致最终速度无法通过增加更多核心来提高。

比如,假设我们有一个调查需要100个人参与,该调查需要花费1分钟,如果我们只有一位提问者(该提问者向第一位参与者提问,等待回答,然后移向下一位参与者),那么我们可以用100分钟完成这个任务。这个单人提问并等待回答的流程就是一个顺序执行的过程。对于一个顺序执行的过程,我们每次只能完成一个操作,后面的操作必须等待前面的操作完成。

然而,如果我们有两位提问者,他们就可以同时进行测试,用50分钟完成任务。这是由于两位提问者之间不需要互相了解,没有依赖关系,所以整个任务就很容易划分。

增加更多提问者可以进一步提速,直到我们有100位提问者。此时,整个流程仅需要1分钟就可以完成,仅取决于参与者回答问题所需要的时间。继续增加提问者将不会带来任何速度提升,因为这些多余的提问者无事可干——所有的参与者都已经在接受调查!此时,唯一能够降低整体时间的办法是降低单个参与者完成调查的时间,也就是降低顺序部分所需要的执行时间。同样,对于CPU,我们可以增加更多的核心直到某个必须单核执行的任务成为瓶颈。也就是说,任何并行计算的瓶颈最终都会落在其顺序执行的那部分任务上。

另外,对于Python来说,充分利用多核性能的阻碍主要在于Python的全局解释器锁(GIL)。GIL确保Python进程一次只能执行一条指令,无论当前有多少个核心。这意味着即使某些Python代码可以使用多个核心,在任意时间点仅有一个核心在执行Python的指令。以前面调查的例子来说,即使我们有100位提问者,然而一次仅有一位可以提问和接受回答,并没有什么用!这看上去是个严重的阻碍,特别是当现在计算机发展的趋势就是拥有更多而非更快的计算单元的时候。好在这个问题其实可以通过一些方法来避免,比如标准库的multiprocessing,或numexpr、Cython等技术,或分布式计算模型等。

计算机的存储单元被用于保存比特。这些比特可能代表程序中的变量,或一幅图片的像素。存储单元的概念包括了主板上的寄存器、RAM以及硬盘。所有这些不同类型的存储单元的主要区别在于其读写数据的速度。更复杂的问题在于,其读写数据的速度还与数据的读写方式息息相关。

比如,大多数存储单元一次读一大块数据的性能远好于读多次小块数据(顺序读取VS随机数据)。将这些存储单元中的数据想象成一本书中的书页,大多数存储单元的读写速度在连续翻页时高于经常从一张随机页跳至另一张随机页。

所有的存储单元或多或少都受到这一影响,但不同类型存储单元受到的影响却大不相同。

除了读写速度以外,存储单元还有一个延时的属性,表示了设备为了查找到需要的数据所花费的时间。一个旋转硬盘的延时可能较高,因为磁盘必须物理旋转到一定速度且读取磁头必须移动到正确的位置。而从另一方面来说,RAM的延时就比较小,因为一切都是固态的。下面是一个标准工作站内常见的各类存储单元的简短描述,以读写速度排序:

旋转硬盘

计算机关机也能保持的长期存储。读写速度通常较慢,因为磁盘必须物理旋转和等待磁头移动。随机访问性能下降但容量很高(TB级别)。

固态硬盘

类似旋转硬盘,读写速度较快但容量较小(GB级别)。

RAM

用于保存应用程序的代码和数据(比如用到的各种变量)。具有更快的读写速度且在随机访问时性能良好,但通常受限于容量(GB级别)。

L1/L2缓存

极快的读写速度。进入CPU的数据必须经过这里。很小的容量(KB级别)。

图1-2展示了当今市面上可以见到的这几类存储单元的区别。

一个清晰可见的趋势是读写速度和容量成反比——当我们试图加快速度时,容量就下降了。因此,很多系统都实现了一个分层的存储:数据一开始都在硬盘里,部分进入RAM,然后很小的一个子集进入L1/L2缓存。这种分层使得程序可以根据访问速度的需求将数据保存在不同的地方。在试图优化程序的存储访问模式时,我们只是简单优化了数据存放的位置、布局(为了增加顺序读取的次数),以及数据在不同位置之间移动的次数。另外,异步I/O和缓存预取等技术还提供了很多方法来确保数据在被需要时就已经存在于对应的地方而不需要浪费额外的计算时间——这些过程可以在进行其他计算时独立进行!

图1-2 各类存储单元的特征(2014年2月的数据)

最后,让我们看看这些基本单元如何互相通信。通信有很多模式,但它们都是同一样东西的变种:总线。

比如说,前端总线是RAM和L1/L2缓存之间的连接。它将已经准备好被处理器操作的数据移入一个集结场所以备计算所需,又将计算结果移出。除此之外还有其他总线,如外部总线就是硬件设备(如硬盘和网卡)通向CPU和系统内存的主干线。该总线通常比前端总线慢。

事实上,L1/L2缓存的很多好处实际上是来自更快的总线。因为可以将需要计算的数据在慢速总线(连接RAM和缓存)上攒成大的数据块,然后以非常快的速度从后端总线(连接缓存和CPU)传入CPU,这样CPU就可以进行更多计算而无须等待这么长的时间。

同样,使用GPU的不利之处很多都来自它所连接的总线:因为GPU通常是一个外部设备,它通过PCI总线通信,速度远远慢于前端总线。结果,GPU数据的输入输出就像是一种抽税操作。异质架构,一种在前端总线上同时具有CPU和GPU的计算机架构的兴起就是为了降低数据传输成本,使得GPU能够被使用在需要传输大量数据的计算上。

除了计算机内部的通信模块,网络可以被认为是另一种通信模块。不过这个模块就比之前讨论的更为灵活,一个网络设备可以直接连接至一个存储设备,如网络连接存储(NAS)设备或计算机集群中的另一台计算机节点。但是网络通信通常要比之前讨论的其他类型的通信慢很多。前端总线每秒可以传输数十GB,而网络则仅有数十MB。

现在我们清楚,一条总线的主要属性是它的速度:在给定时间内它能传输多少数据。该属性由两个因素决定:一次能传输多少数据(总线带宽)和每秒能传输几次(总线频率)。需要说明的是一次传输的数据总是有序的:一块数据先从内存中读出,然后被移动到另一个地方。这就是为什么总线的速度可以被拆分为两个因素,因为这两个因素分别独立影响计算的不同方面:高的总线带宽可以一次性移动所有相关数据,有助于矢量化的代码(或任何顺序读取内存的代码),而另一方面,低带宽高频率有助于那些经常随机读取内存的代码。有意思的是,这些属性是由计算机设计者在主板的物理布局上决定的:当芯片之间相距较近时,它们之间的物理链路就较短,就可以允许更高的传输速度。而物理链路的数量则决定了总线的带宽(带宽这个词真的具有物理上的意义!)。

由于物理接口可以针对某个特定应用优化,所以我们不会奇怪世上存在成百上千种不同类型的连接。图1-3显示了一些常见接口的比特率。注意这图上完全没提到连接的延时,延时决定了一个连接响应数据请求花费的时间(虽然延时跟具体的计算机系统息息相关,但是有来自物理接口的一些基本限制)。

图1-3 各种常见界面的连接速度(图片来自Leadbuffalo)

仅理解计算机的基本组成部分并不足以理解高性能编程的问题。所有这些组件的互动与合作还会引入新的复杂度。本段将研究一些样本问题,描述理想的解决方案以及Python如何实现它们。

警告:本段可能看上去让人绝望——大多数问题似乎都证明Python并不适合解决性能问题。这不是真的,原因有两点。首先,在所有这些“高性能计算要素”中,我们忽视了一个至关重要的要素:开发者。原生Python在性能上欠缺的功能会被迅速开发出来。另外,我们会在本书中介绍各种模块和原理来帮助减轻这里遇到的问题。当这两点结合起来,我们就能在快速开发Python的同时移除很多性能局限。

为了更好地理解高性能编程的要素,让我们来看一段用于判断质数的简单代码样例:

import math
def check_prime(number):
    sqrt_number = math.sqrt(number)
    number_float = float(number)
    for i in xrange(2, int(sqrt_number)+1):
        if (number_float / i).is_integer():
            return False
    return True

print "check_prime(10000000) = ", check_prime(10000000) # False
print "check_prime(10000019) = ", check_prime(10000019) # True

让我们用抽象的计算模型来分析这段代码对比Python运行这段代码时实际发生了什么。由于是抽象模型,我们将忽略很多理想化的计算机以及Python运行代码方式的细节。不过,这是一个在解决实际问题之前很好的练习:思考算法中的通用组件以及如何最优地使用这些组件来解决问题。只要明白在理想情况下以及实际在Python中发生了什么,我们就能让自己的Python代码一步步接近最优。

1.理想计算模型

在代码的开头,我们将number的值保存于RAM中。为了计算sqrt_numbernumber_float,我们需要将该值传入CPU。在理想情况下,我们只需要传一次,它将被保存在CPU的L1/L2缓存中,然后CPU会进行两次计算并将结果传回RAM保存。这是一个理想的情况,因为我们令从RAM中读取number的值的次数最少,转而以快很多的L1/L2缓存的读取代替。另外,我们还令前端总线传输数据的次数最少,以更快的后端总线(连接CPU和各类缓存)的传输代替之。将数据保持在需要的地方并尽量少移动这一场景对于优化来说至关重要。所谓“沉重数据”的概念指的是移动数据需要花费时间,而这就是我们需要避免的。

在代码的循环部分,与其一次次将i输入CPU,我们更希望一次就将number_float和多个i的值输入CPU进行检查。这是可能的,因为CPU的矢量操作不需要额外的时间代价,意味着它可以一次进行多个独立计算。所以我们希望将number_float传入CPU缓存,以及在缓存放得下的情况下传入尽可能多的i的值。对于每一对number_float/i,我们将进行除法计算并检查结果是否为整数,然后传回一个信号表明是否有任意一个结果确实为整数。如果是,则函数结束。如果否,我们继续下一批计算。这样,对于多个i的值,我们只需要传回一个结果,而不是依靠总线返回所有的值。这利用了CPU的矢量化计算的能力,或者说在一个时钟周期内以一条指令操作了多个数据。

这一矢量操作的概念可以用下列代码来表述:

import math
def check_prime(number):
    sqrt_number = math.sqrt(number)
    number_float = float(number)
    numbers = range(2, int(sqrt_number)+1)
    for i in xrange(0, len(numbers), 5):
 # the following line is not valid Python code
 result = (number_float / numbers[i:(i+5)]).is_integer()
 if any(result):
 return False
 return True

这里,我们让程序一次对5个i的值进行除法和整数检查。当被正确地矢量化时,CPU仅需一条指令完成这行代码而不是对每个i进行独立操作。理想情况下,any(result)操作将只发生于CPU内部而无须将数据传回RAM。我们将在第6章讨论更多关于矢量操作的细节,包括它具体如何工作以及在什么情况下有利于你的代码。

2.Python虚拟机

Python解释器为了抽离底层用到的计算元素做了很多工作。这让编程人员无须考虑如何为数组分配内存、如何组织内存以及用什么样的顺序将内存传入CPU。这是Python的一个优势,让你能够集中在算法的实现上。然而它有一个巨大的性能代价。

首先我们要意识到Python核心运行于一组非常优化的指令上。而诀窍就是让Python以正确的次序执行它们来获得更好的性能。比如下例,我们可以轻松看出,虽然两个算法都有O(n)的运行时间,search_fast会比search_slow快,因为它提前中止了循环,跳过了不必要的计算。

def search_fast(haystack, needle):
    for item in haystack:
        if item == needle:
            return True
    return False

def search_slow(haystack, needle):
    return_value = False
    for item in haystack:
        if item == needle:
            return_value = True
    return return_value

通过性能分析查找代码的慢速区域以及寻找更有效的算法其实就是寻找这些无用的操作并删除它们,最终的结果是一样的,但计算和传输数据的次数却大大减少了。

而Python虚拟机抽象层的影响之一就是矢量操作变得不是直接可用了。我们最初的质数函数会循环遍历i的每一个值而不是将多次遍历组合成一个矢量操作。而我们抽象以后的矢量化代码并不是合法的Python代码,因为我们不能用一个列表去除一个浮点。numpy这样的外部库可以通过增加矢量化数学操作的方式帮助我们解决这个问题。

另外,Python抽象还影响了任何需要为下一次计算保存L1/L2缓存中相关数据的优化。这有很多原因,首先是Python对象不再是内存中最优化的布局。这是因为Python是一种垃圾收集语言——内存会被自动分配并在需要时释放。这会导致内存碎片并影响向CPU缓存的传输。另外,我们也没有任何机会去直接改变数据结构在内存中的布局,这意味着总线上的一次传输可能并不包含一次计算的所有相关信息,即使这些信息少于总线带宽。

第二个问题更加基本,根源是Python的动态类型以及Python并不是一门编译性的语言。很多C语言开发者已经在多年开发过程中意识到,编译器总是比你聪明。当编译静态代码时,编译器可以做很多的事情来改变对象的内存布局以及让CPU运行某些指令来优化它们。然而,Python并不是一种编译性的语言:更糟的是,它还具有动态类型,意味着任何算法上可能的优化机会都会更加难以实现,因为代码的功能有可能在运行时被改变。有很多方法可以缓解这一问题,其中最主要的一个方法就是使用Cython,它可以将Python代码进行编译并允许用户“提示”编译器代码究竟有多“动态”。

最后,之前提到的GIL会影响并行代码的性能。比如,假设我们改变代码来使用多个CPU核心,每个核心收到一堆数字,取值范围是2到sqrtN。每个核心可以对自身收到的数据进行计算,当它们都完成时可以互相进行比较。这看上去是一个好方案,虽然我们失去了提前中止循环的能力,但是随着我们使用的核心数的增加,每个核心需要进行的检查数降低了(例如,如果我们有M个核心,每个核心只需要进行sqrtN/M次检查)。然而,由于GIL,一次仅有一个核心可以被使用。这意味着我们还是以非并行的方式运行这段代码,而且还不能提前中止。我们可以使用多进程(multiprocessing模块)而不是多线程,或者使用Cython或外部函数来避免这个问题。

Python具有高度的表现力且容易上手——新开发者会很快发现他们可以在很短时间里做到很多。许多Python库包含了用其他语言编写的工具,使Python可以轻易调用其他系统。比如,scikit-learn机器学习系统包含了LIBLINEAR和LIBSVM(两者皆以C语言写成),numpy库则包含了BLAS以及其他用C和Fortran语言写的库。因此,正确运用这些库的Python代码确实可以在速度上做到跟C媲美。

Python被誉为“内含电池”,因为它内建了很多重要且稳定的库。包括:

unicodebytes

语言核心内建。

array

原始类型的高效数组。

math

基本数学操作,包括一些简单的统计数学。

sqlite3

包含了流行的基于文件的SQL引擎SQLite3。

collections

多种对象,包括双向队列、计数器和字典的变种。

除了这些语言核心库,还有大量的外部库,包括:

numpy

一个Python数字库(矩阵运算的基石库)。

scipy

大量可信的科学库的集合,通常包含了广受尊重的C和Fortran库。

pandas

一个数据分析库,类似于R语言的数据框或Excel表格,基于scipynumpy

scikit-learn

正在快速成为默认的机器学习库,基于scipy

biopython

一个生物信息学库,类似于bioperl

tornado

一个提供了并发机制的库。

各类数据库封装

为了跟基本上所有的数据库通信,包括Redis、MongoDB、HDF5以及SQL。

各类网站开发框架

用于创建网站的各种高性能系统,如djangopyramidflasktornado

OpenCV

计算机视觉的封装。

各类API封装

用于轻松访问各种时髦的web API如Google、Twitter和LinkedIn。

为了适应各种部署环境,还有大量可选的管理环境和shell,包括:

Python的一大优势在于它可以快速实现出一个新主意的原型。由于存在各种支持库,它能够轻易测试出一个主意是否可行,哪怕第一个实现可能是磕磕碰碰做出来的。

如果你想要让你的数学函数更快,看看numpy。如果你想要实验一下机器学习,试试scikit-learn。如果你在清理和操作数据,那么pandas是一个好选择。

总的来说,我们有理由提出这样一个问题,“为了让我们的系统跑得更快而进行的优化从长期来看会不会反而导致我们团队整体跑得更慢了?”只要花费足够的人力,系统总是可以被榨出更多的性能,但这可能导致系统脆弱的可维护性以及难以理解的优化并最终导致整个团队绊倒在地。

Cython就是一个例子(7.6节),它将Python代码注释成类似C语言的类型,被转化后的代码可以被一个C编译器编译。它在速度上的提升令人惊叹(相对较少的努力就能获得C语言的速度),但后续代码的维护成本也会上升。尤其是,对这个新模块可能更难,因为团队成员需要具备一定的编程能力来理解那些为了性能提升而绕开Python虚拟机的折衷。

[1] 不要跟进程间通信(也是IPC)混淆——我们会在第9章讨论这个主题。


相关图书

Python极客项目编程(第2版)
Python极客项目编程(第2版)
动手学自然语言处理
动手学自然语言处理
Python财务应用编程
Python财务应用编程
深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
Web应用安全
Web应用安全
Python+ChatGPT办公自动化实战
Python+ChatGPT办公自动化实战

相关文章

相关课程