书名:Python高性能编程
ISBN:978-7-115-59947-6
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美]米夏•戈雷利克(Micha Gorelick)
[美]伊恩•欧日沃尔德(Ian Ozsvald)
译 张海龙
责任编辑 武晓燕
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e59947”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
Python语言是一种脚本语言,应用领域非常广泛,包括数据分析、自然语言处理、机器学习、科学计算、推荐系统构建等。
本书共有12章,围绕如何进行代码优化和加快实际应用程序的运行速度进行讲解,还介绍了如何解决CPU密集型问题、数据传输和内存密集型问题,如何通过移动数据、PyPy即时编译器和异步I/O提升性能。本书主要包括以下内容:计算机原理、列表和元组、字典和集合、迭代器、Python模块、并发性、集群计算等。最后,本书通过一系列真实案例展现了在应用场景中使用Python时需要注意的问题。
本书适合中级和高级Python程序员,以及有一定Python语言基础想要得到进阶和提高的读者阅读。
O’Reilly以“分享创新知识、改变世界”为己任。40多年来我们一直向企业、个人提供成功所必需之技能及思想,激励他们创新并做得更好。
O’Reilly业务的核心是独特的专家及创新者网络,众多专家及创新者通过我们分享知识。我们的在线学习(Online Learning)平台提供独家的直播培训、图书及视频,使客户更容易获取业务成功所需的专业知识。几十年来,O’Reilly图书一直被视为学习开创未来之技术的权威资料。我们每年举办的诸多会议是活跃的技术聚会场所,来自各领域的专业人士在此建立联系,讨论最佳实践并发现可能影响技术行业未来的新趋势。
我们的客户渴望做出推动世界前进的创新之举,我们希望能助他们一臂之力。
“O’Reilly Radar博客有口皆碑。”
——Wired
“O’Reilly凭借一系列非凡想法(真希望当初我也想到了)建立了数百万美元的业务。”
——Business 2.0
“O’Reilly Conference是聚集关键思想领袖的绝对典范。”
——CRN
“一本O’Reilly的书就代表一个有用、有前途、需要学习的主题。”
——Irish Times
“Tim是位特立独行的商人,他不光放眼于最长远、最广阔的领域,并且切实地按照Yogi Berra的建议去做了:‘如果你在路上遇到岔路口,那就走小路。’回顾过去,Tim似乎每一次都选择了小路,而且有几次都是一闪即逝的机会,尽管大路也不错。”
——Linux Journal
说到高性能计算,你想到的可能是巨型集群,它们模拟复杂的天气现象,或者试图理解收集到的关于遥远星球的数据中隐藏的信号。大家很容易错误地认为,只有构建专用系统的人才需要考虑代码的性能特征。只要拿起本书,你便在学习编写高性能代码所需理论和实践的道路上迈出了第一步。掌握如何构建高性能系统,对每位程序员都有益。
显然,如果不能编写高性能代码,那么在开发某些应用程序时你将寸步难行。如果你正在开发这样的应用程序,那么本书就是为你而写的,但从本书受益的应用程序远不止这些。
我们通常认为,新技术是驱动创新的动力,而我对能够将技术使用门槛降低多个数量级的方式情有独钟。一项技术的时间或计算成本降低为原来的1/10后,其应用范围将突然之间大到超乎想象。
我在十多年前的工作中首次意识到了这一点。当时我在一家社交媒体公司工作,通过分析若干太字节的数据来确定社交媒体用户单击小猫图片的次数多还是单击小狗图片的次数多。
当然是单击小狗图片的次数多,小猫不过是浪得虚名而已。
在那个时候,这种使用计算时间和基础设施的方式简直是离经叛道!通过使用原本只用于高价值场景的技术(如欺诈检测)来解决看似微不足道的问题,打开了我通往新世界的大门。通过从这些实验中学到的知识,我可以构建一系列全新的搜索产品和内容提供产品。
举一个你可能会遇到的例子:能够识别监控视频镜头中意料之外的动物或人物的机器学习系统。如果这个系统的性能足够高,就可以将其嵌入相机中以加强隐私;而在云端运行它时,其消耗的计算能力可以极大地减少,从而可以降低运营成本。你可以将节省的资源用于解决其他问题,进而构建更有价值的系统。
人人都希望自己创建的系统卓有成效、易于理解、性能出色,可惜通常只能从这三者中选择两个甚至一个!本书正是为那些不想放弃其中任何一个的人编写的。
相比于其他介绍相同主题的图书,本书有3个特色。首先,它是为编写代码的人而写的。你将获得所有相关的背景知识,进而明白做出特定选择背后的原因。其次,对于与背景知识相关的理论,Gorelick和Ozsvald做出了出色的诠释。最后,在本书中,你将了解到当今非常有用的性能优化库的独特之处。
能够改变你编程思维的图书为数不多,本书是其中之一。我将本书赠送给了很多可以从中受益的人。无论你使用的是哪种语言和编程环境,本书探讨的理念都将助你成为更出色的程序员。
愿你有一个愉快的探险之旅。
Accel驻企数据科学家Hilary Mason
Python学起来很容易。你阅读本书很可能是想提高能够正确运行的代码的速度。Python代码修改起来很容易,可以让你快速调整想法,对此你甚是满意。然而我们必须要在易于修改和尽可能快速地运行之间进行取舍,这个现象世人皆知,常令人扼腕叹息。但这种矛盾并非是不可调和的。
有人要加快串行进程的运行速度;有人手头的问题可以利用多核架构、集群或图形处理单元;有人要求系统是可扩展的,能够在时机和资金允许的情况下处理更多的工作,同时保证可靠性;还有人借鉴了其他语言的编程方法。
本书涵盖了上述所有主题,为你找出瓶颈并开发出速度更快、可扩展性更强的解决方案提供了实用指南。本书还讲述了一些前辈的实战故事,旨在帮助你少走弯路。
Python非常适用于快速开发、生产环境部署和可扩展的系统。在Python生态系统中,有很多人正在为提高Python的可扩展性而奋斗,这让你能够将更多的时间放在更具挑战性的工作上。
如果使用Python的时间足够长,便知道为何有些代码运行缓慢,还知道可以使用Cython、NumPy和PyPy等技术来提速。你还可能有其他语言的编程经验,知道解决性能问题的方式有多种。
本书主要介绍如何解决CPU密集型问题,但也涉及数据传输和内存密集型问题。科学家、工程师、定量分析专家和学者经常会面临这些问题。
本书还介绍了Web开发人员可能面临的问题,如何通过移动数据、使用PyPy等即时编译器以及异步I/O轻松地提升性能。
具备C(C++或Java)方面的知识可能会对阅读本书有所帮助,但这并非必要条件。CPython是最常见的Python解释器(在命令行输入python
时使用的就是这个标准解释器),它是使用C语言编写的,因此各种钩子和库都完全暴露了C语言的内部机制。本书介绍的很多优化方法都不要求你具备任何C语言知识。
另外,本书也不要求你对CPU、存储架构和数据总线有深入认识。
本书面向中高级Python程序员。一部分初级Python程序员虽然可能看懂,但还是建议牢固地掌握Python后再来阅读。
本书不涉及存储系统优化。如果你遇到的是SQL或NoSQL瓶颈,本书可能帮不上忙。
多年来,本书作者始终奋战在工业和学术领域的海量数据处理前线,面对的诉求都是更快地得到答案、使用可扩展的架构。我们将竭力传授来之不易的经验,使你少走弯路。
每章开头都列出了该章将要回答的问题(如果没有,请告诉我们,以便下次修订时改正)。
本书涵盖如下主题。
● 计算机原理:旨在让你知道幕后发生的情况。
● 列表和元组:这两个基本数据结构在语义和速度方面的细微差别。
● 字典和集合:这两个重要数据结构的内存分配策略和访问算法。
● 迭代器:如何使用迭代以更符合Python风格的方式编写代码并打开通往无穷数据流的大门。
● 纯Python方法:如何卓有成效地使用Python及其模块。
● NumPy:如何随心所欲地使用深受大家喜爱的NumPy库。
● 编译和即时计算:通过将代码编译成机器代码提高处理速度,并根据剖析结果行事。
● 并发性:高效地移动数据。
● multiprocessing
:使用内置库multiprocessing
执行并行计算和高效地共享NumPy库的各种方法,以及进程间通信(InterProcess Communication,IPC)的代价和好处。
● 集群计算:对研究系统和生产系统中的多进程(multiprocessing)代码进行转换,使其能够在本地或远程集群中运行。
● 减少内存占用量:在不购买大型计算机的情况下,解决数据规模庞大的问题。
● 实战经验教训:以实战故事的方式介绍经验教训,以免你重蹈覆辙。
2020年,Python 3已取代Python 2.7成为Python标准版。如果你还在使用Python 2.7,那你就做错了——很多库都不再支持Python 2.7,随着时间的推移,支持Python 2.7的代价越来越高。请给社区一个面子,赶快迁移到Python 3,并在所有新项目中都务必使用Python 3。
本书使用的是64位Python。虽然也可以使用32位的Python,但它在科学领域很少见。使用32位的Python时,所有的库都能正常运行,但数值精度可能不同,因为它取决于存储数字时使用的位数。在科学领域,主要使用的是64位Python和*nix环境(通常是Linux或Mac)。64位让你能够对更大的内存空间编址,而*nix让你开发的应用程序能够以大家熟悉的方式部署和配置,且其行为也是大家熟悉的。
如果你是Windows用户,务必系上“安全带”。本书介绍的大部分内容也适用于Windows系统,但有些因操作系统而异,你必须自己去研究Windows解决方案。Windows用户面临的最大麻烦是模块的安装:在Stack Overflow等网站中,应该能够找到解决方案。如果你使用的是Windows系统,可以创建一个虚拟机(使用VirtualBox),并在其中安装Linux,这将让你能够更自由地试验。
Windows用户绝对应该考虑使用打包好的解决方案,如Anaconda、Canopy、Python(x,y)或Sage。对于Linux和Mac用户,这些发行版也可以让他们的工作轻松得多。
如果你是从Python 2.7升级到Python 3的用户,可能不知道如下重要变化。
● /在Python 2.7中表示整数除法,但在Python 3中表示执行浮点数除法运算。
● str
和unicode
在Python 2.7中用于表示文本数据,但在Python 3中,只有str
,因为所有的字符串都是Unicode。清晰起见,我们用类型bytes
来表示未编码的字节序列。
如果你正在升级代码,有两个不错的指南值得参考,它们是“Porting Python 2 Code to Python 3”和“Supporting Python 3: An in-depth guide”。使用诸如Anaconda和Canopy等的发行版,可以同时运行Python 2和Python 3,这可简化移植工作。
本书采用的许可方式为知识共享(保留署名—非商业用途—禁止修改)。
欢迎你将本书用于包括非商业教学在内的非商业用途。根据前述许可方式,只能完全复制。如果要部分复制,请与O’Reilly出版社联系。请按后面介绍的方式保留署名。
经过协商,本书采用知识共享许可方式,旨在让其内容能够更广泛地传播。如果这个决定对你有所帮助,我们将深感欣慰,O’Reilly出版社的员工也会有同感。
知识共享许可方式要求你在使用本书部分内容时保留署名。所谓保留署名,意思是说应让其他人能够找到本书。为此,这样做就行:“High Performance Python, 2nd ed.,by Micha Gorelick and Ian Ozsvald(O’Reilly). Copy right 2020 Micha Gorelick and Ian Ozsvald,978-1-492-05502-0.”
我们鼓励你在Amazon等公共网站上评论本书,帮助他人了解是否能从本书受益。你也可以给我们发电子邮件,邮件地址为errata@oreilly.com.cn。
期待你指出本书的错误、书中帮助你获得成功的用例以及下一版应涵盖的性能提升方法。为此,可访问O’Reilly网站中关于本书的页面。
该图标表示提示、建议或值得考虑的问题。
该图标表示一般性说明。
该图标表示警告或提醒。
本书的补充材料(示例代码、练习等)可从异步社区下载。
如果你有技术问题或在使用示例代码时遇到问题,可将邮件发送到bookquestions@oreilly.com。
本书是为帮助你完成工作而写的。一般而言,对于本书提供的示例代码,可以将其用于程序或文档中。除非复制了很大一部分代码,否则无须与我们联系以获得许可。例如,编写程序时,如果使用了本书的几段代码,无须获得许可。销售或分发O’Reilly出版社出版的图书中的示例代码时,必须获得许可。引用本书内容或示例代码以回答问题时,无须获得许可。在产品文档中嵌入本书的大量示例代码时,必须获得许可。
如果你觉得你使用示例代码的方式不属于合理使用或不在前述许可范围内,请通过permissions@oreilly.com与我们取得联系。
关于本书的技术性问题或建议,请发邮件到:errata@oreilly.com.cn。
欢迎登录我们的网站,查看更多我们的图书、课程、会议和最新动态等信息。
感谢Hilary Mason为本书作序,让本书的开场白精彩绝伦;感谢Giles Weaver和Dimitri Denisjonok做出宝贵的技术反馈。
感谢Patrick Cooper、Kyran Dale、Dan Foreman-Mackey、Calvin Giles、Brian Granger、Jamie Matthews、John Montgomery、Christian Schou Oxvig、Matt “snakes” Reiferson、Balthazar Rouberol、Michael Skirpan、Luke Underwood、Jake Vanderplas和William Winter宝贵的反馈和贡献。
与O’Reilly出版社的编辑合作非常愉快,如果你想写书,强烈建议你与他们谈谈。
本书第12章的撰稿人非常慷慨地分享了他们来之不易的经验教训。感谢本版新增内容的撰稿人Soledad Galli、Linda Uruchurtu、Vanentin Haenel和Vincent D. Warmerdam,同时感谢Ben Jackson、Radim Řehůřek、Sebastjan Trepca、Alex Kelly、Marko Tasic和Andrew Godwin抽出宝贵的时间和精力为本书第1版撰稿。
Ian要感谢妻子Emily允许自己为编写本书又消失8个月,Emily真是太善解人意了;同时要向他的小狗道歉,为自己只顾坐下来写作,没能像它希望的那样常带它去森林里遛遛说声抱歉。
Micha要感谢Marion、其他所有的朋友以及家人,感谢他们在自己学习以及写作期间如此地有耐心。
本书由异步社区出品,社区(https://www.epubit.com)为您提供后续服务。
本书提供如下资源:
● 代码;
● 彩图文件。
要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者、译者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“发表勘误”,输入错误信息,单击“提交勘误”按钮即可,如下图所示。本书的作者和编辑会对您提交的错误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区投稿(直接访问www.epubit.com/contribute即可)。
如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
异步社区
微信服务号
阅读本章后,你将能够回答如下问题。
● 计算机架构由哪些部件组成?
● 常见的计算机架构有哪些?
● Python是如何对底层计算机架构进行抽象的?
● 编写高性能Python代码时会遇到哪些障碍?
● 哪些策略可助你成为高性能程序员?
可将计算机编程视为以特定方式移动和转换数据,以获得特定结果。这些操作需要时间来完成,因此可将高性能编程视为最大限度地缩短操作所需的时间,为此要么降低开销(如编写效率更高的代码),要么改变操作方式,让每个操作的作用更大(如找到更合适的算法)。
这里将重点放在降低开销上,为此需要更深入地了解数据在其中移动的硬件。这种做法好像徒劳无益,因为Python竭尽全力地避免了你与硬件直接交互。但是,使用Python进行编程时,如果知道在硬件中移动数据的最佳方式,同时知道Python所做的抽象迫使你必须以什么样的方式移动数据,这将对编写高性能程序大有裨益。
计算机的底层部件可简化为3个基本部分:计算单元、存储单元以及它们之间的连接。这些单元有各种不同的属性,可帮助我们认识它们。计算单元的一个属性是每秒能执行多少次计算;存储单元的属性包括可存储多少数据以及读写数据的速度;连接的一个属性是,通过它能够以多快的速度将数据从一个地方移到另一个地方。
通过这些基本单元,我们可以在多个复杂级别上讨论标准工作站。例如,可认为标准工作站有一个充当计算单元的中央处理器(Central Processing Unit,CPU),它连接到两个独立的存储单元——随机存取存储器(Random Access Memory,RAM)和硬盘(它们的容量和读写速度不同),而所有这些单元都是通过一条总线连接起来的。然而,也可以更详细地了解CPU本身的几个存储单元——L1、L2乃至L3和L4缓存,它们的容量很小(从几KB到十多MB不等),但速度非常快。另外,新型计算机架构通常采用了新配置,例如,Intel SkyLake CPU使用的是快速信道互联(Ultra Path Interconnect),而不是前端总线,还重构了众多其他的连接。最后,在这两种有关工作站的近似描述中,都省略了网络连接;网络连接的速度很慢,可用于连接到众多其他的计算单元和存储单元。
为帮助你理清头绪,下面简要地描述一下这些基本单元。
计算单元是计算机的核心,它能够对数据进行转换,还能改变当前进程的状态。最常见的计算单元是CPU,但图形处理单元(Graphic Processing Unit,GPU)正越来越多地用作辅助计算单元。GPU最初用于提高计算机图形的处理速度,但凭借其内在的并行特性,它们越来越适合用于数值应用程序;所谓并行,指的是支持同时执行众多计算。无论是哪种类型的计算单元,它们都接收一系列位(如表示数字的位),并输出另一组位(如表示数字之和的位)。计算单元可对整数和实数执行算术运算、对二进制数执行按位运算,有些计算单元还可执行特殊的运算,如加法和乘法混合运算:接收3个数字A
、B
和C
,并返回A*B+C
的值。
计算单元的主要属性有两个:一个周期内可执行多少次计算;每秒可完成多少个周期。其中第一个属性的度量指标为指令数/周期(Instruction Per Cycle,IPC)[1],而第二个属性的度量指标为时钟速度。设计新的计算单元时,这两个指标会相互制约。例如,Intel Core系列的IPC很高,但时钟速度较低,而Pentium 4芯片则相反。GPU的IPC和时钟速度都很高,但它们存在其他问题,如通信速度慢,这将在1.1.3节讨论。
[1] 第9章将讨论的进程间通信(InterProcess Communication)的缩略语也是IPC,请不要将它们混为一谈。
另外,尽管通过提高时钟速度几乎可以立即提高计算单元上运行的所有程序的速度(因为这让程序能够在单位时间内执行更多的计算),但提高IPC也可改善向量化水平,从而极大地影响计算。向量化(vectorization)发生在一次给CPU提供多项数据,并且它能够同时对这些数据执行运算的时候。这种CPU指令被称为单指令多数据(Single Instruction Multiple Data,SIMD)。
在过去的10年,计算单元的发展速度极其缓慢,如图1-1所示。时钟速度和IPC都停滞不前,这是因为晶体管的尺寸已接近物理极限。有鉴于此,芯片制造商已转而采用其他方法来提高速度,包括多线程技术(同时执行多个线程)、更巧妙的乱序执行以及多核架构。
图1-1 CPU时钟频率变化趋势(摘自CPU DB)
超线程技术让主机操作系统(Operating System,OS)以为有另一个CPU;更巧妙的硬件逻辑试图在单个CPU中交替地执行两个指令线程。如果成功,在单线程上最多可以将速度提高30%。通常,如果两个线程的工作分布在不同的执行单元上(如一个执行浮点数运算,另一个执行整数运算),这样做的效果非常好。
乱序执行让编译器找出程序中彼此独立的部分,进而以任何顺序或同时执行它们。只要能在正确的时间提供中间结果,程序就能往下正确地执行,哪怕没有按程序指定的顺序提供这些结果。这使得在有些指令被阻塞(如等待内存访问)时,可执行其他指令,从而提高可用资源的利用率。
最后是多核架构的普及,这对高级程序员来说也是最重要的一点。这种架构包含多个CPU,无须突破单CPU的速度壁垒就能提高总体计算能力。这就是当前很难找到少于双核的计算机的原因所在(所谓双核计算机,就是有两个彼此相连的计算单元)。这虽然增加了每秒可执行的运算数,但也增加了代码编写工作的难度。
根据阿姆达尔定律,给CPU增加核心并不一定能缩短程序的执行时间。简单地说,阿姆达尔定律是这样的:对于在多核上运行的程序,如果其中有些子程序必须在同一个核上运行,将对速度提升带来限制,使得速度提升到一定程度后,即便再增加核心,也无法进一步提高速度。
例如,假设有项调查,需要调查100人,而调查每个人需要1分钟才能完成。假设只有一位调查员,则完成这项调查任务将需要100分钟(这位调查员向第1位被调查者提问并等待回答,再转向第2位被调查者)。这种由一位调查员提问并等待回答的方法类似于串行处理;在串行处理中,每次只能执行一项操作,每项操作都需要等待前一项操作执行完毕后再执行。
如果有两位调查员,就可以同时进行调查,整个调查工作只需50分钟就能完成。这是因为每位调查员都无须了解另一位调查员的调查情况,因此可将任务分成两个彼此独立的部分。
增加调查员可进一步提高速度。当调查员数量增加到100时,整个调查过程只需1分钟就能完成,这是被调查者回答问题所需的时间。如果此时再增加调查员,并不能进一步缩短调查时间,因为新增的调查员将无所事事——所有的被调查者都在接受调查。在这种情况下,要缩短整个调查时间,唯一的办法是缩短调查单人所需的时间,即完成问题的串行部分所需的时间。同样,对于CPU,可在必要时增加执行计算的核心,但增加到一定程度后将出现瓶颈,这个瓶颈就是特定核心完成其任务所需的时间。换言之,在并行计算中,瓶颈是由必须串行执行的子任务决定的。
另外,在Python中利用多核将面临的一个主要障碍是,Python使用了全局解释器锁(Global Interpreter Lock,GIL)。GIL确保Python进程每次只运行一条指令,而不管当前使用了多少个核心。这意味着虽然有些Python代码能够同时访问多个核心,但在任何时点,都只有一个核心在运行Python指令。如果以前面的调查为例,这意味着即便有100名调查员,任何给定时点也只有一位调查员在提问并等待回答,这相当于让多位调查员带来的好处消失殆尽。这看起来是个巨大的障碍,如果考虑到当前的趋势是使用多个计算单元而不是单个速度更快的计算单元,这个问题就更严重了。采用如下方法可避免这个问题:使用诸如multiprocessing
等标准库工具(第9章);使用诸如numpy
、numexpr
等技术(第6章);使用Cython(第7章);使用分布式计算模型(第10章)。
注意:Python 3.2对GIL做了重大修改,使其更灵活,从而缓解了众多与单线程性能相关的问题。虽然GIL现在依然对Python进行限制,使其每次只能运行一条指令,但在指令切换方面它做得更好,且开销更低。
计算机的存储单元用于存储位,这些位表示的可能是程序中的变量,也可能是图像像素。存储单元包括主板上的寄存器、RAM和硬盘,这些存储单元的主要不同之处在于读写数据的速度。读写速度严重依赖于读写方式。
例如,对大多数存储单元来说,读取一大块数据时,性能要比读取大量小块数据高得多,这两种读取方式分别称为顺序读取和随机读取。如果将数据视为书本中的书页,这意味着对大多数存储单元来说,连续翻页的速度要比随机翻页快。所有存储单元的顺序读取速度都快于随机读取,但在不同的存储单元中,这两种读取方式的速度有天壤之别。
除了读写速度,存储单元的另一个属性是延迟,这可用查找数据所需的时间来表征。旋转式硬盘的延迟可能很长,因为需要让磁盘旋转起来,并将读写头移到合适的位置;而RAM的延迟可能很短,因为一切都是固态的。下面按读写速度从低到高的顺序简要地介绍一下标准工作站中常见的存储单元[2]。
[2] 本节的速度数据摘自O’Reilly网站。
旋转硬盘属于永久性存储,即便计算机关机也不会丢失。它的读写速度通常较慢,因为必须旋转磁盘并移动读写头。随机存取时性能将下降,但容量非常大(数十TB)。
固态硬盘类似于旋转硬盘,但读写速度更快,容量更小(数TB)。
RAM用于存储应用程序的代码和数据(如应用程序中所有的变量)。读写速度较快,随机存取的性能也不错,但通常容量有限(64 GB左右)。
读写速度非常快。进入CPU的数据必须经过这些缓存。容量很小(数MB)。
图1-2说明了这些存储单元的特征。
一个显而易见的规律是,读写速度和容量成反比:速度越快,容量越小。有鉴于此,很多系统都实现了分层存储:所有数据都存储在硬盘中,部分数据进入RAM,更少的部分数据进入L1/L2缓存。这种分层方法让程序能够根据存取速度需求将数据放在不同的地方。优化程序的数据存储模式时,优化的是数据的存储位置和布局(旨在增加顺序读取的次数)以及在不同存储单元之间移动数据的次数。另外,异步I/O和抢占式缓存(preemptive caching)可确保数据位于合适的地方,从而避免浪费计算时间,这是因为这些过程是独立于计算的。
图1-2 各种存储单元的特征值(2014年2月的数据)
最后,我们来看看这些基本部件是如何相互通信的。通信模式有很多,但都是总线的变种。
例如,前端总线是RAM和L1/L2缓存之间的连接,它将准备就绪的数据移到集结场所,供处理器进行转换,并将计算结果移出。还有其他的总线,如外部总线,它是硬件设备(如硬盘和网卡)到CPU和系统内存的主要通道。外部总线的速度通常比前端总线慢。
实际上,L1/L2缓存带来的很多好处都要归功于速度更快的总线。正是因为能够在慢速总线(RAM到缓存的总线)以大块的方式将计算所需的数据排队,再通过从缓存到CPU的总线以非常快的速度提供数据,才让CPU无须等待太长时间,从而能够执行更多的计算。
同样,使用GPU存在的众多缺点也是由其连接的总线导致的:GPU通常是外部设备,通过PCI总线进行通信,而这种总线的速度比前端总线慢得多,因此将数据移入和移出GPU可能是一项开销高昂的操作。有鉴于此,异构计算(前端总线连接CPU和GPU)应运而生,它旨在降低数据传输开销,让GPU计算变得可行,即便在需要传输大量数据的情况下亦如此。
除了计算机内部的通信部件,另一种通信部件是网络设备,它比前面讨论的通信部件更灵活,可连接到存储设备,如网络连接存储(Network Attached Storage,NAS)设备,还可连接到其他计算部件,如集群中的计算节点。网络通信的速度通常比前述其他通信方式慢得多:前端总线每秒可传输几十吉比特,而网络每秒只能传输几十兆比特。
显然,总线的主要属性是速度:在给定时间内可传输多少数据。这个属性由两个数值表征:一次能传输多少数据(总线宽度);每秒能传输多少次(总线频率)。需要指出的是,一次传输的数据总是串行的:从存储单元中读取一个数据块,并将其移到另一个地方。之所以用两个数值来表征总线速度,是因为它们影响的计算方面不同:总线宽度很大时,可一次性传输所有相关的数据,这对向量化代码(以及从存储单元中顺序读取数据的代码)大有裨益,而对于需要从存储单元中随机读取数据的代码来说,低总线宽度和高频率大有裨益。有趣的是,为改变这些属性,计算机设计人员采用的方法之一是调整主板的物理布局:芯片离得越近,用于连接它们的导线就越短,而这有助于提高传输速度。另外,导线数量越多,总线宽度越大(从物理上说,总线就越宽)。
鉴于可根据应用程序的性能要求调整接口,因此接口类型成百上千。图1-3显示了一些常见接口的速度。需要注意的是,该图根本没有提及连接的延迟——对数据请求做出响应的时间。虽然延迟随计算机而异,但每种接口类型都存在固有的基本延迟。
图1-3 各种常见接口的连接速度[3]
[3] 这里的数据摘自O’Reilly网站。
要完全明白高性能编程面临的问题,仅熟悉计算机的基本部件还不够,还需知道它们如何相互影响以及如何协同工作来解决问题。本节将探索一些简单问题,看看它们的理想解决方案是如何工作的,以及Python又是如何解决它们的。
本节可能让你感到绝望,因为它好像主要是说Python无法应对性能方面的问题。但实际情况并非如此,其原因有两个。首先,这里讨论高性能计算时,忽略了一个重要的因素——开发人员。Python本身的性能虽然不高,但使用它可快速开发程序,这就弥补了这种缺陷。其次,通过利用本书后面将介绍的模块和理念,可轻松地消除这里介绍的众多问题。因此,使用Python可快速开发程序,同时规避众多性能约束。
为了更深入地理解高性能编程的因素,来看一段简单的代码,它判断一个数是否是素数:
import math
def check_prime(number):
sqrt_number = math.sqrt(number)
for i in range(2, int(sqrt_number) + 1):
if (number / i).is_integer():
return False
return True
print(f"check_prime(10,000,000) = {check_prime(10_000_000)}")
# check_prime(10,000,000) = False
print(f"check_prime(10,000,019) = {check_prime(10_000_019)}")
# check_prime(10,000,019) = True
我们先根据抽象计算模型分析这段代码,再将其与Python运行这段代码的情况进行比较。与其他抽象一样,这里也将忽略理想计算模型和Python运行这些代码时涉及的众多细节。在解决问题前,先像这里这样做通常都是一个不错的主意:想想算法的基本组成部分以及最佳的解决方案是什么样的。知道理想情况以及Python中的实际情况后,就可不断调整Python代码,使其更接近最优状态。
在上述代码中,首先将number
的值存储到了RAM中。为计算sqrt_number
,需要将number
的值传递给CPU。在理想情况下,只需传递这个值一次:它将存储在CPU的L1/L2缓存中,而CPU将执行计算并将结果传回给RAM进行存储。这是最理想的情况,最大限度地减少了从RAM中读取number
值的次数,转而从L1/L2缓存中读取这个值,而这样做的速度要快得多。另外,通过使用与CPU直接相连的L1/L2缓存,最大限度地减少了通过前端总线传输数据的次数。
提示:对优化来说,确保数据在合适的地方并尽可能少地移动它们至关重要。所谓“繁重的数据”指的是花费大量时间和精力来移动数据,这是需要避免的。
对于上述代码的循环部分,我们希望一次性将number
和多个i
值传递给CPU,而不是每次传递一个i
值。这是因为CPU能够向量化操作,且不会增加时间开销,换句话说,CPU能够同时执行多项独立的计算。因此,我们希望将number
传递给CPU缓存,并根据缓存的容量传递尽可能多的i
值。对于所有number
和i
值组合,都将它们相除并检查结果是否是整数,再返回一个信号,指出是否有结果是整数。如果有结果是整数,整个函数就到此结束;如果没有,就重复前述过程。通过这样做,可针对多个i
值返回一个结果,避免了通过速度缓慢的总线返回每个i
值的结果。这里利用了CPU的向量化功能,即在一个时钟周期内对多项数据执行同一条指令。
下面的代码演示了向量化的概念:
import math
def check_prime(number):
sqrt_number = math.sqrt(number)
numbers = range(2, int(sqrt_number)+1)
for i in range(0, len(numbers), 5):
# the following line is not valid Python code
result = (number / numbers[i:(i + 5)]).is_integer()
if any(result):
return False
return True
这里对流程做了设置,使得每次循环根据5个i
值执行除法运算并检查是否有结果为整数。如果正确地进行了向量化,CPU就能一步执行完整行代码,而无须针对每个i
值分别进行计算。理想情况下,CPU可独立完成操作any(result)
,而无须将结果传回给RAM。第6章将更详细地介绍向量化,包括其工作原理以及在什么情况下使用它可提高代码的性能。
Python解释器做了大量的工作,力图对程序员隐藏它使用的计算部件。这让程序员根本不用考虑如下问题:如何给数组分配内存、如何组织这些内存以及其中的数据是以什么样的顺序发送给CPU的。这是Python的一个优势,让程序员能够专注于要实现的算法,但付出的代价是性能可能急剧下降。
需要指出的是,Python运行的确实是经过极度优化的指令,但你需要掌握一些诀窍,让Python以正确的顺序执行这些指令,以进一步提高性能。例如,在下面的示例中,search_fast
的速度比search_slow
快,这很容易判断出来,这是因为虽然这两个函数的运行时间都是O(n),但search_fast
通过提前结束循环避免了多余的计算。然而,在涉及派生类型、特殊的Python方法或第三方模块时,情况可能更复杂。例如,对于下面的函数search_unknown1
和search_unknown2
,你能迅速判断出哪个的速度更快吗?
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
def search_unknown1(haystack, needle):
return any((item == needle for item in haystack))
def search_unknown2(haystack, needle):
return any([item == needle for item in haystack])
前面演示的是找出无用操作并将其删除,与之类似的是通过剖析找出速度缓慢的代码,并寻找效率更高的计算方式。虽然最终的结果是一样的,但通过这样做,可极大地减少计算次数和数据传输次数。
前述抽象层带来的影响之一是,无法直接利用向量化。在前述判断素数的函数中,对于每个i
值都将运行一次循环迭代,而不会将多个迭代合并。如果你再看看前述向量化示例,将发现它并非合法的Python代码,因为在Python中,不能将浮点数与列表相除。在这种情况下,诸如numpy
等外部库可提供帮助,它让你能够执行向量化数学运算。
另外,Python所做的抽象还会影响这样的优化,即它依赖于将相关的数据保留在L1/L2缓存中,以供下一次计算时使用。导致这种结果的原因很多。首先,Python对象在内存中并不是以最优方式排列的。这是因为Python是一种垃圾收集语言——根据需要自动分配和释放内存。这会导致内存碎片,进而可能影响将数据传输到CPU缓存的过程。与此同时,你无法直接调整数据结构在内存中的排列,这意味着即便与特定计算相关的数据量低于总线宽度,也可能无法通过总线一次性传输它们[4]。
[4] 第6章将演示如何重获这种控制权,进而对代码进行优化,细致到其内存使用模式。
其次,Python不是编译型语言,且其使用的类型是动态的。凭借多年的经验,很多C语言程序员都发现,编译器通常比自己聪明。编译静态代码时,编译器能够巧妙地调整布局以及CPU运行指令的方式,从而对代码进行优化。Python不是编译型语言,雪上加霜的是,其类型是动态的,这意味着根据算法推断出可能的优化机会要难得多,因为在运行期间,代码的功能可能发生变化。缓解这种问题的途径有很多,其中居首的是使用Cython,它让Python代码能够被编译,还让开发人员能够将代码的动态程度告知编译器。
最后,前面提到的GIL也可能影响并行代码的性能。例如,假设为利用多个CPU核心对前述代码进行修改,让每个核心处理2~sqrtN
的一个子范围。每个核心都可独立地处理分配给它的子范围,再在处理完毕后比较结果。虽然这样做会失去提前结束循环的好处(因为每个核心都不知道其他核心的处理结果),但可减少缩小每个核心需要处理的数字范围(如果有M
个核心,则每个核心需要做的检查将为sqrtN/M
次)。但是,由于GIL的存在,不能同时使用多个核心。这意味着效果与非并行版本相同,而且不能提前结束循环。为避免这种问题,可使用模块multiprocessing
实现多进程(而不是多线程),还可使用Cython或外部函数。
Python的表达能力极强,还易于学习,新手很快就会发现,他们能够在很短的时间内完成很多任务。很多Python库都封装了使用其他语言编写的工具,让你能够轻松地调用其他系统。例如,机器学习系统scikit-learn包含LIBLINEAR和LIBSVM(它们都是使用C语言编写的),而numpy
包含BLAS以及其他C和Fortran语言库。因此,如果Python代码能正确地利用这些模块,其速度将可与C代码媲美。
Python被认为是“开箱即用”的,因为它内置了很多重要的工具和稳定的库。
● unicode
和bytes
:身处Python语言的核心。
● array
:基本类型数组,内存使用效率高。
● math
:基本数学运算,包括一些简单的统计计算。
● sqlite3
:封装了基于文件的流行SQL引擎SQLite3。
● collections
:各种对象,包括双向队列、计数器和字典的变种。
● asyncio
:使用async和await语法为I/O密集型任务提供并发支持。
除上述语言核心库外,还有大量的外部库。
● numpy
:一个Python数值运算库,是各种矩阵运算的基石。
● scipy
:包含众多值得信赖的科学库,这些科学库通常封装了备受尊崇的C和Fortran语言库。
● pandas
:一个数据分析库,建立在scipy
和numpy
的基础之上,类似于R数据框架(data frame)和Excel电子表格。
● scikit-learn:正快速成为默认的机器学习库,建立在scipy
的基础之上。
● tornado
:让你能够轻松地实现并发的库。
● PyTorch和TensorFlow:分别来自Facebook和Google的深度学习框架,提供了强大的Python和GPU支持。
● NLTK
、SpaCy
和Gensim
:提供了强大Python支持的自然语言处理库。
● 数据库绑定(database binding):用于同各种数据库通信,包括Redis、MongoDB、HDF5和SQL。
● Web开发框架:用于创建网站的高性能系统,如aiohttp
、django
、pyramid
、flask
和tornado
。
● OpenCV
:提供计算机视觉的绑定。
● API绑定:让你能够轻松地使用流行的Web API,如Google、Twitter和LinkedIn。
还有大量受控环境和shell,可满足各种部署需求。
● 标准发行版。
● pipenv
、pyenv
和virtualenv
:用于搭建简单、可移植的轻量级Python环境。
● Docker:用于搭建可重现的简单开发和生产环境。
● Anaconda:专注于科学计算的环境。
● Sage:一个类似于Matlab的环境,但包含集成开发环境(Integrated Development Environment,IDE)。
● IPython:一个被科学家和开发人员广泛使用的交互式Python shell。
● Jupyter Notebook:一个基于浏览器的IPython扩展,被广泛用于教学和演示。
Python的主要优点之一是,让你能够快速建立想法的原型。因为有众多的支持库,所以很容易检查想法是否可行,即便你的实现相当粗糙。
要提高数学函数的速度,可考虑使用numpy
;要尝试机器学习,可考虑使用scikit-learn;而要清理和操作数据,pandas
是不错的选择。
那么问题来了:长期看,为让系统运行得更快,会不会导致团队的开发速度降低呢?只要投入足够的精力,总是可以进一步提高系统的性能,但这可能导致所做的优化脆弱而难以理解,最终给团队带来障碍。
一个这样的例子是使用Cython(请参见7.6节)。Cython是一种基于编译器的方法,它通过添加C类型声明来修改Python代码,以便能够使用C语言编译器进行编译。这虽然可极大地提高速度(通常只需做少量工作就可让速度与C语言代码媲美),但为支持这些代码而付出的代价将增加。具体地说,支持新模块的工作可能更难,因为为绕开Python虚拟机以提升性能,可能需要做些折中,而要搞明白这些折中,团队成员必须有更高的编程技能。
长期看,要成功地确保项目的高性能,编写出高性能代码只是一个方面。相比于加速和复杂的解决方案,整个团队的效率要重要得多。为确保团队效率,有几个因素至关重要:良好的结构和文档、易于调试以及遵循相同的标准。
假设你开发了一个原型,它虽未经详尽测试,也没有经过团队审核,但看起来“足够好”,因此被推送到了生产环境中。因为它从来没有以结构化的方式编写,所以它缺乏测试且没有文档化。突然之间,这增添了需要他人来支持的代码,而管理人员通常无法评估团队将为此付出什么样的代价。
这种解决方案难以维护,往往不受欢迎(没人去修改其结构,没人去添加可帮助重构的测试,更没有其他人愿意接手),因此确保其正常运行的工作始终落在最初接手的那个开发人员身上。这可能在紧急的情况下导致可怕的瓶颈,还可能带来重大风险:如果这位开发人员离开了,结果将如何呢?
通常,管理团队对难以维护的代码带来的惯性认识不足时,就会出现这种开发风格。事实证明,从长远看,测试和文档有助于保持团队的高效率,还有助于说服管理层分配用于整理原型代码的时间。
在研究环境中验证各种想法和数据集时,经常会在未遵循良好编码实践的情况下创建大量Jupyter Notebook。这背后的理念是以后再正确地编写,但实际上根本没有以后。因此最终的成果是一系列可运行的代码,但没有对代码进行重现、测试和确保它们可信任的基础设施。同样,这种成果带来的风险很高,而可信度又很低。
为避免上述情况发生,可采用以下通用方法。
● 确保可行:先创建一个足够好的解决方案。创建用后即弃的原型解决方案合乎情理,这让你能够在第二个版本中采用更佳的结构。编码前做些规划工作总是明智的选择,不然你肯定会后悔“整个下午都在编码,而没有花1小时来思考”。在有些领域,这被称为“三思而后行”。
● 确保正确:接下来,添加强大的测试套件,并辅以良好的文档和清晰的重现说明,让其他团队成员能够快速接手项目。
● 提高速度:最后,将重点放在剖析和编译或并行化上,使用既有测试套件核实改进后的解决方案仍然可以按预期工作。
有一些东西是必不可少的,这包括文档、良好的结构和测试,它们至关重要。
项目级文档有助于确保结构始终清晰,还可在未来给你和同事提供帮助;如果你省略这部分,没有人(包括你自己)会感谢你。一种合理的做法是,先将这种文档作为一个顶级README文件,以后必要时再将其扩展为一个docs/文件夹。
阐述项目的目的、各个文件夹包含的内容、数据来自何方、哪些文件至关重要以及如何运行项目(包括测试)。
Micha推荐同时使用Docker,这样将有一个顶级Dockerfile,它准确地指出了要成功地运行项目,需要哪些操作系统库,还让你能够轻松地在其他计算机中运行项目以及将其部署到云环境中。
添加一个包含单元测试的tests/文件夹。我们喜欢使用测试框架pytest
,因为它是建立在Python内置模块unittest
的基础之上的。先编写两三个测试,然后慢慢添加,再逐步采用覆盖工具,因为它会指出测试覆盖了多少行代码,有助于避免令人讨厌的意外情况。
如果继承了缺乏测试的遗留代码,那么先给它添加测试将带来极高的回报。另外,编写一些集成测试,对整个项目流程进行检查,确认特定的数据输入将生成特定输出,这有助于你在以后修改代码时保持理智。
每当遇到代码出现问题时,都添加一个测试。在同一个地方跌倒两次毫无意义。
在代码中,给每个函数、类和模块都编写文档字符串,这大有裨益。你的目标是提供有用的描述,指出函数实现了什么,并在可能的情况下通过简短的示例指出函数的预期输出。要获得灵感,请看看numpy
和scikit-learn中的文档字符串。
每当你发现代码块太长(如函数超过一屏)时,一定要通过重构来缩短它们。代码块越短,测试和支持起来就越轻松。
提示:编写测试时,请考虑遵循测试驱动的开发方法。在准确地知道需要开发什么且有现成的可测试示例的情况下,这种方法的效率极高。
你编写并运行测试,发现它们不能通过,再添加函数和最起码的逻辑,让测试得以通过。对所有的工作都进行测试后,就大功告成了。预先确定函数期望的输入和输出后,你将发现函数的逻辑实现起来非常容易。
如果你无法预先定义测试,那么一个自然而然的问题是,你真的明白相应的函数需要做什么吗?如果不明白,又怎么可能正确而高效地编写它呢?如果你采用的流程是创造性的,且对要研究的数据认识不太深刻,那么这种方法就不管用了。
务必进行源代码版本控制,当你在不方便的时候需要重写某些代码时,一定会为自己进行版本控制感到庆幸。养成频繁提交(每天乃至每10分钟提交一次)以及每天推送到仓库的习惯。
务必遵循编码标准PEP8。锦上添花的做法是,对未提交的版本控制钩子采用black
(非常严格的代码格式设置程序),使其根据标准修改代码。同时,使用flake8
对代码进行检查,以避免其他错误。
创建与操作系统隔离的环境可让工作更轻松。作者Ian喜欢使用Anaconda,而Micha 喜欢与Docker配套的pipenv
。这两种方案都可行,比使用操作系统的全局Python环境要好得多。
别忘了自动化是你的朋友,减少手动工作就意味着降低了错误出现的概率。通过使用自动化的测试套件运行程序,将构建系统、持续集成的工作自动化,而通过自动化系统部署过程,可将易于出错的烦琐任务变成任何人都能够运行和支持的标准流程。
最后,别忘了可读性远比抖机灵重要。在你和同事的维护工作中,简短但复杂而难以理解的代码片段将是拦路虎,让人感到害怕,不敢去触碰。因此,请让函数更容易理解(哪怕这样导致其代码更长),并辅以有用的文档,指出函数将返回什么,同时添加测试,用以核实函数像你期望的那样工作。
Jupyter Notebook虽然非常适合用于以视觉化方式交流,但会惯出用户懒惰的毛病。如果你发现Notebook中有很长的函数,请务必将它们提取到Python模块中,再添加相应的测试。
可考虑在IPython或QTConsole中编写原型代码,再将代码行转换为Notebook中的函数,然后将函数提取到模块中并添加配套的测试。最后,如果封装和数据隐藏会有所帮助,请考虑将代码封装在类中。
为检查函数是否像你期望的那样工作,Notebook中可能充斥着assert
语句。在Notebook中,除非对函数进行重构,将其放到模块中,否则无法轻松地测试代码,而assert
检查是一种实现进一步验证的简单方式。除非将代码提取到模块中,并编写合理的单元测试,否则它们是不可信的。
不提倡在代码中使用assert
语句来检查数据。它是一种核实特定条件是否满足的简单方式,但并不符合Python的语言习惯。为让代码更容易理解,请检查数据的状态是否符合预期,并在不符合预期时引发合适的异常。一种常见的异常是ValueError
,它在函数收到的值不符合预期时触发。Bulwark库是一个专注于Pandas的测试框架,可用于检查数据是否满足指定的约束条件。
你可能还需在Notebook末尾添加一些完整性检查,这包括逻辑检查以及指出结果符合预期的raise
和print
语句。当你半年后再来看这些代码时,肯定会庆幸你以前所做的工作,让你能够轻松地确定它们是否能正确地工作。
使用Jupyter Notebook的一个麻烦是,难以将代码与版本控制系统共享。nbdime是一套新工具,让你能够比较不同的Notebook。这是一款救命神器,让你能够与同事协同工作。
人生可能复杂难懂。从本书第1版推出到现在已过去了5年,在此期间,两位作者的家人和朋友遭遇了很多变故,其中包括抑郁、癌症、搬家、生意上的成功和失败以及职业方向的转变。这些外部变故不可避免地会影响工作和生活前景。
千万别忘了不断地寻找工作和生活中的乐趣。只要开始寻找,总能发现有趣的细节或要求。你可能会问,他们为何会做出那样的决定,如果换成我,会做出什么不同的决定。这可能让你醍醐灌顶,顺利地开启有关如何改变或改进的对话。
将值得庆贺的事情记录下来。人们很容易深陷日常琐事,将成绩抛诸脑后;当你为跟上时代不断向前奔跑时,就会忘记自己取得的巨大进步。
建议你用一个清单将值得庆贺的事项记录下来,并注明是如何庆贺的。Ian就有一个这样的清单,每当他去更新这个清单时都会又惊又喜:原来上一年竟然有如此多的好事,要不是更新清单,他早已将这些好事忘在脑后了。这个清单不仅包含工作成就,还有业余爱好和运动方面的好事,以及庆贺成就的情况。Micha的做法是,确保将个人生活放在第一位,并在远离计算机的时候专注于非技术项目。不断提高技能至关重要,但这并不意味着必然失去热情!
编程有赖于好奇心,还有对深究技术细节的乐此不疲,在需要专注于性能时尤其如此。可惜当你失去热情后,首先消失的就是这种好奇心,因此请花时间确保旅途愉快,将乐趣和好奇心保留下来。