编译器的「五个十年」发展史

Posted 信息史学

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译器的「五个十年」发展史相关的知识,希望对你有一定的参考价值。



如果想了解我们在计算机架构和驱动计算机架构的编译器方面的现状,有必要看看编译器如何在六十年间由一种架构改用另一种架构。


先让目光回到1957年的第一个编译器IBM Fortran。这是一项了不起的技术。如果你看看它的起源和取得的成果,付出的巨大努力是今天的人都无法想象的。


IBM想要销售计算机,想要销售让更多的人能够进行编程的计算机。当时编程是用汇编语言完成的。这太难了,IBM很清楚这一点。于是蓝色巨人希望人们有办法更快地编写程序,又不牺牲性能。Fortran的开发人员(指发明Fortran的那些人,而不是发明使用编译器和语言的程序的那些人)希望利用如今所谓的高级编程语言编写的程序,提供尽可能接近手动调整的机器代码的性能。


说到编译器,你必须考虑三个P:性能、生产力和可移植性。Fortran的发明者拿机器代码的性能作了比较。生产力方面的好处是,程序员再也不必编写机器代码。我不知道IBM在可移植性方面的最初意图,不过你在1957年无法为软件获得专利权,IBM也没有抱怨其他企业组织实现Fortran。因此没过多久,市面上出现了来自其他供应商的面向其他机器的Fortran编译器。这立即为Fortran程序提供了机器代码无法想象的可移植性。


从这一刻起,编译器开始迅猛发展起来。让我们看看每个十年的情况。


第一个十年


在20世纪60年代(就在我出生前),计算机架构师和编译器编写者首先开始考虑并行性。即使那样,人们仍然认为计算机速度不够快,速度提升也不够快,觉得并行性有望解决这个问题。我们看到指令级并行性引入到了Seymour Cray公司的CDC 6600和CDC 7600以及IBM System/ 360 Model 91中。更为激进的做法是开发出了由伊利诺伊大学的研究人员设计并由Burroughs公司制造的ILLIAC IV、Control Data Corp STAR-100以及德州仪器(TI)Advanced Scientific Computer。CDC和TI的系统是内存到内存的长向量机,而ILLIAC IV是我们今天所说的SIMD机器。ILLIAC之所以功能有限,是由于没有主要的标量处理器,编程起来确实很难。面向STAR-100的Fortran编译器添加了用于描述长连续向量操作的语法。TI ASC机器最值得关注,因为它拥有第一个自动向量化编译器。TI做了一番了不起的工作,确实提升了当时编译器分析的最新水平。


第二个十年


20世纪70年代,Cray-1成为第一台商业上大获成功的超级计算机。它有众多的跟随者和模仿者,许多读者可能了如指掌。Cray机器的成功很大程度上归功于引入了向量寄存器。就像标量寄存器一样,向量寄存器让程序可以对小小的数据向量执行许多操作,没必要从内存加载和存储到内存。Cray Research还开发了一种非常大胆的向量化编译器。它在许多方面与早期的TI编译器类似,但Cray编译器拥有让它非常备受关注的附加功能。其中最重要的一项功能是能够为程序员提供编译器反馈。


如今开发人员编译程序时,如果程序含有语法错误,编译器将生成出错消息。程序员不断修复这些错误并重新编译,直到拥有一个正常运行的程序。如果开发人员希望程序运行得更快,可以启用编译器优化标志,好让生成的可执行文件运行得更快——他们希望如此。在大多数计算机上,优化代码和非优化代码之间的性能可能相差两倍,通常差异小得多,比如相差10%、20%或者50%。与之相比的是原始Cray机器上可用的向量指令集。在这种情况下,代码被Cray编译器优化和向量化后,程序员常常会看到性能提升5倍到10倍。程序员、尤其是高性能计算(HPC)程序员愿意做大量工作,以便将性能提升5倍或更高。


