案例研究: 面向神经细胞模拟优化代码

关于

英特尔举办的英特尔® 现代代码开发人员挑战赛吸引了来自 19 个国家和地区的 130 所高校的 2,000 名学生。参赛者使用英特尔® 至强™ 处理器和英特尔® 至强融核™ 协处理器,对用于 CERN openlab 人脑模拟研究项目的代码进行优化。该研究项目旨在寻找治疗精神分裂症、癫痫症、自闭症等神经疾病的有效方法。参赛者的任务是检查面向细胞群集和 3D 运动的代码,然后通过优化代码修改面向并行性能的算法,从而缩短运行时间,同时保持正确性。

在本文中,Daniel Vea Falguera(其中一位挑战赛获胜者)将与大家分享开发的原始代码和优化后的代码(注:修改后的代码行), 并介绍他所采用的优化方法。 在有些情况下,优化并不奏效,但他就如何修改代码以实现预期目标提供了一些见解。

包括

原始代码

#include <cstring>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <getopt.h>
#include "util.hpp"

优化后的代码

#include <cstring>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <omp.h>
#include <getopt.h>
#include "util.hpp"
#include <malloc.h>     //Useless
#include <mkl.h>        //Useless
#include <cilk/cilk.h> //Useless

修改的代码行:5

优化说明

需要添加 #include <omp.h> 以运行 OpenMP*。 OpenMP 函数和子句均用于优化后的代码。

在开发过程中,已完成的代码中纳入了 malloc.h、mkl.h 和 cilk.h 以优化内存块,但并未实现任何改进。 这种纳入只是为了说明,我们尝试过使用它们。更多关于英特尔® 数学核心函数库(英特尔® MKL)和英特尔® Cilk™ Plus 的信息请参阅“其他优化”部分。

RandomFloatPos

函数 RandomFloatPos() 可用于三次生成随机数字,以在 cellMovementAndDuplication 函数中生成一个随机 3D 位置。

注:本文仅参考了有关计算的函数,因此 “menu printings” 等函数未予以讨论。

原始代码

static float RandomFloatPos() { 
    // returns a random number between a given minimum and maximum 
    float random = ((float) rand()) / (float) RAND_MAX; 
    float a = 0; 
    float r = random; 
    return a + r; 
}

修改的代码行:  3, 4, 5, 6

优化后的代码

static void RandomFloatPos(float input[3],unsigned sedin) {
    // returns a random number between a given minimum and maximum
    __assume_aligned((float*)input, 64);
    unsigned int seed=sedin,i;
    for(i=0;i<3;++i){
        input[i]=(((float)rand_r(&seed))/(float)(RAND_MAX))-0.5;
    }
}

修改的代码行: 1

优化说明

原始函数返回随机数时进行了一些不必要的步骤(例如,为随机数添加零)。由于函数 rand()每次只能执行一条线程,原始代码无法直接并行化。并行化问题使用 rand_r() 函数得到了解决。rand_r() 函数支持同时执行线程。

函数 RandomFloatPos() 被调用来生成一个减去了常量偏移量 0.5 的随机 3D 位置,因此它可简化成包含三次迭代的 for 循环。 优化后的代码也可如此。 偏移量可用于产生从 -0.5 到 0.5 范围内的位置值,从而将点 (0,0,0) 放在细胞运动空间的中间位置。

优化后的函数包含两个参数:input[3] 和 sedin。 Input[3] 返回生成的 3D 随机位置值,而且已对齐内存 (64)。 sedin 作为参数传递,包含每次针对该函数调用时生成的细胞种子的值。

for 循环生成 3D 坐标并将其保存在输入中。

getNorm

原始代码

static float getNorm(float* currArray) {
    // computes L2 norm of input array
    int c;
    float arraySum=0;
    for (c=0; c<3; c++) {
        arraySum += currArray[c]*currArray[c];
    }
    float res = sqrt(arraySum);
    return res;
}

修改的代码行:3, 6, 8, 9

优化后的代码

static float getNorm(float* currArray) {
	// computes L2 norm of input array
	float arraySum=0;
	for (int c=0; c<3; ++c) {
		arraySum += pow(currArray[c],2);
	}
	return sqrt(arraySum);
}

修改的代码行: 5, 7

优化说明

它从输入浮点 currArray 的特定数字阵列开始计算范数。 针对该函数优化代码的方法有两种:

  • 移除 res 变量。
  • 添加 pow() 函数。
    后续开展的广泛研究说明,pow() 并非显著改进。 另外,鉴于英特尔® 至强融核™ 协处理器能够在每个循环执行多次浮点运算,原始 currArray[c]*currArray[c] 在那些案例中的速度更快。 英特尔编译器对 pow() 函数进行了矢量化处理,但因此添加了更多代码,因此降低了运行时速度。

getL2Distance

该函数用于确定 3D 空间中两点之间的线性距离。

原始代码

static float getL2Distance(float pos1x, float pos1y, float pos1z, float
pos2x, float pos2y, float pos2z) {
	// returns distance (L2 norm) between two positions in 3D
	float distArray[3];
	distArray[0] = pos2x-pos1x;
	distArray[1] = pos2y-pos1y;
	distArray[2] = pos2z-pos1z;
	float l2Norm = getNorm(distArray);
	return l2Norm;
}

修改的代码行: 1, 2, 5, 6, 7, 8, 9

优化后的代码

static float getL2Distance(float* pos1, float* pos2) {
	// returns distance (L2 norm) between two positions in 3D
	float distArray[3] __attribute__((aligned(64)));
	distArray[0] = pos2[0]-pos1[0];
	distArray[1] = pos2[1]-pos1[1];
	distArray[2] = pos2[2]-pos1[2];
	return getNorm(distArray);
}

修改的代码行: 1-8

优化说明

原始函数有 6 个代表 2 个 3D 点的输入,以计算这 2 点之间的距离。 优化后的函数只有 2 个输入,分别为 2 个代表 3D 点的 3 要素阵列。 其中使用的变量已对齐。

优化后的函数看起来是 SIMD 可执行文件,但当使用矢量符号(例如 P[0:2]=a[0:2]*b[0:2])时,执行速度降低了。 定义并使用初等函数也许能够进一步优化该函数。

如欲阅读有关初等函数的白皮书,请访问:http://software.intel.com/sites/default/files/article/181418/whitepaperonelementalfunctions.pdf

produceSubstances

该函数将每细胞位置的物质集中度增加至最大限值:每细胞位置 1 单位。

原始代码

static void produceSubstances(float**** Conc, float** posAll, int* typesAll, int L, int n){

	produceSubstances_sw.reset();
		// increases the concentration of substances at the location of the     cells
		float sideLength = 1/(float)L; // length of a side of a diffusion voxel
		int c, i1, i2, i3;
		for (c=0; c< n; c++) {
			i1 = std::min((int)floor(posAll[c][0]/sideLength),(L-1));
			i2 = std::min((int)floor(posAll[c][1]/sideLength),(L-1));
			i3 = std::min((int)floor(posAll[c][2]/sideLength),(L-1));
		if (typesAll[c]==1) {
			Conc[0][i1][i2][i3]+=0.1;
			if (Conc[0][i1][i2][i3]>1) {
				Conc[0][i1][i2][i3]=1;
			}
		} else {
			Conc[1][i1][i2][i3]+=0.1;
			if (Conc[1][i1][i2][i3]>1) {
				Conc[1][i1][i2][i3]=1;
			}
		}
	}
	produceSubstances_sw.mark();
}

