并行算法 Parallel Algorithm -- 提高执行效率

文章目录

    • 1. 并行排序
    • 2. 并行查找
    • 3. 并行字符串匹配
    • 4. 并行搜索
    • 5. 总结

时间复杂度是衡量算法执行效率的一种标准。但是,时间复杂度 != 性能。即便在不降低时间复杂度的情况下,也可以通过一些优化手段,提升代码的执行效率。即便是像10%、20%这样微小的性能提升,也是非常可观的。

算法的目的就是为了提高代码执行效率。当算法无法再继续优化的情况下,该如何来进一步提高执行效率呢?

一种非常简单又非常好用的优化方法,就是并行计算

1. 并行排序

假设要给8GB的数据进行排序,并且,机器的内存可以一次容纳这么多数据。对于排序来说,最常用的就是时间复杂度为O(nlogn)的三种排序算法,归并排序、快速排序、堆排序。从理论上讲,这个排序问题,很难再从算法层面优化了。而利用并行的处理思想,可以轻松将排序问题的执行效率提高很多倍。实现思路有两种。

  • 归并排序并行处理。将8GB的数据划分成16个小的数据集,每个集合包含500MB的数据。我们用16个线程,并行对这16个500MB的数据集进行排序。这16个小集合分别排序完之后,再将这16个有序集合并。

  • 快速排序并行处理。扫描一遍数据,找到数据所处的范围区间。把这个区间从小到大划分成16个小区间。将8GB的数据划分到对应的区间中。针对这16个小区间的数据,启动16个线程,并行地进行排序。等16个线程都执行结束,得到的数据就是有序数据了。

两种处理思路都是分治思想,数据分片,并行处理。区别在于,第一种处理思路是,先随意地对数据分片,排序之后再合并。第二种处理思路是,先对数据按照大小划分区间,然后再排序,排完序就不需要再处理了。

如果要排序的数据不是8GB,而是1TB,那问题的重点就不是算法的执行效率了,而是数据的读取效率。因为1TB的数据肯定是存在硬盘中,无法一次性读取到内存中,这样在排序的过程中,有频繁地磁盘数据的读写。如何减少磁盘的IO操作,就变成了优化的重点。

2. 并行查找

散列表是一种非常适合快速查找的数据结构。

如果是给动态数据构建索引,数据不断加入时,散列表的装载因子会越来越大。为了保证散列表性能不下降,就需要对散列表进行动态扩容。对如此大的散列表进行动态扩容,一比较耗时,一比较消耗内存。比如,给一个2GB大小的散列表进行扩容,扩到原来的1.5倍,也就是3GB大小。这个时候,实际存储在散列表中的数据只有不到2GB,所以内存的利用率只有60%,有1GB的内存是空闲的。

  • 实际上,我们可以将数据随机分割成k份(比如16份),每份中的数据只有原来的1/k,我们针对这k个小数据集分别构建散列表。这样,散列表的维护成本就变低了。当某个小散列表的装载因子过大的时候,我们可以单独对这个小散列表进行扩容,而其他散列表不需要进行扩容。

还是刚才那个例子,假设现在有2GB的数据,我们放到16个散列表中,每个散列表中的数据大约是150MB。当某个散列表需要扩容的时候,我们只需要额外增加150*0.5=75MB的内存(假设还是扩容到原来的1.5倍)。不管从扩容的执行效率还是内存的利用率上,这种多个小散列表的处理方法,要比大散列表高效

  • 查找某个数据时,只需通过16个线程,并行地在16个散列表中查找。查找性能,比一个大散列表的做法,并不会下降,反倒有可能提高。

  • 当往散列表中添加数据时,可以选择将新数据放入装载因子最小的那个散列表中,有助于减少散列冲突。

3. 并行字符串匹配

之前学过的字符串匹配算法有KMP、BM、RK、BF等。如果处理的是超级大的文本,处理的时间可能就会变得很长,如何加快匹配速度?

  • 把大的文本,分割成k个小文本。假设k是16,我们就启动16个线程,并行地在这16个小文本中查找关键词,这样整个查找的性能就提高了16倍。16倍效率的提升,从理论的角度来说并不多。但对于真实的软件开发来说,是一个非常可观的优化。

这里还有一个细节要处理,大文本中的关键词,被一分为二,分割到两个小文本中,会导致尽管大文本中包含这个关键词,但在16个小文本中查找不到它。需要针对这种特殊情况,做特殊处理。

假设关键词的长度是m。在每个小文本的结尾和开始各取m个字符串。前一个小文本的末尾m个字符和后一个小文本的开头m个字符,组成一个长度是2m的字符串。在这个长度为2m的字符串中再重新查找一遍,就可以补上刚才的漏洞。

4. 并行搜索

前面学习过好几种搜索算法,它们是广度优先搜索、深度优先搜索、Dijkstra 最短路径算法、A*启发式搜索算法。对于广度优先搜索算法,也可以将其改造成并行算法。

广度优先搜索是一种逐层搜索的搜索策略。基于当前这一层顶点,可以启动多个线程,并行地搜索下一层的顶点。在代码实现方面,原来广度优先搜索的代码实现,是通过一个队列来记录已经遍历到但还没有扩展的顶点。现在,经过改造之后的并行广度优先搜索算法,需要利用两个队列来完成扩展顶点的工作。

假设这两个队列分别是A和B。多线程并行处理队列A中的顶点,并将扩展得到的顶点存储在队列B中。等队列A中的顶点都扩展完成之后,队列A被清空,再并行地扩展队列B中的顶点,并将扩展出来的顶点存储在队列A。两个队列循环使用,就可以实现并行广度优先搜索算法。

5. 总结

并行计算是一个工程上的实现思路,尽管跟算法关系不大,但在实际的软件开发中,它确实可以非常巧妙地提高程序的运行效率,是一种非常好用的性能优化手段

特别是,当要处理的数据规模大之后,我们无法通过继续优化算法,来提高执行效率的时候,就需要在实现的思路上做文章,利用更多的硬件资源,来加快执行的效率。所以,在很多超大规模数据处理中,并行处理的思想,应用非常广泛,比如MapReduce就是一种并行计算框架。

课后思考

假设有n个任务,为了提高执行的效率,希望能并行执行,但是各个任务之间又有一定的依赖关系,如何根据依赖关系找出可以并行执行的任务?

答:拓扑排序,没有依赖关系的,可以并行处理

你可能感兴趣的:(算法,《数据结构与算法之美》学习笔记)