来自Cray编译器的反馈将告诉程序员它在第110行向量化了一个循环(loop),在第220行向量化了另一个循环,但是没有在第230行向量化循环,原因是第235行有无法被向量化的I/O语句或函数调用。或者,可能存在编译器无法分析的数组引用,或者某个数组的第二个下标中的一个未知变量阻碍了依赖项分析,因此该循环无法进行向量化。想要获得向量性能的Cray程序员格外注意这些消息。他们根据这种反馈修改了代码,可能从循环中取出I/O语句,或者将循环推入到子程序,或者修改数组引用以删除某个未知变量。有时他们会添加一个编译器指令,以便向编译器传达可以安全地进行向量化这一信息,即使编译器无法通过依赖项分析来确定这一点。


多亏了编译器的反馈,发生了三件事。


首先,更多的程序被向量化,更多的程序员得益于Cray向量性能。其次,Cray程序员受过了培训,不再将I/O语句、条件语句和过程调用放入到循环的中间。他们明白一个步长(stride)的数据访问很重要,确保内部循环中的数组访问是一个步长。第三,用Cray编译器自动向量化的程序可以在来自Alliant、Convex、Digital、富士通、日立、IBM和NEC的许多类似的向量机上重新编译。所有这些系统都拥有带向量寄存器的向量处理器和自动向量化编译器,之前针对Cray优化的代码在所有这些机器上都可以进行向量化,并很顺畅地运行。简而言之,Cray程序员终于实现了性能、生产力和可移植性这三个目标。


这个编译器反馈有多重要、培训整整一代HPC开发人员为向量机编程有多成功,怎么强调都不为过。每个使用它的人都很高兴。那时候,我还在伊利诺伊大学,我们考虑开一家立足于并行化编译器技术的公司。当然,我们的技术比别人的要好,因为我们是一流的学者。在Cray编译器的早期阶段,用户抱怨编译器无法对任何内容进行向量化,因而不得不重写代码。我们认为自己有望解决这些问题,于是开了这家小公司专门搞这一块。几年后,我们接触同样那样用户、展示我们用于并行化和向量化循环的工具时,他们回复自己不需要这类工具,因为Cray编译器已经向量化了所有循环。倒不是说Cray编译器变得更聪明,不过我确信它会渐渐变得更好。主要是程序员在如何编写可向量化的循环方面训练有素。


第三个十年


20世纪80年代多处理得到了广泛的实施和使用。早些时候已有多处理器,包括IBM System /370s和Burroughs系统。但是32位单片微处理器的问世推动多处理技术进入了主流。要开一家计算机公司,你不再需要设计处理器——可以径直购买。你没必要编写操作系统,可以购买Unix的许可。只需要有人全部组装起来、贴上铭牌。要是有编译器就好了。 Sequent、Encore和SGI都构建了有一个微处理器、性能出色的系统,但如果可以让一批处理器并行运行,那就更好了。


不像大获成功的自动向量化,自动并行化基本上一败涂地。它适用于最内层循环,但要实现大幅的并行加速,通常需要对外层循环进行并行化。不过当然,外层循环增添了控制流的复杂性,常常包括过程调用,现在你的编译器分析完全崩溃了。一种确实可行的方法是,让程序员参与其中,分析并行性,并输入编译器指令来驱动它。我们由此看到了面向并行循环的各种指令集纷纷出现。Cray、Encore、IBM和Sequent都有各自的指令集,唯一的共同点就是SGI采用Sequent指令。同样,许多可扩展的系统要传递网络消息,这促使开发了众多针对特定供应商的、学术性的消息传递库。


第四个十年


20世纪90年代,所有那些消息传递库都被消息传递接口(MPI)取而代之,所有那些针对特定供应商的并行化指令都被OpenMP取而代之。出现了扩展性更强的并行系统,比如Thinking Machines CM-5以及有成千上万个商用微处理器的其他系统。基于微处理器的可扩展系统主要用MPI进行编程,但MPI是一个库,对编译器来说不透明。MPI作为一种编程模型而具有的优点是,虽然通信开销很大,但通信在程序中是完全暴露的。MPI程序员知道何时插入显式通信调用,他们不遗余力地尽可能减少和优化通信。缺点是,从编程模型的角度来看,它非常低级,MPI程序员没有得到编译器的帮助。