修改的代码行: 1, 5, 6, 8, 9, 10, 11, 13, 14, 17, 18, 19

优化后的代码

static void produceSubstances(int L, float Conc[2][L][L][L], float posAll[][3], int* typesAll, int n) {

	produceSubstances_sw.reset();
	// increases the concentration of substances at the location of the  cells

	const int auxL=L;
	--L;
	int c,i[3] __attribute__((aligned(32))); //i array aligned
	omp_set_num_threads(240);

	#pragma omp parallel for schedule(static) private(i,c)
	for (c=0; c< n; ++c) {
		__assume_aligned((int*)i, 32);
		__assume_aligned((float*)posAll, 64);
		i[0] = std::min((int)floor(posAll[c][0]*auxL),L);
		i[1] = std::min((int)floor(posAll[c][1]*auxL),L);
		i[2] = std::min((int)floor(posAll[c][2]*auxL),L);

		if (typesAll[c]==1) {
			(Conc[0][i[0]][i[1]][i[2]]>0.9)?
			Conc[0][i[0]][i[1]][i[2]]=1 :
			Conc[0][i[0]][i[1]][i[2]]+=0.1;
		} else {
			(Conc[1][i[0]][i[1]][i[2]]>0.9)?
			Conc[1][i[0]][i[1]][i[2]]=1 :
			Conc[1][i[0]][i[1]][i[2]]+=0.1;
		}
	}
	produceSubstances_sw.mark();
}

修改的代码行: 1,6-11, 13-17, 20-22, 24-26

优化说明

化后的函数输入修改后的序列,以在函数的标头中定义阵列的大小。 这样我们可以不必使用指示器,而是直接处理阵列,以便编译器事先知道我们向该函数传递的要素的大小。

原始代码使用指示器初始化阵列,但由于这些阵列是静态的(长度固定,无法更改),因此不使用指示器,直接将其声明为已定义大小的阵列会更加简单、快速。

优化后的代码包括使用面向函数的 OpenMP 并行,以及静态调度,以将负载平均分配至其他内核。 这是因为,该函数可以并行执行,对结果不会造成影响,即 for 循环的每次循环都可以在不影响下次迭代的情况下独立执行。

在主代码(稍后介绍)执行过程中,该函数调用了多次,每次都增加了 n 值,因此,尽管可以实现函数并行化,但这仅适用于 n 值较小(小于 10000)的情况。 否则,额外的开销会降低代码的速度。

运算 posAll[c][0]/sideLength 与 (posAll[c][0]/1/L) 或 (posAll[c][0]*L) 相同。 posAll[c][0]/sideLength 的计算次数更少,但可生成相同的结果,因此比较有利。

L 变量的使用在优化时进行了更改。 原始代码 3 次使用运算 L-1,从而使用了 3 次额外运算。 在优化版本中,我们只需简单地递减之前的 L 变量。 auxL 常量(用于优化,因为只读变量比读/写变量快)保存 L 的原值,在该函数执行期间保持不变。

同样,我们改变了使用 Conc 变量的方法,以优化代码。 在原始代码中,if 子句使 Conc 变量递增了 0.1,然后检查该值是否大于 1,如果是,将该值限制在 1,从而使之前的加法成为无用功。 该方法用于检查 Conc 是否大于 0.9,如果是,将 Conc 的值设定在 1,如果不是,使 Conc 递增 0.1。

runDiffusionStep

该函数分为两部分。 第一部分将 Conc 变量拷贝至 tempConc。 第二部分迭代 Conc 阵列校验上限和 3D 边界。

原始代码

static void runDiffusionStep(float**** Conc, int L, float D) {
	runDiffusionStep_sw.reset();
	// computes the changes in substance concentrations due to diffusion

	int i1,i2,i3, subInd;
	float tempConc[2][L][L][L];
	for (i1 = 0; i1 < L; i1++) {
		for (i2 = 0; i2 < L; i2++) {
			for (i3 = 0; i3 < L; i3++) {
				tempConc[0][i1][i2][i3] = Conc[0][i1][i2][i3];
				tempConc[1][i1][i2][i3] = Conc[1][i1][i2][i3];
			}
		}
	}

	int xUp, xDown, yUp, yDown, zUp, zDown;

	for (i1 = 0; i1 < L; i1++) {
		for (i2 = 0; i2 < L; i2++) {
			for (i3 = 0; i3 < L; i3++) {
				xUp = (i1+1);
				xDown = (i1-1);
				yUp = (i2+1);
				yDown = (i2-1);
				zUp = (i3+1);
				zDown = (i3-1);
				for (subInd = 0; subInd < 2; subInd++) {
					if (xUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][xUp][i2][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (xDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][xDown][i2][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (yUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][yUp][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (yDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][yDown][i3]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (zUp<L) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][i2][zUp]-tempConc[subInd][i1][i2][i3])*D/6;
					}
					if (zDown>=0) {
						Conc[subInd][i1][i2][i3] += (tempConc[subInd][i1][i2][zDown]-tempConc[subInd][i1][i2][i3])*D/6;
					}
				}
			}
		}
	}
	runDiffusionStep_sw.mark();
}

修改的代码行: 1, 5, 9-12, 16, 21-45

优化后的代码

