本文最初发布于 hackernoon ,经原作者 Nick Humrich 授权由 InfoQ 中文站翻译并分享。
让我们来讨论一个我最近一直在思考的问题:Python 的性能。顺便说一下,我是 Python 的忠实拥趸,我在各种情况下都会积极尝试使用 Python 来解决问题。大家对 Python 最大的抱怨就是它的速度慢。有些人甚至因为 Python 的速度不如某个语言而拒绝使用它。本文中我将阐述,即便 Python 这么慢,为什么还值得你对它进行尝试。
速度不再关键
之前,程序的运行时间相当长。CPU 资源和内存资源都十分珍贵,程序的运行时间在这种情况下是一个重要指标。计算机本身十分昂贵,当然还有随之而来昂贵的电力消耗。优化这些资源就十分必要,因为在商业世界有一个永恒的规则:
优化你最昂贵的资源。
历史上,程序最昂贵的资源是计算机的运行时间。这也就导致了对计算机科学的研究更专注于不同算法的效率。然而在当下环境中,这已经不再适用,现在硅的价格已经十分便宜了。是真的非常便宜。运行时间不再是你最昂贵的资源。一个公司最昂贵的资源现在是其雇佣的员工的时间。也就是正在看这篇文章的你自己的时间。对现在的公司来说,完成项目比让项目跑得更快更重要。这点非常重要,这里再次强调:
完成项目比让项目跑得更快更重要。
你也许会说“我们公司对性能要求很高,我构建的网站应用需要所有的请求在 X 毫秒内返回。”或者“客户认为我们的应用慢而放弃使用我们的应用。”在这里我不是说速度根本不重要,我只是想说明速度不再是最重要的指标,因为它不再是你最昂贵的资源。
速度!
速度是唯一重要的事情
在编程的世界中当你提到速度,一般是指程序的性能,也就是 CPU 周期。而当你的 CEO 提到速度,他通常指的是业务上的速度,其中最重要的是投入市场的时间。你的产品或网络应用有多快并不重要,应用采用哪种语言编写的也不重要,甚至是使项目运行投入了多少资金都不重要。最终,唯一能够让你的公司存活下来的是产品投入市场的时间。这里不是指初创公司观念中的盈利时间,而更多是从想法转换到实际消费者手中所花费的时间。在商业世界中能存活下来的唯一方法是比你的竞争对手更快地进行创新。如果你的竞争对手比你更早地发布产品,那么你有再多的好点子也无济于事。你必须成为市场的第一个进入者,或至少要赶上领先的节奏。一旦你掉队了,那么你就大势已去。
在商业世界中能存活下来的唯一方法是比你竞争对手更快地进行创新。
微服务的例子
亚马逊、谷歌、Netflix 等公司深刻理解速度的重要性。它们创建了一个能快速发展和创新的业务系统。微服务就是这个问题的解决方案。本文并不讨论你是不是应该使用微服务,但最起码亚马逊和谷歌认为它们应采用微服务。
微服务天生就很慢。微服务的最基础的概念就是拆分业务边界,并通过网络调用来相互通讯。这也就意味着你需要把一个只占几个 cpu 周期的方法调用转换成网络调用。从性能层面上来说,这简直糟糕透顶。网络调用的速度和 CPU 调用根本不可同日而语。但是那些大公司仍然选择使用微服务。没有比微服务更慢的架构了。微服务的最大劣势就是其性能,但是它所带来的最大好处是缩短了投入市场需要的时间。通过构建小型项目和少量代码的团队,公司可以以一个非常快的速度进行迭代与演进。这个例子只是为了展示不仅仅是初创公司,大公司也关注投入市场所需的时间。
CPU 并非你的瓶颈
如果你编写像网络服务器上的网络应用,那么 CPU 时间可能并非你应用的瓶颈。当你的网络服务器处理一个请求,它可能会需要调用多个网络调用,例如数据库或 Redis 缓存。这些服务本身速度很快,然而网络调用的过程却很慢。一篇博客很好地描述了各个特定操作速度上的差别。其中,作者将 CPU 时间对应到人们易于理解的时间。如果单个 CPU 周期对应一秒的话,一个从加利福尼亚到纽约的网络调用就大约相当于 4 年。对,网络调用就是这么慢。粗略地估计,在同一数据中心内的一个普通的网络调用需要 3 毫秒,这在前面的对应关系下相当于 3 个月。现在假如你的程序是 CPU 密集型的,需要花费 100,000 个 CPU 周期来处理一次调用。按之前的比例来算,这些时间相当于 1 天。那么如果你用一个慢 5 倍的语言,它也就只花费了 5 天。相对于 3 个月的网络调用,4 天的差别就无足轻重了。如果用户在等待一个至少需要 3 个月的包裹,那么 4 天的差别相对来说就不那么重要了。
说了这么多我只是想说,即便 python 很慢,但这并不重要。语言的速度(也就是 CPU 时间)几乎不会导致问题。谷歌就这个概念做过一个研究,并写了一篇论文。论文中谈论了设计高吞吐量的系统。在结论中这样描述到:
在一个高吞吐量的环境中使用一个解释型语言看似矛盾,但是我们发现 CPU 时间几乎不是瓶颈因素,表达性强的语言意味着大部分代码是短小的,大多数时间花费在了 I/O 以及原生代码运行时上。此外,解释型的实现所具备的灵活性十分有用,它方便了我们在语言层面上的试验,也方便了我们探索将计算分布到多台机器上的方法。
简单说来:
CPU 时间几乎不是瓶颈因素。
那如果 CPU 时间的确是系统的瓶颈呢?
你可能会说“这观点很好,但是我们确实在 CPU 上遇到了瓶颈,造成了我们网络应用的速度缓慢”,或者“在服务器上 X 语言相对 Y 语言需要更少的硬件资源来运行。”这可能都是事实。但网络应用的优势就是你可以几乎无限地进行负载均衡。换而言之,就是使用更多的硬件资源。当然 Python 相较其他语言,如 C 语言,可能需要更多硬件资源。那就使用更多的硬件来解决这个问题。硬件相对于你的人工时间便宜许多。如果你一年内节约了几周的开发时间,这就远胜于你在硬件上所节约下来的花费。
那么,Python 到底快不快?
前面我谈论了最重要的是开发所花费的时间。但是问题还是没有得到回答:Python 的开发时间的确比其他语言快么?经过多方调查,我、谷歌以及许多第三方结论都会告诉你 Python 能提升多大产能。Python 抽象化了诸多内容,可以让你专注于你真正的业务逻辑,而不用关心你是应该使用 vector 还是 array 等底层细节问题。你可能不相信这道听途说的观点,所以让我们看一些经验数据。
总体来说,争论 python 是否高产,最终讨论的是脚本(或动态语言)与静态类型语言之间的比较。我认为大家都赞同静态类型语言的产量较低,但这里有一篇很好的论文解释了其中的原因。就 Python 而言,曾有研究分析了不同语言编写一个字符串处理程序所花费的时间,并做了很好的总结。
使用不同语言编写字符串处理应用所花费的时间。(Prechelt 与 Garret)
在结论中 Python 比 Java 的生产效率高两倍。还有其他诸多研究结果得到类似的结论。Rosetta Code 对不同语言进行了公平而深入地研究。在论文中它们将 Python 和其他脚本 / 解释型语言进行了比较,并认为:
Python 是其中最精练的,甚至比函数式语言更好(平均短 1.2-1.6 倍)。
总体看来 Python 代码的行数总是更少。代码行数听上去是一个糟糕的指标,但是多项研究显示(包括之前提及的两个),在各语言中输入每行代码的时间是不相上下的。因此,减少代码行数也就相当于提高了生产效率。就连C#程序员codinghorror 也写了一篇文章阐述 Python 具有更高的产量。
我认为这已经足够能说明 Python 相较于诸多其他语言更高产。这主要归功于 Python 的开箱即用以及丰富的第三方包。这里有一篇文章简述了 Python 和其他语言的差别。如果你不知道为什么 Python 这么“小”还这么高产,我推荐你学习一下 Python 来亲自体验一下,下面将是你的第一行程序:
import __hello__
那如果运行速度对你真的很重要呢?
上述观点的论调听上去像认为优化和速度根本不重要。但是事实是,许多时候运行时效率至关重要。一个例子是,你的网络应用有一个特定的端点需要相当长的时间来响应请求。同时你知道它需要有多快,也知道它要被优化到什么程度。
在这个例子中,发生了下面两件事:
- 我们关注到某个运行慢的端点。
- 我们认为它慢,因为我们了解什么是足够快,并且它没能达到这个指标。
我们不必在应用中对每个服务进行细节调优。每个服务只需要能“足够快”来满足用户的需求就够了。用户会发现某个端点花费了几秒时间返回,但是他们并不会注意到你把一个 35 毫秒的请求优化到了 25 毫秒。你只需要达到“足够好”就可以了。免责声明:不得不说一些应用,如实时拍卖应用,确实需要细节调优,能提升一毫秒算一毫秒。但是这是一个特例,而不是业界的规则。
为了弄清如何优化某个端点,第一步你需要对你的代码进行性能分析,并尝试整理出其中的瓶颈。归根到底:
任何不考虑瓶颈的调优都是幻想。 --Gene Kim
如果你的优化并不解决瓶颈,那你就是在浪费你的时间,而且还不能解决真正地问题。不解决瓶颈,你就不会在性能上得到显著的提升。如果你尝试着在了解瓶颈前优化,你就像在和你的代码在玩打地鼠的游戏。在排查和确定瓶颈前优化代码也是“不成熟优化”的表现。Donald Knuth 常被引用下面的观点,虽然他本人称这也是从其他人那儿听来的:
不成熟的优化是万恶之源。
Donald Knuth 在一次关于维护代码库的讨论中进行了下面的完整描述:
我们应该忘记那些小的性能提升,这占了 97% 的时间:不成熟的优化是万恶之源。但同时我们也不能放过那至关重要的 3%。
换句话来说,大部分时间,你不应该关心代码的优化。它们通常已经足够好了。如果没有能达到标准,我们应该只需要改变那 3% 的代码。你并不会因为你的代码使用一个 if 替代了一个方法,得到几毫秒的性能提升而获得任何奖励。只有在分析之后再进行优化。
不成熟的优化包含盲目调用某个更快的方法,或使用一个特定的数据结构只因为其总体上更快。计算机科学认为两个方法或算法有一样的渐进增长(或时间复杂度),那么就可以认为它们性能是相同的,就算其中之一比另一个慢两倍。计算机的速度太快,算法在计算上的增长,如数据或使用量的增长比算法本身重要得多。换而言之,如果你有两个 O(log n) 的方法,一个是另一个速度的两倍,这之间的差别根本无关紧要。随着数据量的增长,它们都会以相同的速度变慢。这也就是为什么不成熟的优化是万恶之源,它会浪费我们的时间,最终却在提升性能上帮不上我们什么忙。
就时间复杂度而言,你可以认为用任何的语言写你的程序的复杂度都是 O(n) 的,其中 n 是代码的行数或指令个数。同一指令的增长速率都是相同的。所以一个语言或运行时的快慢并不重要,就渐进增长而言,所有语言都是等价的。在这个逻辑下,你可以认为,因为某个语言速度快而选择其为开发你应用的语言是不成熟优化的一种体现。你不应该主观地判断某个语言快而不去进行衡量、不去了解将会遇到的瓶颈。
因为某个语言速度快而选择其为开发你应用的语言是不成熟优化的一种体现。
优化 Python
我最喜欢 Python 的一点就是它可以让你一步一步地优化你的代码。比如说你有一个 Python 方法,你发现它是你项目中的瓶颈。你已经对其优化了数次,可能是遵循了这里或这里的意见,现在你确定Python 本身是你应用的瓶颈所在。Python 是能够直接调用C 代码的,这就意味着你可以用C 重写这个方法来减少性能问题。你可以一个一个地进行替换。这个过程能让你调用任何最终编译成C 兼容指令的优化的代码,也让你能在大部分情况下继续使用Python,而只在真正需要的时候深入底层进行开发。
有一个叫Cython 的语言,它是Python 的超集。几乎是Python 和C 的结合体,同时它是渐进的类型化语言。任何Python 代码都是合法的Cython 代码,Cython 会将代码编译成C 代码。有了Cython,你可以编写模块或方法,渐渐地引入C 语言的类型和性能。你可以混合使用C 语言的类型和Python 的鸭子类型(duck type)。通过Cython 你可以只在瓶颈处进行调优,而在其他地方仍然使用优美的Python 语言,两者能完美地结合。
使用Python 编写的太空大规模多人在线游戏EVE Online 的截图
当你最终遇到了Python 的性能瓶颈,你不需要将你所有代码移植到其他语言。你总是可以使用Cython 重写部分方法来满足性能上的需求。这也是游戏 EVE Online 所采用的策略。Eve 是一个大型多人在线电脑游戏,它完全使用 Python 和 Cython 开发。游戏开发人员通过在 C/Cython 中调优瓶颈来达到游戏级的性能要求。如果游戏都能达到性能上的需求,那么大部分情况都应该可以满足。此外,还有其他方法来优化你的 Python 程序。例如 PyPy 是一个 Python 的运行时编译执行(JIT)的实现,只需要使用 PyPy 切换默认的 Cython,就可以显著地提升你长时间运行应用的运行时性能,如在网络服务器上。
让我们回顾一下文中的要点:
- 优化你最昂贵的资源。也就是你自己,而不是电脑。
- 选择可以有助于快速开发的语言、框架、架构,例如 Python。不要只因为运行速度快而选择某个技术。
- 当你的应用有性能问题时,找出你应用中性能的瓶颈。
- 你的瓶颈通常不是 CPU 或 Python 本身。
- 如果你已经优化了算法或其他方面,确定 Python 的确是你项目的瓶颈,那么可以将这个热点移到 Cython/C 中进行改写。
- 接下来就坐下来享受快速编码的乐趣吧。
查看英文原文: Yes, Python is Slow, and I Don’t Care
感谢冬雨对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论