代码:
double x(){return (double)rand()/(double)RAND_MAX;}
double y(){return (double)rand()/(double)RAND_MAX;}
double z(){return (double)rand()/(double)RAND_MAX;}
int d(double x, double y, double z){
if ( ( (pow(x,2)+pow(y,2)) <1 ) && ( z<=1 && z>=0 )) return 1;
return 0;
}
double f(double x, double y, double z){
return 1;
}
#pragma omp parallel default(none) private(id,numt,j,local_sum,local_good_dots,local_coi,x_,y_,z_) shared(total_sum,good_dots,count_of_iterations)
{
local_coi = count_of_iterations;
id = omp_get_thread_num() + 1;
numt = omp_get_num_threads();
#pragma omp for
for (j = 1; j <= local_coi; j++){
x_=x();
y_=y();
z_=z();
if (d(x_,y_,z_) == 1){
local_sum += f(x_,y_,z_);
local_good_dots += 1;
}
}
#pragma omp critical
{
total_sum = total_sum + local_sum;
good_dots = good_dots + local_good_dots;
}
}
评论:此代码是用于计算区域f()
中函数d()
的三维积分的蒙特卡罗方法的实现。
我希望,这个代码在多线程模式(openmp)中运行得更快。
但出了点问题。
经过几个小时的修改(reduction
在openmp pragma中,if条件的简化(如f(x_,y_,z_) * d(x_,y_,z_)
))我不明白,为什么这个简单的循环在更大的线程数上变得更慢。
但是在我为循环之前的每个坐标生成一个三维数组并将其放在shared
之后,我的程序变得更快。
所以,问:
如何修改此代码以及并行块中允许哪些函数(操作)?
P.S:如我所见,rand
函数不被允许(或者我错了?)
感谢您的帮助!
修改(@ HristoIliev的帮助)
double x(){return (double)rand()/(double)RAND_MAX;}
double y(){return (double)rand()/(double)RAND_MAX;}
double z(){return (double)rand()/(double)RAND_MAX;}
int d(double x, double y, double z){
if ( ( (pow(x,2)+pow(y,2)) <1 ) && ( z<=1 && z>=0 )) return 1;
return 0;
}
double f(double x, double y, double z){
return 1;
}
#pragma omp parallel default(none) private(j,local_coi,x_,y_,z_) shared(count_of_iterations) reduction(+:total_sum,good_dots)
{
local_coi = count_of_iterations;
#pragma omp for(prng)
for (j = 1; j <= local_coi; j++){
#pragma omp critical(prng)
{
x_=x();
y_=y();
z_=z();
}
if (d(x_,y_,z_) == 1){
total_sum += f(x_,y_,z_);
good_dots += 1;
}
}
}
答案 0 :(得分:2)
随机数生成器rand()
使用全局静态分配状态,由所有线程共享,因此不是线程安全的。从多个线程中使用它会遇到一个非常糟糕的情况,即对共享变量进行不受保护的访问,这会破坏缓存并降低程序速度。您应该使用rand_r()
或erand48()
- 它们使用您必须提供的单独的状态存储。你必须为每个线程声明一个状态(例如让它private
),基本上为每个线程创建不同的PRNG。然后你必须相应地播种它们,否则你会得到统计上不好的结果。原则上,您可以使用一个rand48()
生成器的输出来播种其他生成器 - 它应该足以获得中等长度的不相关序列。
以下是使用rand_r()
的示例实现(不是这是用于蒙特卡罗模拟的非常糟糕的生成器,erand48
更好,最好的是使用GNU科学图书馆的“Mersenne Twister”型发生器(如果有的话):
unsigned int prng_state;
#pragma omp threadprivate(prng_state)
double x(){return (double)rand_r(&prng_state)/(double)RAND_MAX;}
double y(){return (double)rand_r(&prng_state)/(double)RAND_MAX;}
double z(){return (double)rand_r(&prng_state)/(double)RAND_MAX;}
int d(double x, double y, double z){
if ( ( (pow(x,2)+pow(y,2)) <1 ) && ( z<=1 && z>=0 )) return 1;
return 0;
}
double f(double x, double y, double z){
return 1;
}
...
#pragma omp parallel default(none) \
private(id,numt,x_,y_,z_) \
shared(count_of_iterations) \
reduction(+:total_sum,good_dots)
{
id = omp_get_thread_num() + 1;
numt = omp_get_num_threads();
// Sample PRNG seeding code - DO NOT USE IN PRODUCTION CODE!
prng_state = 67894 + 1337*id;
#pragma omp for
for (j = 1; j <= count_of_iterations; j++){
x_=x();
y_=y();
z_=z();
if (d(x_,y_,z_) == 1){
total_sum += f(x_,y_,z_);
good_dots += 1;
}
}
}
这只是一个非常糟糕的(从质量的角度来看)实现,但它应该让你了解事情是如何工作的。它还可以通过对原始代码进行最少的更改来实现线程安全。基本点是:
prng_state
指令使PRNG状态threadprivate
对每个线程都是私有的; rand_r()
使用特定于线程的状态变量而不是rand()
,x()
和y()
中的z()
;
prng_state = 67894 + 1337*id;
,以便不同的线程(希望)获得不相关的伪随机数流。请注意,rand()
和rand_r()
的质量非常差,这只是一个学术范例。使用更长的PRNG序列,您将获得不同线程中的相关流,这将破坏统计数据。我建议您使用erand48()
重写代码。
要回答您的初始问题 - parallel
块内允许所有线程安全函数调用。您也可以调用非线程安全函数,但必须保护(命名)critical
构造内部的调用,例如:
#pragma omp for
for (j = 1; j <= local_coi; j++) {
#pragma omp critical(prng)
{
x_=x();
y_=y();
z_=z();
}
if (d(x_,y_,z_) == 1) {
local_sum += f(x_,y_,z_);
local_good_dots += 1;
}
}
这将确保不会并行调用rand()
。但是你仍然可以对共享状态进行读 - 修改 - 写入访问,因此与缓存相关的减速。
此外,不尝试重新实现OpenMP reduction
或类似的结构。编译器供应商已经在努力确保以最佳(尽可能快的方式)实现它们。