static void runDiffusionStep(int L, float Conc[2][L][L][L], float D) {
	runDiffusionStep_sw.reset();
	// computes the changes in substance concentrations due to diffusion
	int i1,i2,i3,auxx;
	const float auxD=D/6;
	const int auxL=L-1;
	float tempConc[2][L][L][L] __attribute__((aligned(64)));
	omp_set_num_threads(240);
	#pragma omp parallel
	{
		#pragma omp for schedule(static) private(i1,i2) collapse(2)
		for (i1 = 0; i1 < L; ++i1) {
			for (i2 = 0; i2 < L; ++i2) {
				memcpy(tempConc[0][i1][i2],
				Conc[0][i1][i2],sizeof(float)*L);
				memcpy(tempConc[1][i1][i2],
				Conc[1][i1][i2],sizeof(float)*L);
			}
		}

		#pragma omp for schedule(static) private(i1,i2,i3) collapse(2)
		for (i1 = 0; i1 < L; ++i1) {
			for (i2 = 0; i2 < L; ++i2) {
				Conc[0][i1][i2][0] += (tempConc[0][i1][i2][1]-
				tempConc[0][i1][i2][0])*auxD;
				Conc[1][i1][i2][0] += (tempConc[1][i1][i2][1]-
				tempConc[1][i1][i2][0])*auxD;
				for (i3 = 1; i3 < auxL; ++i3) {
					const float aux=tempConc[0][i1][i2][i3];
					const float aux1=tempConc[1][i1][i2][i3];
					__assume_aligned((float*)tempConc[0], 64);
					__assume_aligned((float*)tempConc[1], 64);
					__assume_aligned((float*)Conc[0], 64);
					__assume_aligned((float*)Conc[1], 64);
					if (i1<auxL) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][(i1+1)][i2][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][(i1+1)][i2][i3]-aux1)*auxD;
					}
					if (i1>0) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][(i1-1)][i2][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][(i1-1)][i2][i3]-aux1)*auxD;
					}
					if (i2<auxL) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][i1][(i2+1)][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] +=
						(tempConc[1][i1][(i2+1)][i3]-aux1)*auxD;
					}
					if (i2>0) {
						Conc[0][i1][i2][i3] +=
						(tempConc[0][i1][(i2-1)][i3]-aux)*auxD;
						Conc[1][i1][i2][i3] += (tempConc[1][i1][(i2-1)][i3]-aux1)*auxD;
					}
					Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3+1)]-aux)*auxD;
					Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3+1)]-aux1)*auxD;
					Conc[0][i1][i2][i3] +=  (tempConc[0][i1][i2][(i3-1)]-aux)*auxD;
					Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3-1)]-aux1)*auxD;
				}	
			Conc[0][i1][i2][auxL-1] += (tempConc[0][i1][i2][auxL]-tempConc[0][i1][i2][auxL-1])*auxD;
			Conc[1][i1][i2][auxL-1] += (tempConc[1][i1][i2][auxL]-tempConc[0][i1][i2][auxL-1])*auxD;
		}
	}
}

修改的代码行: 1, 4-11, 14-17, 21, 24-64

优化说明

该函数包含两个主要部分:第一部分将 Conc 变量(使用 for 循环)拷贝至 tempConc,第二部分迭代 Conc 阵列,同时不断校验上维边界和下维边界。 这两部分可以轻松实现并行化:所有线程拷贝 Conc 的除法,然后所有线程执行下一个大循环的除法。 该函数中开销最大的部分在第二个大循环中,每次迭代期间,都需要增减坐标并校验边界。 如果计算的坐标在边界中,将执行计算。

优化后的函数可创建所有线程。 它们的 memcpy 循环部分在每条线程中执行,全部完成后,开始执行维度循环。 使用 memcpy 会增加迭代进行的拷贝次数。

变量的优化方法与之前函数相同:尽量使用常量,并对齐阵列。

两个循环在 OpenMP 编译指示标头下方‘重叠’,这样:

  • 第一个循环在
    #pragma omp for schedule(static) private(i1,i2) collapse(2) 下方
  • 第二个循环在
    #pragma omp for schedule(static) private(i1,i2,i3) collapse(2) 下方

第一个循环 — tempConc copy 循环通过多线程化实现优化。 该循环借助‘parallel for’完成多线程化,并通过使用 memcpy(如上所述)移动每次迭代的大部分数据,以实现优化。 我不知道将 Conc 拷贝至 tempConc 的过程中是否占用了所有可用内存带宽,因此使用所有可用内存带宽(约为 L*float(64bit)*240)也是实现改进的一种方法。

第二个循环(处理维度)的优化方式不同。

初始优化必须涉及 i3 边界校验。 这种校验位于第 4 行代码:

Conc[0][i1][i2][0]
Conc[1][i1][i2][0]

Conc[0][i1][i2][auxL-1]
Conc[1][i1][i2][auxL-1]

如果循环是 L-2 迭代(i3 从 1 前往 auxL),可以避免 i3 边界校验,因此我们手动添加第一次和最后一次运算。 受到这四行代码的限制,i3 边界永远不会被超越:

Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3+1)]-aux)*auxD;
Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3+1)]-aux1)*auxD;
Conc[0][i1][i2][i3] += (tempConc[0][i1][i2][(i3-1)]-aux)*auxD;
Conc[1][i1][i2][i3] += (tempConc[1][i1][i2][(i3-1)]-aux1)*auxD;

如果未达到边界,将执行 if 代码中的代码。

也就是说,如果删除其中的 if 代码,该循环将得以改进。我们可以手动展开该循环,仔细观察边界,然后将任务分配至线程和 OpenMP 任务函数。

runDecayStep

该函数迭代所有 Conc 变量,以完成每次迭代上的乘法。

原始代码

static void runDecayStep(float**** Conc, int L, float mu) { 
	runDecayStep_sw.reset(); 
	// computes the changes in substance concentrations due to decay 
	int i1,i2,i3; 
	for (i1 = 0; i1 < L; i1++) { 
		for (i2 = 0; i2 < L; i2++) { 
			for (i3 = 0; i3 < L; i3++) { 
				Conc[0][i1][i2][i3] = Conc[0][i1][i2][i3]*(1-mu); 
				Conc[1][i1][i2][i3] = Conc[1][i1][i2][i3]*(1-mu); 
			} 
		} 
	} 
	runDecayStep_sw.mark();
}

修改的代码行: 1, 8, 9

优化后的代码

static void runDecayStep(int L, float Conc[2][L][L][L], float mu) { 
	runDecayStep_sw.reset(); 
	// computes the changes in substance concentrations due to decay 
	const float muu=1-mu; 
	int i1,i2,i3; 
	omp_set_num_threads(240); 
	#pragma omp parallel for schedule(static) private(i1,i2,i3) collapse(3) 
	#pragma simd 
	for (i1 = 0; i1 < L; ++i1) { 
		for (i2 = 0; i2 < L; ++i2) { 
			for (i3 = 0; i3 < L; ++i3) { 
				__assume_aligned((float*)Conc[0][i1][i2], 64); 
				__assume_aligned((float*)Conc[1][i1][i2], 64); 
				Conc[0][i1][i2][i3] = Conc[0][i1][i2][i3]*muu; 
				Conc[1][i1][i2][i3] = Conc[1][i1][i2][i3]*muu; 
			} 
		} 
	} 
	runDecayStep_sw.mark();
}

修改的代码行: 1, 4, 6-8, 12-15

优化说明

该函数只需一些简单的调整就可实现优化。

  • 函数的输入阵列在标头上定义。
  • 变量的优化方式与之前的函数一样(尽量使用常量,并对齐阵列)。
  • 循环在 OpenMP 编译指示标头下方‘重叠’。
  • 使用 #pragma simd 循环。 借助 SIMD 指令对该循环进行矢量化处理,可缩短优化后代码的执行时间。 使用 SIMD 指令支持同时执行乘法运算。

其他优化方法包括最大限度发挥英特尔 MKL 函数的作用。 英特尔 MKL 函数面向大数据阵列而优化,因此如果 Conc 阵列扁平化至一个维度,英特尔 MKL 的执行速度会更快。

cellMovementAndDuplication

该函数用于生成每个细胞及每个细胞副本的随机运动。 但并不是所有细胞副本。

原始代码

