关于 Unified Parallel C 中 upc_forall 语句的使用与技巧
简介: UPC 并行编程语言在国外已经得到重用,然而在国内学习该语言的材料还比较匮乏。继《浅谈并行编程 Unified Parallel C 》整体概括地介绍 UPC 语言之后,我们将针对 UPC 对 C 语言所进行的扩展,与多伦多的 UPC 开发和测试人员合作,推出一系列关于 UPC 专题的文章。 本文主要以 UPC 中 upc_forall 语句为专题,本文通过大量的程序实例,使 UPC 语言的学习者,可以比较全面地学习关于 upc_forall 的使用及技巧。
upc_forall 语句可以用来为不同的线程进行分工。如果不对每个线程进行分工的话,一个程序将会被所有的线程执行。如清单 1 的例子所示。
清单 1. UPC 语言的 Hello world
# include <upc.h> // 假定由 4 个线程执行该程序 , 即 THREADS=4
# include <stdio.h>
int main()
{
printf("Hello world! (THREAD %d of %d THREADS)\n", MYTHREAD, THREADS);
return 0;
}
|
图 1. 清单 1 程序输出
![图 1. 清单 1 程序输出](https://i-blog.csdnimg.cn/blog_migrate/6d01816f5314603abbf108dfe44b1db7.png)
upc_forall 语句与 C 语言中的 for 语句非常相似。for 语句有 3 个关系表达式,而 upc_forall 在 for 语句的基础上,增加了第四个参数:亲缘关系表达式。亲缘关系表达式决定了哪个线程来执行当前的循环。
upc_forall 语句的一般形式为:
upc_forall ( 表达式 1; 表达式 2; 表达式 3; 亲缘关系表达式 )
|
每个线程对 upc_forall 语句中前三个表达式的语法,与 C 语言 for 语句中的三个表达式语法相同,具体请参照 C 语言 for 语句的用法,在此不作赘述。我们下面来重点分析亲缘关系表达式。通过亲缘关系表达式,upc_forall 语句对各个线程进行分工。
upc_forall 语句的亲缘关系表达式有下面 4 种情况:
如果亲缘关系表达式为整型表达式时,则由 MYTHREAD 值为 n 的线程来来执行当前的循环 (n= 亲缘关系表达式 % 运行程序的总线程数)。如清单 2 的例子所示。
清单 2. 亲缘关系表达式为整形表达式例子
# include <upc.h> // 假定由 10 个线程执行该程序 , 即 THREADS=10
# include <stdio.h>
int main()
{
upc_forall (int i=0; i<15; i++; i)
{
// 当 (MYTHREAD == i%THREADS), 下面语句由 MYTHREAD 执行
printf(“Hello world! ( 当 i=%d 的时候,此语句由线程 %d 执行 )\n”, i, MYTHREAD);
}
return 0;
}
|
例如,当 i 为 6 的时候,则有 MYTHREAD 值为 n 的线程来运行当前循环。因为 n= 亲缘关系表达式 % 运行程序的总线程数 =6%10=6,即线程 6 来执行当前循环。同理,当 i 为 12 的时候,再对亲缘关系表达式进行模运算,MYTHREAD=12%10=2, 即线程 2 来执行当前循环。详情,请参照图 2 中的程序输出。
图 2. 清单 2 程序输出
![图 2. 清单 2 程序输出](https://i-blog.csdnimg.cn/blog_migrate/127a56e03fdfc928a6e5a60dd63387ec.png)
情况 2
如果亲缘关系表达式为指向共享数据指针类型时,则由与该指针所指向的共享内存地址有亲缘关系的线程来执行 , 即由 MYTHREAD 值为 upc_threadof(亲缘关系表达式 )的线程来执行当前的循环。upc_threadof(亲缘关系表达式 )函数返回与其参数指向的共享数据具有亲缘关系的线程索引。如清单 3 中的例子所示。
清单 3. 亲缘关系表达式为指向共享数据指针类型例子
# include <upc.h> // 假定由 4 个线程执行该程序 , 即 THREADS=4
# include <stdio.h>
shared[2] int A[3*THREADS];
int main()
{
upc_forall (int i=0; i<3*THREADS; i++; &A[i])
{
// 当 MYTHREAD == upc_threadof(&A[i]), MYTHREAD 执行下面语句
A[i]= i; // 线程对数组 A 进行初始化
printf(“A[%d]=%d ( 当 i=%d 的时候,此语句由和 A[%d] 元素具有亲缘关系的线程 %d 执行 )\n”,
i, A[i], i, i, MYTHREAD);// 线程对数组 A 进行初始化
}
return 0;
}
|
在清单 3 的例子中,shared[2] int A[3*THREADS] 数组,布局类型限定词是 [2],有此我们可以得知 A[0], A[1],A[8], A[9] 和线程 0 有亲缘关系;A[2], A[3], A[10], A[11] 和线程 1 有亲缘关系;A[4], A[5] 和线程 2 有亲缘关系;A[6], A[7] 和线程 3 有亲缘关系。
因此,我们可以得出结论:
- 线程 0 对数组元素 A[0], A[1], A[8], A[9] 进行初始化。
- 线程 1 对数组元素 A[2], A[3], A[10], A[11] 进行初始化。
- 线程 2 对数组元素 A[4], A[5] 进行初始化。
- 线程 3 对数组元素 A[6], A[7] 进行初始化。
参见图 3 程序输出。
图 3. 清单 3 程序输出
![图 3. 清单 3 程序输出](https://i-blog.csdnimg.cn/blog_migrate/3562987b1ad914aa788272f14d8ed289.png)
情况 3
当亲缘关系表达式为关键字 continue 的时候,所有线程执行当前的循环。见清单 4 的例子
清单 4. 亲缘关系表达式为关键字 continue 的例子
# include <upc.h> // 假定由 5 个线程执行该程序 , 即 THREADS=5
# include <stdio.h>
int main()
{
upc_forall (int i=0; i<3; i++; continue)
{
printf(“Hello world! ( 当 i=%d 的时候,此语句由线程 %d 执行 )\n”, i, MYTHREAD);
}
return 0;
}
|
如图 4 输出所示,每个线程执行当前的循环。例如当 i=0 的时候,所有线程执行了当前循环。
图 4. 清单 4 程序输出
![图 4. 清单 4 程序输出](http://www.ibm.com/developerworks/cn/linux/l-cn-upcforall/image004.jpg)
情况 4
当亲缘关系表达式缺省的时候,每个线程执行当前的循环。
清单 5. 亲缘关系表达式缺省的例子
# include <upc.h> // 假定由 5 个线程执行该程序 , 即 THREADS=5
# include <stdio.h>
int main()
{
upc_forall (int i=0; i<3; i++;)
{
printf(“Hello world! ( 当 i=%d 的时候,此语句由线程 %d 执行 )\n”, i, MYTHREAD);
}
return 0;
}
|
图 5 清单 5 程序输出
![图 5 清单 5 程序输出](https://i-blog.csdnimg.cn/blog_migrate/0f4dfd8c66f23e726a010a91bc636c72.png)
如图 5 所示,所有线程执行当前循环。通过图 5 和图 4 的比较我们可以发现当亲缘关系表达式为关键字 continue 或者亲缘关系表达式缺省的情况下,所有线程执行当前循环。至于线程执行的先后顺序,是随机的,可能会改变的。
upc_forall 语句是可以直接,或者是间接通过函数的调用嵌套在其它的 upc_forall 语句中。如果一个 upc_forall 语句内部还嵌套着一个或者多个 upc_forall 语句,那么最外面一层亲缘关系表达式不是关键字 continue 的 upc_forall 语句为控制性 upc_forall 语句。嵌套在控制性 upc_forall 语句中的所有 upc_forall 语句的行为好比它们的亲缘关系表达式全部为关键字 continue。参见清单 6 的例子
清单 6. 控制性 upc_forall 语句例子
#include <upc.h>
#include <stdio.h>
#define NTHREADS THREADS
#define ARR_SIZE (NTHREADS*2)
shared int array[ARR_SIZE];
int main () {
short i,j;
int rc=0;
for (i=0;i<ARR_SIZE;i++) {
array[i]=0;
}
upc_barrier;
upc_forall(i=0;i<NTHREADS;i++;i) {
// 此处的 upc_forall 语句为控制性 upc_forall 语句。
// 符合控制性 upc_forall 语句的定义,最外层亲缘关系
// 表达式不为关键字 continue 的 upc_forall 语句。
upc_forall(j=0;j<ARR_SIZE;j++;j) {
// 控制性 upc_forall 语句中嵌套的所有 upc_forall 语
// 句的行为好比它们的亲缘关系表达式为关键字
//continue。换言之,即控制性 upc_forall 语句内嵌
// 套的 upc_forall 语句亲缘关系表达式不再起到对线程 // 进行分工的作用。
array[i] += 1;
#ifdef DEBUG
printf ("i=%d\tmythread=%d\tarray[%d]=%d\n",i,MYTHREAD,i,array[i]);
#endif
}
}
upc_barrier;
if (MYTHREAD==0) {
for (i=0;i<NTHREADS;i++) {
rc += (array[i]==ARR_SIZE?0:1);
}
rc = rc?66:55;
}
return rc;
}
|
在 UPC 中,一个线程对本地共享内存的访问速度要大大快于对其他线程的共享内存的访问速度, 所以从程序优化的角度来说,要尽量让线程来访问本地共享内存中的数据,减少对其它线程的共享内存的数据进行远程访问。
upc_forall 的语句用来为多线程进行分工,其亲缘关系表达式的设定会影响到程序的性能。如何优化 upc_forall 语句,来提高程序性能,关键就是在于如何设置 upc_forall 语句的亲缘关系表达式,使线程对本地共享内存数据的访问达到最大化。
下面我们通过具体的例子来说明。在清单 7 的例子中,由于亲缘关系表达式设置不当,致使所有线程对数组 A 和数组 B 的元素访问类型均为远程共享内存访问,不利于程序性能。
清单 7. 亲缘关系表达式设置不当的例子
# define ARRSIZE (3*THREADS) // 假定由 4 个线程来运行该程序
shared int A[ARRSIZE];
shared int B[ARRSIZE];
upc_forall (int i=0; i<ARRSIZE; i++; i+1) // 亲缘关系表达式设置为 i+1
{
A[i] = B[i]/2; // 线程对数组 A 和数组 B 的元素的访问均为远程共享内存访问
}
|
数组 A 和数组 B 的布局类型限定词为缺省,这种情况下其默认值为 1. 关于共享数组如何在线程内存之间分配,请参阅“浅谈并行编程 Unified Parallel C”中的“共享和私有数据”部分。根据其布局类型限定词的值为 1,程序由 4 个线程运行,我们可以得出以下结论:
- 线程 0 和 A[0], B[0], A[4], B[4],A[8],B[8] 有亲缘关系
- 线程 1 和 A[1], B[1],A[5], B[5] 有亲缘关系
- 线程 2 和 A[2], B[2], A[6], B[6] 有亲缘关系
- 线程 3 和 A[3], B[3], A[7], B[7] 有亲缘关系
而该程序的亲缘关系表达式 i+1 使得每个线程对数组 A 和数组 B 元素的访问类型均是远程共享内存访问:
- 线程 0 访问 A[3], B[3], A[7], B[7]
- 线程 1 访问 A[0], B[0], A[4], B[4],A[8],B[8]
- 线程 2 访问 A[1], B[1],A[5], B[5]
- 线程 3 访问 A[2], B[2], A[6], B[6]
为了改善程序的性能,对 upc_forall 语句进行优化,我们必须重新设定亲缘关系表达式,以达到各个线程访问本地共享内存数据的目的。在清单 8 的例子中,亲缘关系表达式被修改为 i.
清单 8. 亲缘关系表达式设置改进的例子
#define ARRSIZE (3*THREADS) // 假定程序由 4 个线程运行
shared int A[ARRSIZE];
shared int B[ARRSIZE];
upc_forall (int i=0; i<ARRSIZE; i++; i) // 亲缘关系表达式由原来的 i+1 变为 i
{
A[i]=B[i]/2; // 线程对数组 A 和数组 B 的元素的访问均为本地共享内存访问
}
|
经过对亲缘关系表达式的优化,每个线程访问类型均为本地共享内存访问,即每个线程访问的数据均与该线程具有亲缘关系,情况如下:
- 线程 0 访问 A[0], B[0], A[4], B[4],A[8],B[8]
- 线程 1 访问 A[1], B[1],A[5], B[5]
- 线程 2 访问 A[2], B[2], A[6], B[6]
- 线程 3 访问 A[3], B[3], A[7], B[7]
我们再看一个亲缘关系表达式设置不当的例子,如清单 9 的例子。
清单 9. 亲缘关系表达式设置不当的例子
# define ARRSIZE (3*THREADS) // 假定程序由 4 个线程运行
shared int A[ARRSIZE];
shared int B[ARRSIZE];
upc_forall (int i=0; i<ARRSIZE-1; i++; &A[i+1]) // 亲缘关系表达式为 &A[i+1]
{
A[i]=B[i]/2; // 线程对数组 A 和数组 B 的元素的访问均为远程共享内存访问
}
|
这个例子中的数组 A 和数组 B 在内存中的分布与清单 7 和清单 8 中的数组 A 和数组 B 是一样的,亲缘关系如下:
- 线程 0 和 A[0], B[0], A[4], B[4],A[8],B[8] 有亲缘关系
- 线程 1 和 A[1], B[1],A[5], B[5] 有亲缘关系
- 线程 2 和 A[2], B[2], A[6], B[6] 有亲缘关系
- 线程 3 和 A[3], B[3], A[7], B[7] 有亲缘关系
由于亲缘关系表达式设置不当,亲缘关系表达式 &A[i+1] 使得所有线程对数组 A 和数组 B 元素的访问类型为远程共享内存访问,情况如下:
- 线程 0 访问 A[3], B[3], A[7], B[7]
- 线程 1 访问 A[0], B[0], A[4], B[4],A[8],B[8]
- 线程 2 访问 A[1], B[1],A[5], B[5]
- 线程 3 访问 A[2], B[2], A[6], B[6]
为了改善该程序的性能,对 upc_forall 语句进行优化,我们必须重新考虑设定亲缘关系表达式,将其由原来的 &A[i+1] 变为 &A[i],如清单 10 的例子所示。
清单 10. 亲缘关系表达式改进的例子
# define ARRSIZE (3*THREADS) // 假定程序由 4 个线程运行
shared int A[ARRSIZE];
shared int B[ARRSIZE];
upc_forall (int i=0; i<ARRSIZE-1; i++; &A[i]) // 亲缘关系表达式变为 &A[i]
{
A[i]=B[i]/2; // 每个线程对数组 A 和数组 B 元素的访问均为本地共享内存访问
}
|
经过对亲缘关系表达式的优化,每个线程对数组 A 和数组 B 元素访问类型均为本地共享内存访问,即每个线程访问的数据均与该线程具有亲缘关系,情况如下:
- 线程 0 访问 A[0], B[0], A[4], B[4],A[8],B[8]
- 线程 1 访问 A[1], B[1],A[5], B[5]
- 线程 2 访问 A[2], B[2], A[6], B[6]
- 线程 3 访问 A[3], B[3], A[7], B[7]
upc_forall 语句的使用技巧的核心就是将线程对本地共享内存的访问最大化 , 这也是对 UPC 程序进行优化的最核心的思想。因为亲缘关系表达式在 upc_forall 的语句中决定了线程如何分工,所以 upc_forall 语句的使用技巧,关键在于亲缘关系表达式的设定。为了优化程序性能,我们需要巧妙地设置亲缘关系表达式,尽量让线程去访问与之有亲缘关系的数据。
学习
- 参考 “UPC Language Specifications V1.2”, 了解更多关于 UPC 语言的细节规定和用法介绍。
- 参考“UPC Manual”,了解更多关于 UPC 入门的知识。
- 参考“UPC 相关文档”,了解关于 UPC 方面的内容。
- 参考“浅谈并行编程 Unified Parallel C”,了解 UPC 的基本知识。
- 在 developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料,查阅我们 最受欢迎的文章和教程。
- 在 developerWorks 上查阅所有 Linux 技巧和 Linux 教程。
- 随时关注 developerWorks 技术活动和 网络广播。
讨论
- 加入 developerWorks 中文社区,developerWorks 社区是一个面向全球 IT 专业人员,可以提供博客、书签、wiki、群组、联系、共享和协作等社区功能的专业社交网络社区。