通过编程语言的突破性创新满足并行计算的新格局

Yue Chen · August 30, 2022

通过编程语言的突破性创新满足并行计算的新格局

计算处理器的发展有两种方式,第一种是延长线创新,在不改变计算架构的情况下,通过先进的3nm或更低线宽的半导体制程技术提升单位面积晶体管密度,获得性能或功耗的改善。另一种通过多核低功耗处理器、软件和芯片的协同设计和先进的供电、散热和芯粒封装等技术,进行计算架构的系统性创新,在制程技术不变的情况下,持续获得成本、性能和功耗的改善,这种由几十、上百个甚至上千个CPU、GPU和领域应用加速器组成的复杂芯片系统不仅用于终端设备,而且也会广泛应用于云计算、汽车、机器人和其他工业领域。为了更有效的使用这些异构并行芯片系统,软件系统必须提供更高的开发效率,更高的性能和良好的通用性。

而如下表所示,当前并行计算编程语言技术难以在上述三方面都达到最优(Michale Wong),而只能选择优化其中的两个属性。 (The landscape of Parallel Programming Models Part 1 - IXPUG,https://www.ixpug.org/resources/download/keynote-the-landscape-of-modern-parallel-programming-using-open-standards)

作为主力系统编程语言,C/C++ 语言选择了优化性能和通用性,但是难以像Python那样简单易用,而Python语言的能效比只能达到C/C++语言的1/75,参见下图。

Programming Language Productivity Performance Generality
C++   x x
Python x   x
Java x   x
SQL x   x
OpenMP x x  

Source: ACM Digital Library, “Energy efficiency across programming languages: how do energy, time, and memory relate?” https://dl.acm.org/doi/pdf/10.1145/3136014.3136031

具体到移动应用领域,虽然并行异构的移动SoC已经出现了10年以上,但是无论是iOS还是Android体系都还未为开发者提供足够好的并行编程语言和开发工具,满足性能、效率和通用性的要求。在性能方面,线程爆炸、优先级失调等对多线程的错误使用反而降低了应用性能;在编程语言层面缺乏描述异步控制机制的语意,缺乏对共享可变内存的良好抽象,造成了回调地狱,数据竞争等难以开发、难以调试的问题;并且,不同的语言和操作系统对异构并行的抽象在编程语言语意和API上互不兼容,应用难以跨平台。综上所述,当前的并行软件开发技术被铁三角定律锁死,不能在效率、性能、通用性上同时达到最优,难以满足对新形态的异构并行芯片架构的支持

  • 新的异构并行硬件的底层软件都采用C/C++ 语言开发,C++ 语言虽然还在演进,但是受到大量的存量代码拖累,难以做激进的变革,而且其标准制定过程达成共识的速度也较慢(Google开发新语言Carbon,因为其对C++ ABI的建议不被C++标准委员会接受),难以迅速采纳编程语言的最新成果来提升并行应用的开发效率问题。因此,对新并发硬件的底层软件的支持速度会比较慢,成本会比较高。

https://github.com/carbon-language/carbon-lang/blob/trunk/docs/project/difficulties_improving_cpp.md?utm_source=thenewstack&utm_medium=website&utm_campaign=platform

  • 另一方面,应用开发语言,如:Python,JS/TS,Go,甚至Swift在性能、通用性、安全性上难以匹敌C/C++,不能作为系统语言来开发firmware,驱动程序,编译器,运行时和OS内核等系统软件。

  • 同时,系统编程语言和应用编程语言的生态分野,造成大量的软件开发人员选择进入体验好、开发效率高而且市场需求大的的应用编程领域,造成C/C++开发者逐步减少,尤其缺少新一代的系统软件开发者,造成了计算系统领域创新(OS、计算框架等)的巨大的人才断层。

为了支撑新的计算领域的快速创新,我们必须打破并行计算领域的铁三角定律制约,在支撑异构并行计算的编程语言、编译器和运行时、操作系统、计算框架领域做全栈创新。

提高并行软件开发效率

基于过去10年以来在移动和云计算领域支持并行计算的经验和教训,主流编程语言都在不断演进来更好的支持并行计算应用,例如:Go语言通过goroutine支持大规模网络并发应用,Swift语言对异步并发语意的支持,对基于共享可变数据方式并行计算的actor模式抽象,正如Chris Lattner在Swift并发宣言中提出的目标,未来的并发并行软件系统,可以让开发者可以做数据抽象,并发抽象和并很好的理解他们系统,而这样的软件系统可以在运行在多核甚至多机系统上,而不需要每次重新编写程序。

https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782

同时,是否存在统一应用编程语言和系统编程语言的可能性?这样大大减少了开发者的学习成本,也减少了产业需要维护多个不兼容的编程语言体系的成本。这样的统一编程语言,不可能是对现有编程语言的简单叠加,而是作为一个元编程语言,模拟既有编程语言的语意,但是在编译器和运行时层面做统一。

元编程是一种编程技术,其中计算机程序能够将其他程序视为其数据。 这意味着一个程序可以被设计为读取、生成、分析或转换其他程序,甚至在运行时修改自身。在某些情况下,这允许程序员最大限度地减少表达解决方案的代码行数,从而减少开发时间。 它还允许程序更灵活地有效处理新情况而无需重新编译。- Wikipedia

例如,C++ 的模版是图灵完整的,意味着基于 C++ 的模版可以开发功能完整的新语言,新的语言被视为C++ 应用的数据,在运行时被实时解释和执行,这样可以为特定领域应用开发出新的DSL,同时又可以共享C++ 的编译器、运行时和其他工具链。例如:ChaiScript语言是基于C++ 模版实现的一种语意接近JavaScript的脚本语言,提供给开发者类似JavaScript的用户体验,又能获得C++ 的性能和通用性。

https://rtraba.files.wordpress.com/2015/05/cppturing.pdf https://github.com/ChaiScript/ChaiScript

提高并行应用开发的性能

C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for [Stroustrup, 1994]. And further: What you do use, you couldn’t hand code any better.

– Stroustrup

C++ 之所以能作为系统编程语言,其核心思想是zero-overhead abstraction,即零成本抽象。C++的创造者Stroustrup首先提出了这个概念,它涵盖了编程语言的编译器和运行时的性能目标,第一个原则是不需要为不使用的特性付出语言运行时的性能代价,其次,对需要使用的特性,编译器可以产生出媲美手写优化性能的机器代码。

Python,Go,JS/TS这些应用编程语言都带有相当可观的运行时,提供动态内存管理、轻量级线程管理等能力,降低了软件开发学习成本,提升了软件开发效率,但是也付出了性能代价,例如:对内存使用的跟踪和回收,对动态数据类型的猜测和实现,对软件对象泛型的支持等。而在现实世界的计算场景里,为了达到高性能而可以取舍的设计空间是不同的,例如:对于服务器和云端计算,可以容忍比较大的应用分发尺寸,有较为单一的计算环境,对应用加载时间也没有太多约束,可以打开大量静态编译优化选项,可以采取内存空间换取运行速度的静态泛型设计策略,获得性能收益。而在移动端,应用分发大小决定了下载和应用启动时间,是非常重要的优化指标,目标硬件平台也五花八门,很难预先做编译优化,为此一般采用byte code分发格式结合JIT等动态编译技术达到降低应用分发大小,同时受制于运行内存大小,需要通过动态泛型来减少内存使用。可见,语言、编译器和运行时的设计不可能采用one-size-fits-all的设计方式,而需要结合应用场景来选择不同的优化策略。零成本抽象提供了面向计算场景的定制能力,使用不同的语言特性来做实现空间和时间的置换、可以在静态编译和运行时开销方面做取舍,以达到在既定应用场景下的并行应用性能最优。

提高并行应用的通用性

C++语言作为系统编程语言,在并行计算能力上,提供了POSIX标准的pthread线程库,作为基础OS的提供的多线程能力开放给应用编程语言,应用编程语言可以对OS的多线程能力做简单的包装,提供给用户使用。为了支持更好的多线程的开发体验,应用编程语言也会提供语言原生的轻量级多线程能力,又叫做纤维(fiber),绿色任务(green task)或者协程(coroutine),相比OS的多线程,协程具有内存占用少,切换速度快,使用体验好等特点,并且不同OS实现的多线程API并不完全兼容,而语言协程则是和OS无关的,通过语言运行时和OS的多线程能力对接。下表对比了OS多线程和语言协程的能力,https://kerkour.com/cooperative-vs-preemptive-scheduling

    | 操作            | 协程              | 线程             |
    |----------------|------------------|------------------|
    | 创建            | 0.3 microseconds | 17 microseconds  |
    | 上下文切换       | 0.2 microseconds | 1.7 microseconds |

主流编程语言例如:GO,Swift,C##,Kotlin,JavaScript都提供基于协程实现协作式或者抢夺式的异步并发和并行的应用开发能力。而其他并行应用模式,例如:通过actor模式实现基于共享可变数据的并行化,利用声明式编程实现计算密集型应用的数据和算法的并行化(例如:Pytorch,Tensorflow),更加依赖于编程语言的并行化开发能力,而不是操作系统。通用性的并行计算能力成为当下编程语言生态竞争的主要战场。

Rust有望打破并行计算的铁三角规律的限制

系统编程语言具备的性能,通用性和通过元编程能力实现的开发效率提升,有望打破制约并行计算发展的铁三角定律,但相比GO,Swift等语言,C++ 语言在支持新的异构并发和并行计算能力上发展较慢。而业界普遍认为Rust语言将会取代部分甚至大部分C++语言在系统编程语言领域的地位,也会在并行计算领域承担起更重要的作用。

Rust语言的内存安全使用机制同时提升了并行应用的开发效率和性能

Rust独创性的实现了对内存使用的所有权(ownership),借用检查(borrow checker),生命周期(lifecycle)和智能指针(Smart Pointer)等语言特性,通过强大的编译器技术,基本杜绝了和不当内存使用相关的软件代码错误,而且为此付出了极小的代价,Rust的能效比和C/C++ 是一个量级的。这些内存安全技术,为异构并发并行应用提供了新的工具。C/C++ 应用的高性能多来源于可以直接操作内存,减少高层应用编程语言因为数据抽象和数据隔离带来的性能损失,这也是传统并行计算受制于铁三角定律的一个主要原因,即易用性和性能的矛盾。Rust的内存安全语意和编译器技术给予开发者对内存使用的直接控制,达到了高性能和高开发效率,这种高开发效率并不完全体现在代码开发阶段,而包括了应用交付后的运行和维护的成本。

Rust强大的零成本抽象能力为领域问题提供了的高度优化的性能保障

如上文所述,传统的应用编程语言采用one-size-fits-all的语言特性和运行时设计,局限了针对领域问题的设计选择,因而难以获得性能优势。Rust作为系统编程语言把零成本抽象作为其最重要的设计原则,相对于C++,Rust解决一个问题可以使用的方法更多,例如:支持基于generics的静态分发,也支持基于trait object的动态分发能力,针对不同的场景可以选择不同的对象分发方式。又比如,Rust的异步并发并不自带运行时实现,由用户根据自己的应用场景来实现,出现了例如:tokio,rayon,lunatic等不同风格的异步并发运行时实现方案。

Rust支持异步并发和并行计算需要的现代编程语意

在过去10多年的异步并发和并行计算应用开发过程中,一个趋势是多线程计算能力从以OS提供的多线程API为主,转变为以编程语言提供的并行计算语意和框架库为主,async/await,actor等语意已经成为事实标准,但是因为各个编程语言的生命周期,这些并行并发计算的语意在编程语言层面的支持并不同步,C++,Java等语言对这些语意的支持和生态接受度相比Rust、Go、Swift、C##等语言差距比较大。而Rust对并行并发语意的支持可以完全对标Go(大规模coroutine并发,channel消息机制),Swift(async/await,actor)。Rust的语意扩展可以在核心语言实现,例如:加入async/await,支持异步并发,因为async/await被公认为是描述异构并发控制流程的标准语意,也可以通过library扩展并行计算语意,例如:rayon提供并行迭代器(parallel interator)来支持数据并行化计算。

Rust可以通过元编程语言能力来构建应用开发语言能力

Rust引入了内存安全、零成本抽象等概念,以获得和系统软件编程的效率和高性能,但是要使用这些特性需要开发者对计算机体系架构有比较深入的理解,这也成为Rust学习曲线陡峭的主要原因。降低Rust的学习和使用难度成为Rust社区的主要目标之一。

(Benefits and Drawbacks of Adopting a Secure Programming Language: Rust as a Case Study,https://www.usenix.org/conference/soups2021/presentation/fulton)

https://lang-team.rust-lang.org/roadmaps/roadmap-2024.html#Theme-Flatten-the-learning-curve

Rust可以进一步简化语意概念,提供更好的编译器对错误的指示,但是如果要作为可以对标Python和JavasScript等最流行的应用开发语言,Rust需要支持动态类型等基本的易用性,而和Rust作为系统编程语言的零成本抽象原则产生严重的矛盾。不过,Rust具备强大的元编程能力,通过macro,proc macro等特性可以构建支持动态类型的脚本语言和领域特性语言,例如:Rhai就是一个嵌入在Rust内支持和JavaScript语意对等的脚本语言。还有一些在Rust语言直接引入基于垃圾回收(Garbage Collector)方式的动态类型的例子. https://manishearth.github.io/blog/2021/04/05/a-tour-of-safe-tracing-gc-designs-in-rust/

此外,Rust具备构建编译器的能力,不仅仅为其他语言,事实上Rust的前端编译器rustc就是Rust语言开发的,如果应用编程语言的编译器/解释器可以通过Rust实现,自然也可以作为Rust应用的一部分分发,用于动态编译和解释应用开发语言。

https://swc.rs/

综上所述,基于Rust语言来构建面向未来异构并行处理器的软件体系有望突破目前系统和应用编程语言生态的分裂的现状,打破制约并行计算发展的铁三角定律,使用Rust语言构建兼顾效率,性能和通用性的并行计算软件生态是大有可为的。

Twitter, Facebook