static int cellMovementAndDuplication(float** posAll, float* pathTraveled, 
 int* typesAll, int* numberDivisions, float pathThreshold, int 
 divThreshold, int n) { 
	cellMovementAndDuplication_sw.reset(); 
	int c; 
	currentNumberCells = n; 
	float currentNorm; 
	float currentCellMovement[3]; 
	float duplicatedCellOffset[3]; 
	for (c=0; c<n; c++) { 
		// random cell movement 
		currentCellMovement[0]=RandomFloatPos()-0.5; 
		currentCellMovement[1]=RandomFloatPos()-0.5;  
		currentCellMovement[2]=RandomFloatPos()-0.5; 
		currentNorm = getNorm(currentCellMovement); 
		posAll[c][0]+=0.1*currentCellMovement[0]/currentNorm; 
		posAll[c][1]+=0.1*currentCellMovement[1]/currentNorm; 
		posAll[c][2]+=0.1*currentCellMovement[2]/currentNorm; 
		pathTraveled[c]+=0.1; 
		// cell duplication if conditions fulfilled 
		if (numberDivisions[c]<divThreshold) { 
			if (pathTraveled[c]>pathThreshold) { 
				pathTraveled[c]-=pathThreshold; 
				numberDivisions[c]+=1; // update number of divisions this cell has undergone 
				currentNumberCells++;  // update number of cells in 
				 the simulation 
				numberDivisions[currentNumberCells-1]=numberDivisions[c]; // update number of divisions the  duplicated cell has undergone 
				typesAll[currentNumberCells-1]=-typesAll[c]; 
				// assign type of duplicated cell (opposite to current cell) 

				// assign location of duplicated cell 
				duplicatedCellOffset[0]=RandomFloatPos()-0.5; 
				duplicatedCellOffset[1]=RandomFloatPos()-0.5; 
				duplicatedCellOffset[2]=RandomFloatPos()-0.5; 
				currentNorm = getNorm(duplicatedCellOffset); 
				posAll[currentNumberCells-1][0]=posAll[c][0]+0.05*duplicatedCellOffset[0]/currentNorm; 
				posAll[currentNumberCells-1][1]=posAll[c][1]+0.05*duplicatedCellOffset[1]/currentNorm; 
				posAll[currentNumberCells-1][2]=posAll[c][2]+0.05*duplicatedCellOffset[2]/currentNorm; 
			} 
		} 
	} 
	cellMovementAndDuplication_sw.mark(); 
	return currentNumberCells;
}

修改的代码行: 1, 12-14, 16-18, 21, 22, 24-34, 36-38

优化后的代码

static int cellMovementAndDuplication(float posAll[][3], float* pathTraveled, 
 int* typesAll, int* numberDivisions, float pathThreshold, int 
 divThreshold, int n) { 
	cellMovementAndDuplication_sw.reset(); 
	int c,currentNumberCells = n; 
	float currentNorm; 
	unsigned int seed=rand(); 
	float currentCellMovement[3] __attribute__((aligned(64))); 
	float duplicatedCellOffset[3] __attribute__((aligned(64))); 
	omp_set_num_threads(240); 

	#pragma omp parallel for simd schedule (static) shared(posAll) private(c,currentNorm,currentCellMovement) 

	for (c=0; c<n; ++c) { 
		// random cell movement 
		RandomFloatPos(currentCellMovement,seed+c); 
		currentNorm = getNorm(currentCellMovement)*10; 
		__assume_aligned((float*)posAll, 64); 
		__assume_aligned((float*)currentCellMovement, 64); 
		posAll[c][0:3]+=currentCellMovement[0:3]/currentNorm; 
		__assume_aligned((float*)pathTraveled, 64); 
		pathTraveled[c]+=0.1; 
	} 
	seed=rand(); 
	for (c=0; c<n; ++c) { 
		if ((numberDivisions[c]<divThreshold) && (pathTraveled[c]>pathThreshold)) { 
			pathTraveled[c]-=pathThreshold; 
			++numberDivisions[c]; // update number of divisions this cell has undergone 
			numberDivisions[currentNumberCells]=numberDivisions[c]; // update number of divisions the duplicated cell has undergone 
			typesAll[currentNumberCells]=-typesAll[c]; // assign type of 
			 duplicated cell (opposite to current cell) 

			// assign location of duplicated cell 
			RandomFloatPos(duplicatedCellOffset,seed+c); //The seed+c value will be different for each thread and iteration, this way the random value is random always.
			currentNorm = getNorm(duplicatedCellOffset)*20; 
			__assume_aligned((float*)posAll, 64); 
			__assume_aligned((float*)duplicatedCellOffset, 64); 
			posAll[currentNumberCells][0:3]=posAll[c][0:3]+duplicatedCellOffset[0:3]/currentNorm; 
			++currentNumberCells; // update number of cells in the simulation 
		} 
	} 
	cellMovementAndDuplication_sw.mark(); 
	return currentNumberCells;
}

修改的代码行: 1, 8-12, 16-21, 26, 28, 29, 34, 36-39

优化说明

面向 cellMovementAndDuplication 的优化代码分为两部分,其优化方法与之前的函数相同:

  • RandomFloatPos() 函数。 这一优化后的函数可以实现多线程化,并返回整个位置阵列(3D 位置阵列)。
  • 包含 currentNorm 的代码行可通过 SIMD 实现矢量化,也可以以数学的方式进行简化。

另外,优化后的代码更新了标头,并对齐了阵列。

在调用 seed=rand() 之前,新函数 RandomFloatPos() 要求一个随机生成的种子,而且每条线程需要该种子不同的值。 为此,每条线程增加了随机生成的种子值,以对应循环迭代。 这样,每条线程的数字既是随机的,又各不相同。

为了实现该函数的多线程化,我将其分成两个大循环。 第一个(随机细胞运动)可轻松实现并行化。 第二个循环(从 seed=rand(); 开始)必须在单条线程中执行,因为每次迭代都依赖之前迭代的值。

runDiffusionClusterStep

该函数用于根据物质的渐变决定细胞运动。

原始代码