OpenMP诞生于针对特定供应商的并行指令集百花齐放的时期,因为用户需要它。用户们才不愿仅仅为了能在可供使用的所有不同系统上运行程序而将程序重写12次。OpenMP的整个宗旨就是以同样的方式彰显“并行性可以”。不像MPI,编译器必须支持指令,因为指令是语言的一部分。你无法将OpenMP作为一个库来实现。

同样在20世纪90年代,市面上出现了面向单芯片微处理器的SIMD指令集,比如来自英特尔的SSE和SSE2,我们看到编译器恢复了当时已有20年或25年历史的同样的向量化技术,以便自动利用那些SIMD指令。


第五个十年


2000年后不久,众多供应商开始普遍提供多核微处理器。全世界突然意识到必须为大家解决这个并行编程问题。


当时,许多应用程序在一块芯片上的所有核心上以及分布式内存节点上使用扁平的MPI编程模型。你始终可以添加更多的MPI序号(rank)来使用更高的并行性。这一招效果有限,但是你开始获得大量序号时,一些MPI程序会遇到扩展问题。数据在每个MPI序号中复制时,内存使用会有点失控。一旦你开始在每个节点上放置一二十个核心,复制的每一项数据可供使用的内存量是12倍或24倍。如果是为单个节点编程,OpenMP及其他共享内存并行编程模型开始受到追捧。比如在20世纪90年代,英特尔推出了线程构建模块(又名TBB);有人声称,TBB是如今最受欢迎的并行编程语言。


大概在同一时期,异构HPC系统开始出现,不过异构性其实不是新话题。我们在20世纪60年代就遇到了异构性,使用附加的协处理器用于数组处理。附加的处理器大受欢迎,原因是它们可以比CPU更快地完成专门操作。它们常常能够以小型机的价格提供大型机的性能,大约15年来浮点系统在这方面做得很好。在21世纪初期,有几款专门为HPC市场开发或加以改造的加速器,比如ClearSpeed加速器和IBM Cell处理器。这两款产品都取得了一些真正的技术成功,但是HPC市场规模太小,无法支持开发独立的定制处理器芯片。


这给GPU计算留下了缺口。GPU相对定制加速器的优势在于,GPU的主打功能很好——早期的主要任务是图形和游戏,现在还包括AI和深度学习,因此从最新硅片技术和驱动硅片所需的软件方面来看,开发处理器非常高昂的成本维持得下去。


伴随异构性而来的是这个显而易见的问题:我们如何为这些系统编程?早在那时,浮点系统提供了在底层使用加速器的子程序库。程序员调用该库,HPC用户可以高效地使用加速器,无需实际编程(不过有一些程序员进行了编程)。今天,想支持多年来在标量、向量和可扩展系统上开发的众多应用程序,HPC开发人员需要能够为加速器高效地编程,他们希望程序看起来尽可能正常。使用OpenCL或CUDA可以为你提供了很强的控制性,但是对于现有HPC源代码的影响可能非常大。程序员通常将有待在加速器上运行的代码提取到特别注释的函数中,而且编写的方式常常与为主机编写的方式全然不同。这时候,专门为通用并行编程设计的基于指令的编程模型和语言就有一些优势,而优秀的优化编译器大有用场。


置身于平行世界


这给我们引出了另一个至关重要的方面。20世纪60年代为指令级并行性开发的所有技术现在都在微处理器里面。20世纪70年代的向量处理概念存在于微处理器里面的SIMD寄存器中。20世纪80年代Cray和基于商用处理器的系统的多个处理器都以多个核心的形式存在于微处理器里面。今天的微处理器可以说整合了过去50年来所做的全部架构工作,而由于我们现在拥有出色的晶体管技术(数十亿个门),我们可以这么做。另外我们现在还有异构性;在一些情况下,我们甚至可以在同一块芯片上做到异构性。比如说,中国太湖之光系统中的“神威”芯片从封装的角度来看是一块芯片,但芯片上每个四分之一的部分都包括一个主处理器和64个计算处理器。因此,它基本上是异构的,编程起来更像是CPU-GPU混合体,而不是像多核处理器。


