三家研究机构汇合的早期规范
1999年,Unified Parallel C(统一并行C,简称UPC)的研究者把一组面向并行计算的C语言扩展写成《UPC导论与语言规范》。现存报告题名页标明1999年5月13日、第二次印刷,附录列出0.91版规范。这份文件既说明语言规则,也记载Cray T3E上的初始实现,留下了早期设计如何落到机器上的具体记录。
六位署名作者来自三个研究地点:William W. Carlson和Jesse M. Draper在美国马里兰州鲍伊的国防分析研究所(Institute for Defense Analyses,IDA)计算科学中心;David E. Culler和Kathy Yelick在加利福尼亚大学伯克利分校;Eugene Brooks和Karen Warren在劳伦斯利弗莫尔国家实验室。报告以1989年的C语言标准为底本,希望为显式并行编程提供共同的语法和含义。
参考:[1]
把已有的并行C经验接在一起
UPC承接了Split-C、AC和并行C预处理器(Parallel C Preprocessor,PCP)的研究。Zhang Zhang后来整理这段历史时指出,Split-C让程序能够区分本地与全局访问,并把通信分成发起和完成两个阶段;AC加强了分布数组和指针的表达;PCP则探索同一种并行编程方式怎样跨越不同内存组织。
这些经验回应的是超级计算机上的实际困难:数据分散在不同处理器的内存中,访问远处的数据会花时间,但程序员仍希望像写普通赋值那样读写它们。1999年报告一面保留共享地址空间的便利,一面要求程序员考虑数据放在哪里、由谁处理;统一写法并未抹去本地和远程访问的代价差别。
共享数据仍然有归属
UPC采用单程序多数据的执行方式:多个线程分别进入同一个程序入口,各自执行指令。声明中的shared限定词把共享对象与线程私有对象区分开来。共享表示各线程可以引用同一对象,数据同时又与特定线程有“亲和性”,为安排本地计算提供依据。
以默认分块方式声明共享数组时,相邻元素轮流分给相邻线程。假设有四个线程,编号从0到3,数组下标0、4、8等归线程0,下标1、5、9等归线程1,依此类推。程序员也可以指定较大的连续块,使相邻的一批元素留在同一线程。这种布局选择直接影响循环计算需要多少远程读写。
参考:[1]
从报告中的数组相加看工作分配
报告用三个各含100个浮点数的共享数组x、y、z说明方法,计算任务是逐项求x[i] = y[i] + z[i]。它在upc_forall循环的第四项写入&x[i],让一次迭代归到与输出元素x[i]有亲和性的线程,而不是让每个线程把全部100项重复计算。
沿用四线程、默认分块的假设,线程0处理下标0、4直到96,线程1处理1、5直到97,各做25项。三个数组按相同规则对齐,同一下标的输入和输出便可由同一线程就地处理。这个例子成立的前提是输入已经准备好,且各次迭代互不依赖;循环本身不会自动让所有线程在开始或结束时相互等待。
参考:[1]
等待之前可以做什么
多线程协作还要决定什么时候能安全读取别人的结果。报告把屏障同步分为通知和等待两步:线程先发出已经到达的通知,随后在等待处确认其他线程也已到达。程序可以在两步之间插入不依赖共享读写的本地工作,从而利用原本可能空等的时间。
这使同步位置成为程序设计的一部分。数组相加后若马上开始另一轮依赖全部结果的计算,就需要程序员安排必要的同步。UPC提供表达这些约束的办法,编译器和运行系统负责落实;它不把一个普通串行循环自动变成正确的并行程序。
参考:[1]
在Cray T3E上表达远程地址
报告记载的T3E初始实现,把指向分块共享数据的指针分成块内位置、线程号和虚拟地址几个部分。线程号对应作业中的处理器,编译器据此定位远程数据;访问时可以去掉记录块内位置的高位,保留适合远程读取和写入的地址形式。这样的安排增加了一些指针运算工作,却力求保持远程访问的效率。
1999年的成果由此同时包括早期语言规范和具体机器上的实现经验。报告也讨论怎样映射到共享内存或消息传递环境,把可移植性作为设计目标。它把数据布局、工作分配和同步放进C程序能够明确表达的结构中,使不同研究机构已有的并行编程经验获得了一套可以继续实现和检验的共同写法。