static void runDiffusionClusterStep(float**** Conc, float** movVec, float** posAll, int* typesAll, int n, int L, float speed) { 
	runDiffusionClusterStep_sw.reset(); 
	// computes movements of all cells based on gradients of the two substances 
	float sideLength = 1/(float)L; // length of a side of a diffusion voxel 

	float gradSub1[3]; 
	float gradSub2[3]; 
	float normGrad1, normGrad2; 
	int c, i1, i2, i3, xUp, xDown, yUp, yDown, zUp, zDown; 

	for (c = 0; c < n; c++) { 
		i1 = std::min((int)floor(posAll[c][0]/sideLength),(L-1)); 
		i2 = std::min((int)floor(posAll[c][1]/sideLength),(L-1)); 
		i3 = std::min((int)floor(posAll[c][2]/sideLength),(L-1)); 

		xUp = std::min((i1+1),L-1); 
		xDown = std::max((i1-1),0); 
		yUp = std::min((i2+1),L-1); 
		yDown = std::max((i2-1),0); 
		zUp = std::min((i3+1),L-1); 
		zDown = std::max((i3-1),0); 

		gradSub1[0] = (Conc[0][xUp][i2][i3]-Conc[0][xDown][i2][i3])/(sideLength*(xUp-xDown)); 
		gradSub1[1] = (Conc[0][i1][yUp][i3]-Conc[0][i1][yDown][i3])/(sideLength*(yUp-yDown)); 
		gradSub1[2] = (Conc[0][i1][i2][zUp]-Conc[0][i1][i2][zDown])/(sideLength*(zUp-zDown)); 
		gradSub2[0] = (Conc[1][xUp][i2][i3]-Conc[1][xDown][i2][i3])/(sideLength*(xUp-xDown)); 
		gradSub2[1] = (Conc[1][i1][yUp][i3]-Conc[1][i1][yDown][i3])/(sideLength*(yUp-yDown)); 
		gradSub2[2] = (Conc[1][i1][i2][zUp]-Conc[1][i1][i2][zDown])/(sideLength*(zUp-zDown)); 
		normGrad1 = getNorm(gradSub1); 
		normGrad2 = getNorm(gradSub2); 
		if ((normGrad1>0)&&(normGrad2>0)) { 
			movVec[c][0]=typesAll[c]*(gradSub1[0]/normGrad1-gradSub2[0]/normGrad2)*speed; 
			movVec[c][1]=typesAll[c]*(gradSub1[1]/normGrad1-gradSub2[1]/normGrad2)*speed; 
			movVec[c][2]=typesAll[c]*(gradSub1[2]/normGrad1-gradSub2[2]/normGrad2)*speed; 
		} else { 
			movVec[c][0]=0; 
			movVec[c][1]=0; 
			movVec[c][2]=0; 
		} 
	} 
	runDiffusionClusterStep_sw.mark();
}

修改的代码行: 1-2, 5-10, 13-39 

优化后的代码

static void runDiffusionClusterStep(int L, float Conc[2][L][L][L], float movVec[][3], float posAll[][3], int* typesAll, int n, float speed) { 
	runDiffusionClusterStep_sw.reset(); 
	// computes movements of all cells based on gradients of the two substances 
		const float auxL=L; 
			--L; 
	float gradSub[6] __attribute__((aligned(64))); 
	float aux[3] __attribute__((aligned(64))); 
		int i[3] __attribute__((aligned(32))); 
	float normGrad[2] __attribute__((aligned(64))); 
	int c, xUp, xDown, yUp, yDown, zUp, zDown; 
	   omp_set_num_threads(240); 

	#pragma omp parallel for schedule(static) private(i,xUp,xDown,yUp,yDown,zUp,zDown,gradSub,normGrad,aux) if (n>240) 
	for (c = 0; c < n; ++c) { 
		__assume_aligned((int*)i, 32); 
		__assume_aligned((float*)posAll, 64); 
		__assume_aligned((float*)gradSub, 64); 
		__assume_aligned((float*)normGrad, 64); 
		__assume_aligned((float*)movVec, 64); 
		__assume_aligned((float*)typesAll, 64); 
		i[0:3] = std::min((int)floor(posAll[c][0:3]*auxL),L)-1; 
		xDown = std::max(i[0],0); 
		yDown = std::max(i[1],0); 
		zDown = std::max(i[2],0); 
		xUp = std::min((i[0]+2),L); 
		yUp = std::min((i[1]+2),L); 
		zUp = std::min((i[2]+2),L); 
		aux[0]=auxL/((xUp-xDown)); 
		aux[1]=auxL/((yUp-yDown)); 
		aux[2]=auxL/((zUp-zDown)); 
		gradSub[0] = (Conc[0][xUp][i[1]][i[2]]-Conc[0][xDown][i[1]][i[2]])*aux[0]; 
		gradSub[1] = (Conc[0][i[0]][yUp][i[2]]-Conc[0][i[0]][yDown][i[2]])*aux[1]; 
		gradSub[2] = (Conc[0][i[0]][i[1]][zUp]-Conc[0][i[0]][i[1]][zDown])*aux[2]; 
		normGrad[0] = getNorm(gradSub); 
		if (normGrad[0]>0){ 
			gradSub[3] = (Conc[1][i[0]][yUp][i[2]]-Conc[1][i[0]][yDown][i[2]])*aux[1]; 
			gradSub[4] = (Conc[1][xUp][i[1]][i[2]]-Conc[1][xDown][i[1]][i[2]])*aux[0]; 
			gradSub[5] = (Conc[1][i[0]][i[1]][zUp]-Conc[1][i[0]][i[1]][zDown])*aux[2]; 
			normGrad[1] = getNorm(gradSub+3); 
			if ( normGrad[1]>0) { 
				movVec[c][0:3]=typesAll[c]*(gradSub[0:3]/normGrad[0]-gradSub[3:3]/normGrad[1])*speed; 
			} else movVec[c][0:3]=0; 
		} 
	} 
	runDiffusionClusterStep_sw.mark();
}

修改的代码行: 1, 5-10, 12-14, 12-43

优化说明

该函数通过一些小小的简化、矢量化和多线程化行为实现了优化,与其他函数大体相同。

  • 输入阵列的维度在标头中规定。
  • 阵列对齐。
  • 尽量将变量替换成常量。
  • OpenMP 并行化。
  • 简化运算。
  • 简化矢量化。

if 条件可以过滤掉一部分运算。 例如,如果 normGrad[0] 等于或小于 0,则不必计算 normGrad[1]。 使用 if 条件可以减少对比和无用运算的次数。

使用更多 SIMD 优化,可进一步优化该函数。 不过,如需使用 SIMD 运算,max 和 min 函数必须以不同的方式实施。

getEnergy

该函数通过假设整卷的分布均匀以及确定一定数量的目标细胞数,计算细胞子集的能量测度。

原始代码

Original Code
static float getEnergy(float** posAll, int* typesAll, int n, float spatialRange, int targetN) { 
	getEnergy_sw.reset(); 
	// Computes an energy measure of clusteredness within a subvolume. The 
	// size of the subvolume is computed by assuming roughly uniform 
	// distribution within the whole volume, and selecting a volume 
	// comprising approximately targetN cells. 
	int i1, i2; 
	float currDist; 
	float** posSubvol=0; // array of all 3 dimensional cell positions 
	posSubvol = new float*[n]; 
	int typesSubvol[n]; 
	float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2; 
	if(quiet < 1) 
		printf("subVolMax: %f\n", subVolMax); 
	int nrCellsSubVol = 0; 
	float intraClusterEnergy = 0.0; 
	float extraClusterEnergy = 0.0; 
	float nrSmallDist=0.0; 

	for (i1 = 0; i1 < n; i1++) { 
		posSubvol[i1] = new float[3]; 
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]-0.5)<subVolMax)) { 
			posSubvol[nrCellsSubVol][0] = posAll[i1][0]; 
			posSubvol[nrCellsSubVol][1] = posAll[i1][1]; 
			posSubvol[nrCellsSubVol][2] = posAll[i1][2]; 
			typesSubvol[nrCellsSubVol] = typesAll[i1]; 
			nrCellsSubVol++; 
		} 
	} 

	for (i1 = 0; i1 < nrCellsSubVol; i1++) { 
		for (i2 = i1+1; i2 < nrCellsSubVol; i2++) { 
			currDist = getL2Distance(posSubvol[i1][0],posSubvol[i1][1],posSubvol[i1][2],posSubvol[i2][0], 
			posSubvol[i2][1],posSubvol[i2][2]); 
			if (currDist<spatialRange) { 
				nrSmallDist = nrSmallDist+1;//currDist/spatialRange; 
				if (typesSubvol[i1]*typesSubvol[i2]>0) { 
					intraClusterEnergy = intraClusterEnergy+fmin(100.0,spatialRange/currDist); 
				} else { 
					extraClusterEnergy = extraClusterEnergy+fmin(100.0,spatialRange/currDist); 
				} 
			} 
		} 
	} 
	float totalEnergy = (extraClusterEnergy-intraClusterEnergy)/(1.0+100.0*nrSmallDist); 
	getEnergy_sw.mark(); 
	return totalEnergy;
}

