必须收藏:20 个开发技巧教你开发高性能计算代码
摘要:华为云专家从优化规划 / 执行 / 多进程 / 开发心理等 20 个要点,教你如何开发高性能代码。
高性能计算,是一个非常广泛的话题,可以从专用硬件/处理器/体系结构/GPU,说到操作系统/线程/进程/并行/并发算法,再到集群/网格计算,最后到天河二号(TH-1)。
我们这次的分享会从个人的实践项目探索出发,与大家分享自己摸爬滚打得出的心得体会,一如既往的坚持原创。其中内容涉及到优化规划 / 执行 / 多进程 / 开发心理等约 20 个要点,其中例子代码片段,使用 Python。
高性能计算,在商业软件应用开发过程中,要解决的核心问题,用很白话的方式来说,“在有限的硬件条件下,如何让一段原本跑不动的代码,跑起来,甚至飞起来。”
性能提升经验
举 2 个例子,随意感受下。
(1)635 万条用户阅读文档的历史行为数据,数据处理时间,由 50 小时,优化到 15 秒。(是的,你没有看错)
(2)基于 Mongo 的宽表创建,由 20 小时,优化到出去打杯水的功夫。
在大数据的时代,一个优秀的程序员,可以写出性能比其他人的程序高出数百倍,甚至数千倍,具备这样的技能,对产品的贡献无疑是很大的,对个人而言,也是自己履历上亮点和加分项。
聊聊历史
2000 年前后,由于 PC 硬件限制,那一代的程序员,比如,国内的求伯君 / 雷军,国外的比尔盖茨 / 卡马特,都是可以从机器码 / 汇编的角度来提升程序性能。
到 2005 年前后,PC 硬件性能发展迅速,高性能优化常常听到,来自嵌入式设备和移动设备。那个年代的移动设备主流使用 J2ME 开发,可用内存 128KB。那个年代的程序员,需要对程序大小(OTA 下载,有数据流量限制,如 128KB),内存使用都精打细算,真的是掐着指头算。比如,通常一个程序,只有一个类,因为新增一个类,会多使用几 K 内存。数据文件会合并为一个,减少文件数,这样需要算,比如从第几个字节开始,是什么数据。
2008 年前后,第一代 iOS / Android 智能手机上市,App 可用内存达到 1GB,App 可以通过 WIFI 下载,App 大小也可以达到一百多 MB。我刚才看了下我的 P30,就存储空间而言,QQ 使用了 4G,而微信使用了 10G。设备性能提升,可用内存和存储空间大了,程序员们终于“解放”了,直到–大数据时代的到来。
在大数据时代下,数据量疯狂增长,一个大的数据集操作,你的程序跑一晚上才出结果,是常有的事。
基础知识
本次分享假设读者已经了解了线程/进程/GIL 这些概念,如果不了解,也没有关系,可以读下以下的摘要,并记住下面 3 点基础知识小结即可。
什么是进程?什么是线程?两者的差别?
以下内容来自 Wikipedia: https://en.wikipedia.org/wiki/Thread_(computing)
Threads differ from traditional multitasking operating-system processes in several ways:
processes are typically independent, while threads exist as subsets of a process
processes carry considerably more state information than threads, whereas multiple threads within a process share process state as well as memory and other resources
processes have separate address spaces, whereas threads share their address space
processes interact only through system-provided inter-process communication mechanisms
context switching between threads in the same process typically occurs faster than context switching between processes
著名的 GIL (Global interpreter lock)
以下内容来自 wikipedia.
A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time.[1] An interpreter that uses GIL always allows exactly one thread to execute at a time, even if run on a multi-core processor. Some popular interpreters that have GIL are CPython and Ruby MRI.
基础知识小结:
因为著名的 GIL,为了线程安全,Python 里的线程,只能跑在同一个 CPU 核,无法做到真正的并行
计算密集型应用,选用多进程
IO 密集型应用,选用多线程
实践要点
以上都是一些铺垫,从现在开始,我们进入正题,如何开发高性能代码。
一直以来,我都在思考,如何做有效的分享?首先,我坚持原创,如果同样的内容可以在网络上找到,那就没有分享的必要,浪费自己和其他人的时间。其次,对不同的人,采用不同的方法,讲不同的内容。
所以,这次分享,听众大都是有开发经验的 python 程序员,所以,我们不在一些基础的内容上花太多时间,不了解也没关系,下来自已看看也都能看懂。这次我们更多来从实践问题出发,我总结了约 20 个要点和开发技巧,希望能对大家今后的工作有帮助。
规划和设计尽可能早,而实现则尽可能晚
接到一个项目时,我们可以先识别下,哪些部分可能会出现性能问题,做到心里有数。在设计上,可以早点想着,比如,选用合适的数据结构,把类和方法设计解耦,便于将来做优化。
在我们以前的项目中,见过有些项目,因为早期没有去提前设计,后期想优化,发现改动太大,风险非常高。
但是,这里一个常见的错误是,上来就优化。在软件开发的世界里,这点一直被经常提起。我们需要控制自己想早优化的心理,而应优先把大框架搭起来,实现主要功能,然后再考虑性能优化。
先简单实现,再评估,做好计划,再优化实施
评估改造成本和收益,比如,一个模块费时一小时,如果优化,需要花费开发和测试时间 3 小时,可能节省 30 分钟,性能提升 50%;另一模块,费时 30 秒,如果优化,开发和测试需要花费同样的时间,可以节省 20 秒,性能提升 67%。你会优先优化哪个模块?
我们建议优先考虑第一个模块,因为收益更大,可节省 30 分钟;而第二个模块,费时 30 秒,不优化也能接受,应该把优化优先级放到最低。
另一个情况,如第 2 个模块被其它模块高频调用,那我们又要重新评估优先级。
优化时,我们要控制我们可能产生的冲动:优化一切能优化的部分。
当我们没有“锤子”时,我们遇到问题很苦恼,缺乏技能和工具;但是,当我们拥有“锤子”时,我们又很容易看一切事物都像“钉子”。
开发调试时,使用 Sampling 数据,并配合开关配置
开发时,对费时的计算,可以设置 sampling 参数,调动时,传入不同的参数,既可以快速测试,又可以安全管理调试和生产代码。千万不要用注释的方式,来开/关代码。
参考以下示意代码:
梳理清楚数据 Pipeline,建立性能评估机制
我自己写了个 Decorator @timeit 可以很方便地打印代码的用时。
这样生成的 log,菜市场大妈都看的懂。上了生产后,也可以通知配置来控制是否打印。
另外,Python 也提供了 Profiling 工具,可以用于费时函数的定位。
优先处理数据读取性能
一个完整的项目,可能会有很多性能提升的部分,我建议,优先处理数据读取,原因是,问题容易定位,修改代码相对独立,见效快。
举例来说,很多机器学习项目,都需要建立数据样本数据,用于模型训练。而数据样本的建立,常通过创建一个宽表来实现。很多 DB 都提供了很多提升操作性能的方法。假设我们使用 MongoDB,其提供了 pipeline 函数,可以把多个数据操作,放在一个语句中,一次传给 DB。
如果我们粗暴地单条处理,在一个项目中我们试过,需要近 20 个小时,花了半天的时间来优化,跑起来,离开座位去接杯水,回来就已经跑完了,费时降为 1 分钟。
注意,很多时候我们没有动力去优化数据读取的性能,因为数据读取可能次数并不多,但事实上,特别是在试算阶段,数据读取的次数其实并不少,因为我们总是没有停止过对数据的改变,比如加个字段,加个特征什么的,这时候,数据读取的代码就要经常被用到,那么优化的收益就体现出来了。
再考虑降低时间复杂度,考虑使用预处理,用空间换时间
我们如果把性能优化当做一桌宴席,那么可以把数据读取部分的性能优化,当作开胃小菜。接下来,我们进入更好玩的部分,优化时间复杂度,用空间换时间。
举例来说,如果你的程序的复杂度为 O(n^2),在数据很大时,一定会非常低效,如果能优化为复杂度为 O(n),甚至 O(1),那就会带来几个数据级的性能提升。
比如上面提到的,使用倒排表,来做数据预处理,用空间换时间,达到从 50 小时到 15 秒的性能提升。
因著名的 GIL,使用多进程提升性能,而非多线程
在 Python 的世界里,由于著名的 GIL,如果要提升计算性能,其基本准则为:对于 I/O 操作密集型应用,使用多线程;对于计算密集型应用,使用多进程。
一个多进程的例子:
我们准备了一个长数组,并准备了一个相对比较费时的等差数列求和计算函数。
单进程执行例子代码:
从这里我们可以看到,单进程需要 ~9 秒。
接下来,我们来看看,如何使用多进程来优化这段代码。
同样的计算,使用单进程,需要约 9 秒;在 8 核的机器上,如果我们使用多进程则只需要 3 秒,耗时节省了 66%。
多进程:设计好计算单元,应尽可能小
我们来设想一个场景,假设你有 10 名员工,同时你有 10 项工作,每项工作中,都由相同的 5 项子工作组成。你会如何来做安排呢?理所当然的,我们应该把这 10 名员工,分别安排到这 10 项工作中,让这 10 项工作并行执行,没毛病,对吧?但是,在我们的项目中,如果这样来设计并行计算,很可能出问题。
这里是一个真实的例子,最后性能提升的效果很差。原因是什么呢?(此处可按 Pause 键,思考一下)
主要的原因有 2 个,并行的计算单元颗粒度不应太大,大了以后,通常会有数据交换或共享问题。其次,颗粒度大了以后,完成时间会差别比较大,形成短板效应。也就是,颗粒度大了以后,任务完成时间可能会差别很大。
在一个真实的例子中,并行计算需要 1 个小时,最后分析后才发现,只有一个进程需要 1 小时,而其他进程的任务都在 5 分钟内完成了。
另一个好处是,出错了,好定位,代码也好维护。所以,计算单元应尽可能小。
多进程:避免进程间通信或同步
当我们把计算单元设计的足够小后,应该尽量避免进程间通信或同步,避免造成等待,影响整体执行时间。
多进程:调试是个问题,除了 log 外,尝试 gdb / pdb
并行计算的公认问题是,难调试。通常的 IDE 只可以中断一个进程。通过打印 log,并加上 pid,来定位问题,会是一个比较好的方法。注意,并行计算时,不要打太多 log。如果你按照上面讲的,先调通了单进程的实现,那么这时,最重要是,打印进程的启动点,进程数据和关闭点,就可以了。比如,观测到某个进程拖了大家的后腿,那就要好好看看那个进程对应的数据。
这是个细致活,特别是,当多进程启动后,可能跑着数小时,你也不知道在发生什么?可以使用 linux 下的 top,或 windows 下的 activity 等工具来观测进程的状态。也可以使用 gdb / pdb 这样的工具,进入某个进程中,看看卡在哪里。
多进程:避免大量数据作为参数传输
在真实的项目中,我们设计的计算单元,不会像上面的简单例子一样,通常都会带有不少参数。这时需要注意,当大数据作为参数传输时,会导致内存消耗很大,并且,子进程的创建也会很慢。
多进程:Fork? Spawn?
Python 的多进程支持 3 种模式去启一个进程,分别是,spawn, fork, forkserver。他们之间的差别是启动速度,和继承的资源。spawn 只继承必要的资源,而 fork 和 forkserver 则与父进程完全相同。
依赖于不同的操作系统,和不同版本的 python,其默认模式也不同。对 python 3.8,Windows 默认 spawn;从 python 3.8 开始,macOS 也默认使用 spawn;Unix 类 OS 默认 fork;fork 和 forkserver 在 windows 上不可用。
灵魂拷问:多进程一定比单进程快吗?
讲到这里,我们的分享基本可以结束了,对吧?按照 python multiprocessing API,找几个例子,并参考我上面说的几点,能解决 80%以上的问题。够了,毕竟性能优化也不是天天需要。以下内容可能要从事性能优化一年后,才会思考到,这里写出来,供参考,帮助以后少走些弯路。
比如,多进程一定更快吗?
正如第一点所说,任何优化都有开销。当多进程解决不了你的问题时,别忘了试试,改回单进程,说不定就解决了。(这也是一个真实的例子,花了 2 周去优化一个,10 进程也需要 3 小时才能执行完的程序,改回单进程后,直接跑进 30 分钟内了。)
优化心理:手里有了锤子,一切都长的像钉子
同上要点,有时候需要的,可能是优化数据结构,而不是多进程。
优化心理:不要迷信“专家”
相信很多团队都这样,当项目遇到重大技术问题,比如性能需要优化,管理者都会召集一些专家来帮忙。根据我的观察,80%的情况下,没有太多帮助,有时甚至更糟。
原因很简单,用一句话来说,你花了 20 个小时解决不了的问题,其他人用 5 分钟,根据你提供的信息,指出问题所在,可能性很低,无论他相关的经验有多么丰富。如果不信,你可以回想下自己的经验,或将来注意观察下,再回过头来看这个观点。为什么可能更糟?因为依赖心理。有了专家的依赖,人们是不会真拼的,“反正有专家指引”。就像尼采说过,“人们要完成一件看似不可能的事时,需要鼓胀到超过自己的能力。”,所以,如果这件事真的很难,你“疯狂”地相信,“这件事只有你能解决,只能靠你自己,其他人都无法解决”,说不定效果更好。
在一个持续近一个月的性能优化项目中,我脑海中时常响起《名侦探柯南》中的一句台词:真相只有一个。我坚定无比地相信,解法离我越来越近,哪怕事实是,一次又一次地失败,但这份信念到最后的成功帮助很大。
优化心理:优化可能是一个长期过程,每天都在迷茫中挣扎
性能优化的过程,漫长而煎熬,如果能有一个耐心的听众,会帮助很大。他/她可能不会帮你指出问题的解决办法,只是耐心地听着,只说,“it will be fine.” 但这样的述说,会帮助理清思路,能灵感迸发也说不定。这跟生活中其它事情的道理,应该也是一样的吧。
优化心理:管理者帮助争取时间,减轻心理压力
比如,有经验的管理者,会跟业务协商,分阶段交付。而有些同学,则会每隔几小时就过来问下,“性能有提升吗?” 然后脸上露出一种诡异的表情:“真的有那么难?”
目前我所有知道的一个案例,其性能优化持续了近一年,期间几拨外协人员,来了,又走了,搞得奔溃。
所以,我们呼吁,项目管理者应该多理解开发人员,帮助开发人员挡住外部压力,而不是直接透传压力,或者甚至增大压力。
References
https://baike.baidu.com/item/高性能计算
https://www.liaoxuefeng.com/wiki/1016959663602400/1017627212385376
https://en.wikipedia.org/wiki/Thread_(computing)
https://en.wikipedia.org/wiki/Global_interpreter_lock#:~:text=A global interpreter lock (GIL,on%20a%20multi%2Dcore%20processor.
https://git.huawei.com/x00349737/nqutils
https://docs.python.org/3/library/profile.html
版权声明: 本文为 InfoQ 作者【华为云开发者社区】的原创文章。
原文链接:【http://xie.infoq.cn/article/92befa9d2868f188d000e0033】。文章转载请联系作者。
评论