在百度实习的两个多月,在技术上还是有很多收获的,首当其冲的便是对高性能计算方面有了一些见解。
大约在四十多年前,我们的计算机还只是单线程的——它们顺序的读程序,然后交给CPU进行计算,偶尔跳转到别的地方去读写数据。随后,一个叫做操作系统的东西出现了,更高级的操作系统支持多任务,一开始的多任务很粗糙,只是把每个任务分成几个定长的时间片,然后备份CPU寄存器里的数据,轮换各个任务罢了。然后,多任务变得越来越人性化——如加入了优先级,对IO任务和计算任务加以区分等。多任务也成就了日后的线程和进程。
随着计算能力的不断提升,多任务切换所需的额外花销也就不算什么大事了。但是,单个CPU的计算能力在近些年增长缓慢。于是,我们很容易的想到,使用多个CPU去解决问题。但是,一个已有的程序更改成为可以并行计算的程序时很困难的。还有,CPU的数量是未知的,为了适应越来越多的CPU并不浪费其能力,我们建立尽可能多的线程/进程(远远大于CPU数),并行的处理任务,操作系统会帮我们处理它们之间的切换。网络的兴起使得服务器大多采用了这样的方式:每个请求都是一个线程,它们互不干扰的运算、存取、返回结果。
这里便出现了三个问题:一个是怎样把已有的程序改造成为并行程序。这让躲在学术圈的函数式语言得以大放光彩。函数式语言的几乎所有语句都是由函数组成的,而且,每个函数的调用不会修改已有的数据,也就是说,一个对象被创建后,便不会被修改。这样,首先它是线程安全的;另外,它可以在不修改代码的情况下,自适应多CPU的情况,只需将不同的函数调用分配到不同的CPU中去便可以了。但是,真正的函数式语言比较理想化,不是十分浪费内存就是十分浪费计算量,虽然写起来和听起来很好很强大的样子。所以,比较新的函数式语言,如Scala等,也兼容普通的命令式编程和面向对象的思想,虽然浪费了一些并行性,但是最近也越来越火。
第二个问题是,怎样让这些并行运行的线程之间进行通信。并行计算中的一个很常见的东西是锁,或者CAS之类的替代品,因为多个线程同时操作同一个数据时会出现难以预料的后果。但是,锁的使用一方面带来的死锁的现象,另一方面则降低了效率,就算现代的CPU硬件支持某些指令,但是在某个线程对某个对象上锁的过程中,另一个也需要访问该对象的线程便只能干等着,这浪费了CPU的很多性能。多个机器的情况下,频繁的数据通信会加重网络的负担。
第三个问题是,IO。CPU那点时间和IO比起来不值一提,对内存的读写、对硬盘的读写、网络的访问,这些都时时刻刻影响着程序的效率。并行计算虽然解放了CPU,但是并没有解放IO。
这三个问题又衍生出很多其他的问题,比如说,在多个线程在同一个CPU中运行时,CPU内部的寄存器、Cache都会被更换,这在线程数比较少的时候不算什么,但是线程数会尽可能的多,则线程之间切换的花销就不能被忽略了而且功耗也是一个问题。线程过多、IO、锁也时时刻刻影响着机器的稳定性;再比如,有些任务是IO密集型任务,有些则是计算密集型任务。我们需要把它们放到适当的机器中运行,不然则会产生浪费。但是,一个线程所在的CPU是随机的,甚至它所在的机器都是随机的。这将会造成一定的浪费。
在这些问题日益增加的情况下,我们勤劳的劳动人民想出了很多的办法。比如说,为了区分IO密集型任务和计算密集型任务,对机器们进行统一个管理和调度,把新的IO密集型任务放在计算密集型任务比较多的机器中,反之亦然;再比如说,关于线程过多计算量过大、机器容易不稳定的问题,通过减少高性能的机器而增加更多的普通机器,来让任务们各得其所。
但是这解决不了根本问题,那就是,本来计算机就是为顺序执行程序而设计的,但是我们为了让计算能力提升而逼迫程序以一种完全不同的方式去运行,这种背离了计算机本身运行方式的计算方法肯定会存在问题的。于是,我们便从另一个方向去思考这个问题:为什么不让一个机器(CPU)以它最大的可能去运行一个它最擅长的任务呢?正如,我们要分别考语文数学英语三科,我们为什么要让一个非常牛的人用两只手加一只脚分别写三科试卷,而不让三个分别擅长语文数学英语的人分别用一只手去写他们擅长的试卷呢。
所以,我们的思绪回到了几十年前,那个没有操作系统的时代,计算机是那样的高效。我们希望回到过去,让每个CPU执行单一的任务,不同任务之间采用无锁的通信方式。但这显然需要很多的工作,比如创建一个崭新的操作系统。但是,现在的许多计算框架已经有这个趋势了:它们只有几个为数不多的线程,线程数根据CPU个数而定,而且各自分工明确;每个线程都拥有一个队列或其他容器,用来进行线程之间的通信;一个机器与另外机器通信后还可以继续执行它后面的任务,也就是异步执行;它们可以很容易的移植到多个机器上,每个机器做特定的事情。
通过这样设计出的系统在性能和稳定性上都优于以往的基于多线程的系统,可以说,这是高性能计算的趋势。但是,它似乎仍然没有解决一个问题,那就是锁和IO,数据通信和对数据的写入成为了瓶颈。但是,锁是用来干啥的呢?一是用来在写数据的时候,不与同时进行的其他写数据的操作冲突,二是在分步操作数据的时候,不被同时进行的其他读数据的操作读到中间结果。但是,我们目前的存储器支持的操作都是单线程的,那我们对其的操作为啥要并行呢?所以,我们可以让一个机器只负责IO,它也有一个容器用来盛放其他机器对它的请求,它依次执行并把执行结果通知拥有请求的机器,它不需要很强大的CPU。如果IO的负担过大,可以把它分为好几个这样的部分。关于数据通信,也就是各个CPU之间的通信。在同一个机器上可以通过共享内存,可那就需要锁,于是,一些无锁队列的实现方式最近火了起来;但是,解决这个问题的终极方法只有一个,那就是制造出适应于并行计算的存储器。不同机器只能通过网络,可是网络不可靠,而且传输速度有上限。于是,很多分布式计算的理论出现了。
我在百度做的东西,Gearman和Kestrel,都采用了这样的思想。
所以,高性能计算的未来不是并行运算,而是分布式运算。
以上有感于12306...