修改的代码行: 1, 22, 34, 35, 37-41, 46, 48

优化后的代码

static float getEnergy(float posAll[][3], int* typesAll, int n, float spatialRange, int targetN) { 
	getEnergy_sw.reset(); 
	// Computes an energy measure of clusteredness within a subvolume. The 
	// size of the subvolume is computed by assuming roughly uniform 
	// distribution within the whole volume, and selecting a volume 
	// comprising approximately targetN cells. 
	int i1, i2; 
	float currDist; 
	float posSubvol[n][3] __attribute__((aligned(64))); 
	int typesSubvol[n] __attribute__((aligned(64))); 
	const float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2; 
	if(quiet < 1)printf("subVolMax: %f\n", subVolMax); 

	int nrCellsSubVol = 0; 
	float intraClusterEnergy = 0.0; 
	float extraClusterEnergy = 0.0; 
	float nrSmallDist=0.0; 
	for (i1 = 0; i1 < n; ++i1) { 
		__assume_aligned((float*)posAll, 64); 
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]- 
			0.5)<subVolMax)) { 
			__assume_aligned((float*)posSubvol[nrCellsSubVol], 64); 
			__assume_aligned((float*)typesAll, 64); 
			__assume_aligned((int*)typesSubvol, 64); 
			posSubvol[nrCellsSubVol][0] = posAll[i1][0]; 
			posSubvol[nrCellsSubVol][1] = posAll[i1][1]; 
			posSubvol[nrCellsSubVol][2] = posAll[i1][2]; 
			typesSubvol[nrCellsSubVol] = typesAll[i1]; 
			++nrCellsSubVol; 
		} 
	} 
	omp_set_num_threads(240); 
	#pragma omp parallel for schedule(static) reduction(+:nrSmallDist,intraClusterEnergy,extraClusterEnergy) private(i1,i2,currDist) 
	for (i1 = 0; i1 < nrCellsSubVol; ++i1) { 
		for (i2 = i1+1; i2 < nrCellsSubVol; ++i2) { 
			currDist = getL2Distance(posSubvol[i1],posSubvol[i2]); 
			if (currDist<spatialRange) { 
				++nrSmallDist; //currDist/spatialRange; 
				(typesSubvol[i1]*typesSubvol[i2]>0)? intraClusterEnergy += fmin(100.0,spatialRange/currDist) : 
				extraClusterEnergy += fmin(100.0,spatialRange/currDist); 
			} 
		} 
	} 
	getEnergy_sw.mark(); 
	return (extraClusterEnergy-intraClusterEnergy)/(1.0+100.0*nrSmallDist);
}

修改的代码行: 1, 9-11, 19, 22-24, 32, 33, 36-40

优化说明

该代码中的一部分比较容易优化,包括‘new’构造函数、部分是常量的变量,以及第二个循环依赖第一个循环的事实。 第一个循环由于依赖 nrCellsSubVol 的值,因此无法实现并行化,再加上该循环中包括三个条件的 if,其并行化难度更大。 不过,第二个循环可以轻松实现并行化。

该函数的具体优化方式与其他函数相同。

  • 输入阵列的维度在标头中规定。
  • 尽量使用常量。
  • 对齐阵列。
  • 使用减法在第二个 ‘for loop’ 上实现 OpenMP 并行化。

下一个优化步骤是对第一个循环的 if 条件进行并行化处理。很难,但可以实现,并且还可以提升性能。

getCriterion

该函数用于确定子集中的细胞是否在集群中。

注:原始代码和优化后的代码都是缩减版,仅展示与本文相关的部分。

原始代码

static bool getCriterion(float** posAll, int* typesAll, int n, float spatialRange, int targetN) {
	getCriterion_sw.reset();
	// Returns 0 if the cell locations within a subvolume of the total
	// system, comprising approximately targetN cells, are arranged as clusters, and 1 otherwise.
	int i1, i2;
	int nrClose=0; // number of cells that are close (i.e. within a distance of spatialRange)
	float currDist;
	int sameTypeClose=0; // number of cells of the same type, and that are close (i.e. within a distance of spatialRange)
	int diffTypeClose=0; //number of cells of opposite types, and that are close (i.e. within a distance of spatialRange)
	float** posSubvol=0; // array of all 3 dimensional cell positions in the subcube
	posSubvol = new float*[n];
	int typesSubvol[n];
	float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2;
	int nrCellsSubVol = 0;

	// the locations of all cells within the subvolume are copied to array PosSubvol
	for (i1 = 0; i1 < n; i1++) {
		posSubvol[i1] = new float[3];
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2]
			-0.5)<subVolMax)) {
			posSubvol[nrCellsSubVol][0] = posAll[i1][0];
			posSubvol[nrCellsSubVol][1] = posAll[i1][1];
			posSubvol[nrCellsSubVol][2] = posAll[i1][2];
			typesSubvol[nrCellsSubVol] = typesAll[i1];
			nrCellsSubVol++;
		}
	}

[section of truncated code]

	for (i1 = 0; i1 < nrCellsSubVol; i1++) {
		for (i2 = i1+1; i2 < nrCellsSubVol; i2++) {
			currDist = getL2Distance(posSubvol[i1][0],posSubvol[i1][1],posSubvol[i1][2],posSubvol[i2][0], posSubvol[i2][1],posSubvol[i2][2]);
		if (currDist<spatialRange) {
			nrClose++;
			if (typesSubvol[i1]*typesSubvol[i2]<0) {
				diffTypeClose++;
			} else {
				sameTypeClose++;
			}
		}
	}

[section of truncated code]

}

修改的代码行: 9, 10, 32-39

优化后的代码