为了充分利用异构的GPU加速节点,程序需要具有很强的并行性,而且是类型合适的并行性。这些高度并行处理器不是通过更快的时钟提供性能,不是由于某种异常奇特的架构。确切地说,那是由于更多的可用门用于并行核心,而这些并行核心用于缓存、乱序执行或分支预测。商用CPU的所有那些主要任务被排除在GPU之外,结果是大规模并行处理器在合适的内核和应用程序上提供更高的吞吐量。


这让我们回到了三个P,而性能不是HPC开发人员的唯一目标。程序员需要高性能、良好的生产力和广泛的可移植性。大多数程序员希望程序只编写一次,而不是多次。


在当时只有节点上的单处理器、所有并行性横跨节点的时期,程序员可以使用扁平的MPI来应付。这类程序可在一系列广泛的机器上顺畅运行,但这不再是我们现在所置身的HPC环境。我们在一个节点中有多个处理器,有SIMD指令,还有异构加速器,引入了更多类型的并行性。为了获得可移植性,我们需要能够编写针对不同数量的核心、不同的SIMD或向量长度可以高效映射的程序,以及同构或异构的系统,没必要每次改用新系统就要重写程序。为了确保生产力,我们需要把其中尽可能多的部分抽取出来。我们使用编译器为今天的HPC系统试图做到的是与IBM在六十年前使用第一个Fortran编译器做到同样的抽象质量,后者实际上是第一种任何类型的高级语言编译器。


目标是在如今极其复杂的硬件上获得尽可能接近手动编程的性能。今天,就小小的简单代码块而言,针对英伟达GPU的PGI OpenACC编译器常常可以提供接近原生CUDA的性能。对于像大型函数这种更复杂的代码序列,或者整批调用树被移植到GPU时,接近原生代码性能要困难得多。


客观地说,OpenACC编译器天生处于劣势。在CUDA中重写代码时,程序员可能查明某个数据结构不适合GPU,可能改变数据结构以增强并行性或性能。也许程序员查明程序逻辑的一部分不是很适合GPU,或者查明数据访问模式并不理想,因此重写该逻辑或循环以改进数据访问模式。如果你在对应的OpenACC程序中进行同样这些更改,常常也会获得好得多的性能。但是你最好不这么做,如果重写减慢了多核CPU上代码的运行速度,更是如此。因此,OpenACC代码可能无法获得与你花了这番编程工作量同样的性能级别。即便如此,如果OpenACC代码的性能足够接近GPU上的CUDA,基于指令的编程模型在生产力和可移植性方面的好处常常很明显。


尽管过去的60年间我们在编译器技术方面取得了诸多进展,但一些人仍然认为编译器与其说是解决办法,还不如说是问题。他们想要的是来自编译器的可预测性,而不是在后台优化编译器的高级分析和代码转换。这可能引出了这条道路:我们尝试从编译器中获取功能,将更多的责任交到程序员的手里。有人会说,这正是OpenMP今天所走的道路;我看到的危险是,我们到头来可能为了可预测性而牺牲了可移植性和生产力。比如说,设想你仍然得使用英特尔的SSE和AVX内部函数,对每个循环进行手动向量化,而不是针对英特尔至强处理器上的SIMD指令进行自动向量化。你实际上在编写内联汇编代码。这具有很强的可预测性,但很少有程序员想要在该层面编写所有的计算密集型代码,你要为每一代SIMD指令重写代码,或者在非X86 CPU上使用SIMD指令时更是如此。


计算机架构方面任何合理的进步都伴随编译器技术方面合理的进步。不能仅用那些架构和编译器所提供的性能来衡量好处,还要用程序从一代HPC系统移植到下一代(不必完全重写)后,节省的人力和提高的生产力来衡量。





以上是关于编译器的「五个十年」发展史的主要内容,如果未能解决你的问题,请参考以下文章

深度学习十年发展回顾:里程碑论文汇编

互联网医疗的下个十年,缺的不是“卖药和问诊”

十年磨一剑!万字长文剖析华为方舟编译器的前世今生

[转帖]21世纪第二个十年里,人民海军下水舰艇汇总

下个十年,Python的“王者”地位还能保住吗?

云计算下个十年:云原生