关于性能:必须收藏20个开发技巧教你开发高性能计算代码

7次阅读

共计 8713 个字符,预计需要花费 22 分钟才能阅读完成。

摘要:华为云专家从优化布局 / 执行 / 多过程 / 开发心理等 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…

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 参数,调动时,传入不同的参数,既能够疾速测试,又能够平安治理调试和生产代码。千万不要用正文的形式,来开 / 关代码。

参考以下示意代码:

 # Bad
    def calculate_bad():
        # uncomment for debugging
        # data = load_sampling_data()
        data = load_all_data()
     
    # Good
    def calculate(sampling=False):
        if sampling:
            data = load_sampling_data()
        else:
            data = load_all_data()

梳理分明数据 Pipeline,建设性能评估机制

我本人写了个 Decorator @timeit 能够很不便地打印代码的用时。

 @timeit
    def calculate():
        pass

这样生成的 log,菜市场大妈都看的懂。上了生产后,也能够告诉配置来管制是否打印。

[2020-07-09 14:44:09,138] INFO: TrialDataContainer.load_all_data - Start
...
[2020-07-09 14:44:09,158] INFO: preprocess_demand - Start
[2020-07-09 14:44:09,172] INFO: preprocess_demand - End - Spent: 0.012998 s
...
[2020-07-09 14:44:09,186] INFO: preprocess_warehouse - Start
[2020-07-09 14:44:09,189] INFO: preprocess_warehouse - End - Spent: 0.002611 s
...
[2020-07-09 14:44:09,454] INFO: preprocess_substitution - Start
[2020-07-09 14:44:09,628] INFO: preprocess_substitution - End - Spent: 0.178258 s
...
[2020-07-09 14:44:10,055] INFO: preprocess_penalty - Start
[2020-07-09 14:44:20,823] INFO: preprocess_penalty - End - Spent: 10.763566 s

[2020-07-09 14:44:20,835] INFO: TrialDataContainer.load_all_data - End - Spent: 11.692677 s
[2020-07-09 14:44:20,836] INFO: ObjectModelsController.build - Start
[2020-07-09 14:44:20,836] INFO: ObjectModelsController.build_penalties - Start
[2020-07-09 14:44:20,836] INFO: ObjectModelsController.build_penalties - End - Spent: 0.000007 s
[2020-07-09 14:44:20,837] INFO: ObjectModelsController.build_warehouses - Start
[2020-07-09 14:44:20,848] INFO: ObjectModelsController.build_warehouses - End - Spent: 0.011002 s

另外,Python 也提供了 Profiling 工具,能够用于费时函数的定位。

优先解决数据读取性能

一个残缺的我的项目,可能会有很多性能晋升的局部,我倡议,优先解决数据读取,起因是,问题容易定位,批改代码绝对独立,见效快。

举例来说,很多机器学习我的项目,都须要建设数据样本数据,用于模型训练。而数据样本的建设,常通过创立一个宽表来实现。很多 DB 都提供了很多晋升操作性能的办法。假如咱们应用 MongoDB,其提供了 pipeline 函数,能够把多个数据操作,放在一个语句中,一次传给 DB。

如果咱们粗犷地单条解决,在一个我的项目中咱们试过,须要近 20 个小时,花了半天的工夫来优化,跑起来,来到座位去接杯水,回来就曾经跑完了,费时降为 1 分钟。

留神,很多时候咱们没有能源去优化数据读取的性能,因为数据读取可能次数并不多,但事实上,特地是在试算阶段,数据读取的次数其实并不少,因为咱们总是没有进行过对数据的扭转,比方加个字段,加个特色什么的,这时候,数据读取的代码就要常常被用到,那么优化的收益就体现进去了。

再思考升高工夫复杂度,思考应用预处理,用空间换工夫

咱们如果把性能优化当做一桌宴席,那么能够把数据读取局部的性能优化,当作开胃小菜。接下来,咱们进入更好玩的局部,优化工夫复杂度,用空间换工夫。

举例来说,如果你的程序的复杂度为 O(n^2),在数据很大时,肯定会十分低效,如果能优化为复杂度为 O(n),甚至 O(1),那就会带来几个数据级的性能晋升。

比方下面提到的,应用倒排表,来做数据预处理,用空间换工夫,达到从 50 小时到 15 秒的性能晋升。

因驰名的 GIL,应用多过程晋升性能,而非多线程

在 Python 的世界里,因为驰名的 GIL,如果要晋升计算性能,其基本准则为:对于 I / O 操作密集型利用,应用多线程;对于计算密集型利用,应用多过程。

一个多过程的例子:

咱们筹备了一个长数组,并筹备了一个绝对比拟费时的等差数列求和计算函数。

 MAX_LENGTH = 20_000
    data = [i for i in range(MAX_LENGTH)]
     
    def calculate(num):
        """Calculate the number and then return the result."""
        result = sum([i for i in range(num)])
        return result

单过程执行例子代码:

 def run_sinpro(func, data):
        """The function using a single process."""
        results = []
        
        for num in data:
            res = func(num)
            results.append(res)
            
        total = sum(results)
        
        return total
     
    %%time
    result = run_sinpro(calculate, data)
    result
CPU times: user 8.48 s, sys: 88 ms, total: 8.56 s
Wall time: 8.59 s

1333133340000

从这里咱们能够看到,单过程须要 ~9 秒。

接下来,咱们来看看,如何应用多过程来优化这段代码。

 # import multiple processing lib
    import sys
     
    from multiprocessing import Pool, cpu_count
    from multiprocessing import get_start_method, 
                                set_start_method, 
                                get_all_start_methods
     
    def mulp_map(func, iterable, proc_num):
        """The function using multi-processes."""
        with Pool(proc_num) as pool:
            results = pool.map(func, iterable)
            
        return results
     
    def run_mulp(func, data, proc_num):
        results = mulp_map(func, data, proc_num)
        total = sum(results)
        
        return total
     
    %%time
    result = run_mulp(calculate, data, 4)
    result
CPU times: user 14 ms, sys: 19 ms, total: 33 ms
Wall time: 3.26 s

1333133340000

同样的计算,应用单过程,须要约 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/w…
  • https://en.wikipedia.org/wiki…
  • https://en.wikipedia.org/wiki…:~:text=A global interpreter lock (GIL,on%20a%20multi%2Dcore%20processor.
  • https://git.huawei.com/x00349…
  • https://docs.python.org/3/lib…

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0