static bool getCriterion(float posAll[][3], int* typesAll, int n, float spatialRange, int targetN) { 
	getCriterion_sw.reset(); 
	// Returns 0 if the cell locations within a subvolume of the total 
	// system, comprising approximately targetN cells, are arranged as clusters, and 1 otherwise. 
	int i1, i2; 
	int nrClose=0; // number of cells that are close (i.e. within a  distance of spatialRange) 
	int sameTypeClose=0; // number of cells of the same type, and that are  close (i.e. within a distance of spatialRange) 
	int diffTypeClose=0; // number of cells of opposite types, and that are  close (i.e. within a distance of spatialRange) 
	float posSubvol[n][3] __attribute__((aligned(64))); 
	int typesSubvol[n] __attribute__((aligned(64))); 
	const float subVolMax = pow(float(targetN)/float(n),1.0/3.0)/2; 
	int nrCellsSubVol = 0; 

	// the locations of all cells within the subvolume are copied to array  posSubvol 

	for (i1 = 0; i1 < n; ++i1) { 
		__assume_aligned((float*)posAll, 64); 
		if ((fabs(posAll[i1][0]-0.5)<subVolMax) && (fabs(posAll[i1][1]-0.5)<subVolMax) && (fabs(posAll[i1][2] 
			-0.5)<subVolMax)) { 
			__assume_aligned((float*)posSubvol[nrCellsSubVol], 64); 
			__assume_aligned((float*)typesAll, 64); 
			__assume_aligned((int*)typesSubvol, 64); 
			posSubvol[nrCellsSubVol][0] = posAll[i1][0]; 
			posSubvol[nrCellsSubVol][1] = posAll[i1][1]; 
			posSubvol[nrCellsSubVol][2] = posAll[i1][2]; 
			typesSubvol[nrCellsSubVol] = typesAll[i1]; 
			++nrCellsSubVol; 
		} 
	} 

[section of truncated code]

	omp_set_num_threads(240); 
	#pragma omp parallel for schedule(static)    
	reduction(+:nrClose,diffTypeClose,sameTypeClose) private(i1,i2) 
	for (i1 = 0; i1 < nrCellsSubVol; ++i1) { 
		for (i2 = i1+1; i2 < nrCellsSubVol; ++i2) { 
				if (getL2Distance(posSubvol[i1],posSubvol[i2])<spatialRange) { 
					++nrClose; 
					(typesSubvol[i1]*typesSubvol[i2]<0) ? ++diffTypeClose: ++sameTypeClose; 
				} 
		} 
	} 

[section of truncated code]

}

修改的代码行: 1, 9-11, 17, 20-22, 33-35, 38-40

优化说明

该函数的可优化部分包括变量声明和初始化。 第一个循环的可优化性较小,但优化第二个循环意义重大。

该函数的具体优化方式与其他函数相同。

  • 输入阵列的维度在标头中规定。
  • 尽量使用常量。
  • 使用减法在第二个 ‘for loop’ 上实现 OpenMP 并行化。

如欲进一步优化,可以对第一个循环的 if 条件进行并行化处理。另外,扁平化 posAll 和 posSubVol 阵列可以加快执行速度。

主 – 变量声明

主代码较大,因此本文将其分成两部分。这一部分主要处理代码以及面向变量声明优化后的代码。

原始代码

int i,c,d; 
int i1, i2, i3, i4; 
float energy; // value that quantifies the quality of the cell clustering   output. The smaller this value, the better the clustering 
float** posAll=0; // array of all 3 dimensional cell positions 
posAll = new float*[finalNumberCells]; 
float** currMov=0; // array of all 3 dimensional cell movements at the last  time point 
currMov = new float*[finalNumberCells]; // array of all cell movements in the  last time step 
float zeroFloat = 0.0; 
float pathTraveled[finalNumberCells]; // array keeping track of length of path traveled until cell divides 
int numberDivisions[finalNumberCells]; //array keeping track of number of  division a cell has undergone 
int typesAll[finalNumberCells]; // array specifying cell type (+1 or -1)

修改的代码行: 2, 4-7, 

优化后的代码

int i,c,d,i1; 
float energy; // value that quantifies the quality of the cell clustering output. The smaller this value, the better the clustering 
float posAll[finalNumberCells][3] __attribute__((aligned(64))); 
float currMov[finalNumberCells][3] __attribute__((aligned(64))); //array of 
  // all cell movements in the last time step 
float pathTraveled[finalNumberCells] __attribute__((aligned(64))); // array 
  // keeping track of length of path traveled until cell divides 
int numberDivisions[finalNumberCells] __attribute__((aligned(64))); //array 
  // keeping track of number of division a cell has undergone 
int typesAll[finalNumberCells] __attribute__((aligned(64))); // array 
  // specifying cell type (+1 or -1) 
float Conc[2][L][L][L] __attribute__((aligned(64)));

修改的代码行: 3-12

优化说明

这部分代码的优化方式有多种。

  • 避免使用‘new’构造函数。
  • 删除无用变量(比如 zeroFloat)。
  • 如果可以声明大小已知的静态阵列,避免使用指示器。
  • 内存对齐;尽管可以使用更多内存,但内存访问速度更快。
  • 在初始化阶段定义变量;这比之后定义该值的速度更快。

主 – 变量初始化

主代码较大,因此本文将其分成两部分。 这一部分主要处理代码以及面向变量初始化优化后的代码。变量的大小和值在该代码中声明,包括最大的阵列 currMov、posAll、pathTraveled 和 Conc。

原始代码

// Initialization of the various arrays 
for (i1 = 0; i1 < finalNumberCells; i1++) { 
	currMov[i1] = new float[3]; 
	posAll[i1] = new float[3]; 
	pathTraveled[i1] = zeroFloat; 
	pathTraveled[i1] = 0; 
	for (i2 = 0; i2 < 3; i2++) { 
		currMov[i1][i2] = zeroFloat; 
		posAll[i1][i2] = 0.5; 
	} 
} 
// create 3D concentration matrix 
float**** Conc; 
Conc = new float***[L]; 
for (i1 = 0; i1 < 2; i1++) { 
	Conc[i1] = new float**[L]; 
	for (i2 = 0; i2 < L; i2++) { 
		Conc[i1][i2] = new float*[L]; 
		for (i3 = 0; i3 < L; i3++) { 
			Conc[i1][i2][i3] = new float[L]; 
			for (i4 = 0; i4 < L; i4++) { 
				Conc[i1][i2][i3][i4] = zeroFloat; 
			} 
		} 
	} 
}

修改的代码行: 3-5, 7-10, 13, 14, 16, 18, 20, 22

优化后的代码

// Initialization of the various arrays 
omp_set_num_threads(240); 
#pragma omp parallel 
{ 
	#pragma omp for simd schedule (static) private (i1) nowait 
	for (i1 = 0; i1 < finalNumberCells; ++i1) { 
		__assume_aligned((float*)posAll, 64); 
		__assume_aligned((float*)currMov[i1], 64); 
		__assume_aligned((float*)pathTraveled, 64); 
		currMov[i1][0:3]=0; 
		posAll[i1][0]=0.5; 
		posAll[i1][1]=0.5; 
		posAll[i1][2]=0.5; 
		pathTraveled[i1] = 0; 
	} 
	#pragma omp for schedule (static) private (i1,c,d) collapse(3) 
	for (i1 = 0; i1 < 2; ++i1) { 
		for (c = 0; c < L; ++c) { 
			for (d = 0; d < L; ++d) { 
				Conc[i1][c][d][0:L]=0; 
			} 
		} 
	} 
}

