cilk之User Guide学习笔记(4)cilk_for的分析

说明:

下载User Guide: http://software.intel.com/zh-cn/forums/showthread.php?t=77996&o=a&s=lr(Cilk_User_Guide.pdf)

主要是对该用户指南(中文版)的一些学习笔记和简化并更加自己的理解添加一些代码示例,可以参考原文档获取更多细节。


1. cilk_for基础

cilk_for 循环用于取代常规的C/C++ for循环,它允许循环迭代并行执行。

cilk_for的语法规则基本如下: cilk_for (declaration; conditional expression; increment expression) body


2. 串行或并行的cilk_for结构

使用cilk_for并不同于衍生每次循环迭代。事实上,Intel编译器将循环体转换成一个函数,然后用二分法策略递归的调用它;这极大的提高了性能。这两种策略的区别可以清楚的用一个有向无环图表示出来。

cilk之User Guide学习笔记(4)cilk_for的分析_第1张图片

上图是N=8的循环的衍生过程图,其中数字表示循环迭代的i,其中4..7表示i=4567的循环迭代。下面是衍生每次迭代的串行循环的有向无环图。在这种情况下,工作是不均衡的,因为每个子任务在引起同步调度开销前只做了一次迭代的工作。对于一个短小,或者循环体执行的部分远高于控制和衍生开销的循环来说,并没有太多可测量的性能差别。但是对于具有很多简单迭代的循环来说,开销的代价将覆盖掉并行所带来的好处。

cilk之User Guide学习笔记(4)cilk_for的分析_第2张图片


3. cilk_for的粒度

cilk_for语句将循环划分到包含一次或者多次循环迭代的区块中。每个区块中的循环迭代串行执行,并且在循环的执行过程中,作为一个区块而被衍生。每个区块中包含的最大迭代次数被称为粒度。

粒度的设置:

使用cilk grainsize编译指示来指定一个cilk_for循环的粒度:#pragma cilk grainsize = expression

如果你没有指定粒度,系统计算出一个适合多数情况的缺省值。缺省值的设置如同添加了下列编译指示: #pragma cilk grainsize = min(512, N / (8*p))
其中N是循环迭代次数,p是当前程序创建的工作线程个数。这个公式将产生最少为8,最多为512的并行度。对于迭代次数很少的循环(少于8*工作线程个数)粒度就设成1,每次循环迭代都会并行执行。对于迭代次数超过(4096*工作线程个数)次迭代的循环,粒度就设成512。
如果你将粒度设成0,缺省值公式也将会被使用。如果粒度小于0,结果是未定义的。

说明:这里的缺省grainsize请参考最新的cilk文档查看,当前的最新文档的描述是:#pragma cilk grainsize = min(2048, N / (8*p))


说明:cilk_for的划分算法是采用二分递归划分的。对于cilk_for来说,其相对于一般的for还有一些限制,但是只要理解了cilk_for的工作机制,这些限制也比较容易理解了。

为了更好的理解cilk_for的相关内容,理解下面的例子:

// File: test.cpp
#include <stdio.h>
#include <cilk/cilk.h>

#define N	8

void loop_body(int i) {
	printf("loop body:%d\n",i);
}

void fun_serial_for() {
	for(int i=0;i<N;i++) {
		loop_body(i);
	}
}

void fun_cilk_for() {
	cilk_for(int i=0;i<N;i++) {
		loop_body(i);
	}
}

// 衍生每次迭代的串行循环,不平衡,每次循环都需要进行一次衍生。
void fun_spawn_cilk_for() {
	for(int i=0;i<N;i++) {
		cilk_spawn loop_body(i);
	}
	cilk_sync;
}

// 二分法,运行时动态划分,平衡
// 循环被反复的分割成两部分,直到剩下的区块中的迭代次数小于等于粒度。在一个区块中实际执行的迭代次数通常都小于粒度。
int grainsize = 2;	// 尝试修改grainsize,并理解其含义。
int divide_times=0;
#define LOOP_BODY loop_body(i)
template <typename T>
void run_loop(T first,T last) {
	if ((last - first) <= grainsize) {
		for (int i=first; i < last; ++i) LOOP_BODY;
	} else {
		int mid = first + (last-first)/2;
		divide_times++;
		cilk_spawn run_loop(first, mid);
		run_loop(mid, last); 
	}
}

void fun_runtime_cilk_for() {
	divide_times=0;
	run_loop(0, N);
	printf("Divide times:%d\n",divide_times);
}

int main()
{
//	fun_serial_for();
//	fun_cilk_for();
//	fun_spawn_cilk_for();
	fun_runtime_cilk_for();
	return 0;
}
其中,fun_cilk_for为使用cilk_for的关键字,fun_spawn_cilk_for为每次循环迭代进行衍生,fun_runtime_cilk_for为使用二分法进行动态划分,即cilk_for的内部实现,重点需要理解其划分过程和粒度的含义。当然,并不能从输出结果分析得到什么有用的信息,主要是从代码上理解。针对上面的动态划分的实现,就能理解cilk_for的基本限制,如:

为了利用二分法策略并行化一个循环,运行时系统必须提前计算迭代的总次数和每次迭代时循环控制变量的值。因此,控制变量必须像一个整数一样,可以加、减和比较,即使是用户定义的类型也是如此。整数,指针和标准模板库中的随机访问迭代器均具备这样的整数行为而满足此要求。

必须只存在一个循环控制变量,而且循环的初始化从句必须对它赋值。
循环控制变量不能在循环体内修改。
终止和递增值会在循环开始前计算,而不会在每次循环迭代时再次计算。
在C++中,控制变量必须在循环语句上定义, 而不是在循环外。(在C中支持)
一条break 或者 return 语句在cilk_for循环体中将不能工作。
一条goto语句只能在goto 的目标行也存在于当前循环体中时,才能在cilk_for循环中使用。

总之,在实际使用中,如果发现使用for能满足要求,使用cilk_for不行或者报错,就对照这些限制进行检查即可,大部分的for循环都可以直接使用cilk_for的。

你可能感兴趣的:(工作,user,ini,文档,编译器,fun)