修改的代码行: 2-5, 7-13, 16, 20

优化说明

这部分代码的优化方法有三种。 最基本的方法是将 for 循环的数量从 4 减至 3。 另外两种方法是内存对齐和借助 SIMD 实现 OpenMP 线程并行化。

主 – 阶段 1

主代码较大,因此本文将其分成两部分。 这一部分处理前几个模拟步骤。

原始代码

// Phase 1: Cells move randomly and divide until final number of cells is reached
while (n<finalNumberCells) {
	produceSubstances(Conc, posAll, typesAll, L, n); // Cells produce 
	// substances. Depending on the cell type, one of the two substances is produced.
	runDiffusionStep(Conc, L, D); // Simulation of substance diffusion
	runDecayStep(Conc, L, mu);
	n = cellMovementAndDuplication(posAll, pathTraveled, typesAll, numberDivisions, pathThreshold, divThreshold, n);

	for (c=0; c<n; c++) {
		// boundary conditions
		for (d=0; d<3; d++) {
			if (posAll[c][d]<0) {posAll[c][d]=0;}
			if (posAll[c][d]>1) {posAll[c][d]=1;}
		}
	}
}

修改的代码行: 4, 6, 7, 14

优化后的代码

// Phase 1: Cells move randomly and divide until final number of cells is reached
while (n<finalNumberCells) {
	produceSubstances(L,Conc, posAll, typesAll, n); // Cells produce substances. Depending on the cell type, one of the two substances is produced.
	runDiffusionStep(L,Conc, D); // Simulation of substance diffusion
	runDecayStep(L,Conc, mu);
	n = cellMovementAndDuplication(posAll, pathTraveled, typesAll, numberDivisions, pathThreshold, divThreshold, n);
	omp_set_num_threads(240);
	#pragma omp parallel for simd schedule (static) private (c,d) if (n>500)
	for (c=0; c<n; ++c) {
		// boundary conditions
		for (d=0; d<3; d++) {
			if (posAll[c][d]<0) {
				posAll[c][d]=0;
			}else if (posAll[c][d]>1) posAll[c][d]=1;
		}
	}
}

修改的代码行: 4-6, 8, 9, 15

优化说明

用于优化这部分代码的方法有两种: 借助 SIMD 实现 OpenMP 线程并行化和减少 if 子句。

因为目录是静态的,所以借助 SIMD 实现 OpenMP 线程并行化非常实用。另外我们知道,循环大小和每次迭代的成本是一样的,因此所有线程的负载成本都是相同的。 if n>500 条件可帮助降低前 500 个 n 值的额外开销。

原始代码通常执行两次校验。 优化后的代码将 if 校验次数降为一次,只在需要时执行第二次 if 校验。这样可以减少对比的总次数。

主 – 阶段 2 结束

原始代码

for (c=0; c<n; c++) {
	posAll[c][0] = posAll[c][0]+currMov[c][0];
	posAll[c][1] = posAll[c][1]+currMov[c][1];
	posAll[c][2] = posAll[c][2]+currMov[c][2];

	// boundary conditions: cells can not move out of the cube [0,1]^3
	for (d=0; d<3; d++) {
		if (posAll[c][d]<0) {posAll[c][d]=0;}
		if (posAll[c][d]>1) {posAll[c][d]=1;}
	}
}

修改的代码行:  2-4, 9

优化后的代码

#pragma omp parallel for simd schedule (static) private (c,d) if (n>500)
for (c=0; c<n; ++c) {
	__assume_aligned((float*)posAll, 64);
	__assume_aligned((float*)currMov[c], 64);
	posAll[c][0:3] += currMov[c][0:3];
	// boundary conditions: cells can not move out of the cube [0,1]^3
	for (d=0; d<3; d++) {
		if (posAll[c][d]<0) {
			posAll[c][d]=0;
		}else if (posAll[c][d]>1) posAll[c][d]=1;
	}
}

修改的代码行: 1, 3-5, 10

优化说明

这最后一部分代码与阶段 1 的优化方式相同: 借助 SIMD 实现 OpenMP 线程并行化和减少 if 子句。

因为目录是静态的,所以借助 SIMD 实现 OpenMP 线程并行化非常实用。另外我们知道,循环大小和每次迭代的成本是一样的,因此所有线程的负载成本都是相同的。 if n>500 条件可帮助降低前 500 个 n 值的额外开销。

原始代码通常执行两次校验。 优化后的代码将 if 校验次数降为一次,只在需要时执行第二次 if 校验。 这样可以减少对比的总次数。

其他优化

除上述代码各部分列出的优化方法外,还有许多其他优化方法尚未探讨:

  • 将所有后增量定义为前增量。 后增量拷贝实际变量,然后对其进行增量处理。 前增量不进行拷贝,所以速度更快。 只有当增量的点不影响结果时,才可以实现这一性能提升。
  • 在所有阵列上实现内存对齐。 如果阵列对齐,内存操作的速度更快,而且 CPU 无需标记或转换数据。

被否定的优化技巧

在开发最后一部分代码时,我试验了其他并行化和矢量化技巧,比如英特尔 Cilk Plus、英特尔® 线程构建模块(英特尔® TBB),以及英特尔 MKL 中的部分数学线程优化函数。 使用英特尔 Cilk Plus 会降低代码的速度,尽管造成这种结果的原因是我不完全了解如何使用英特尔 Cilk 函数。

由于 OpenMP 函数的表现非常好,我没有使用英特尔 TBB 函数进行其他测试,因此英特尔 TBB 可能也能够提升性能。

另外还可以使用英特尔 MKL 函数,它在处理大量数据时表现良好。 尽管代码移动大量数据,但即时数据的量非常小。 由于即时数据量小(以及其他原因),采用英特尔 MKL 函数进行优化并不十分可行。

关于作者

 

Daniel Vea Falguera 是电子系统工程专业的学生,也是一名创业者,在电子与计算机编程方面拥有浓厚的兴趣和深厚的功底。

链接

实施的大部分代码优化均基于文章《英特尔至强融核协处理器》:https://software.intel.com/zh-cn/articles/optimization-and-performance-tuning-for-intel-xeon-phi-coprocessors-part-1-optimization

如欲了解其他评论过的选项和解决方案,请访问英特尔并行架构现代代码论坛:

https://software.intel.com/zh-cn/forums/intel-moderncode-for-parallel-architectures

英特尔® MKL:https://software.intel.com/zh-cn/mkl-reference-manual-for-c

英特尔® TBB:https://www.threadingbuildingblocks.org/

英特尔® Cilk Plus:https://www.cilkplus.org/

OpenMP*: http://openmp.org/wp/

 

Para obtener información más completa sobre las optimizaciones del compilador, consulte nuestro Aviso de optimización.