使用非线程安全随机数生成器在 C 中为 pi monte carlo 更正 OpenMP 编译指示

Posted

技术标签:

【中文标题】使用非线程安全随机数生成器在 C 中为 pi monte carlo 更正 OpenMP 编译指示【英文标题】:Correct OpenMP pragmas for pi monte carlo in C with not thread-safe random number generator 【发布时间】:2013-12-25 11:42:30 【问题描述】:

我需要一些帮助来通过给定的随机数生成器使用蒙特卡罗方法和 openmp 并行计算 pi,这不是线程安全的。

首先:This SO 线程对我没有帮助。

我自己的尝试是以下#pragma omp 语句。我认为 i、x 和 y 变量应该由每个线程初始化,并且应该是私有的。 z 是圈内所有命中的总和,因此它应该在 for 循环之后的隐含屏障之后求和。

认为主要问题是随机数生成器的静态 var。我在调用函数的地方做了一个关键部分,这样每次只有一个线程可以执行它。但是 Pi 解决方案不会随着更高的值扩展。

注意:我不应该使用另一个 RNG,但可以对其进行一些小改动。

int main (int argc, char *argv[]) 

    int i, z = 0, threads = 8, iters = 100000;
    double x,y, pi;

    #pragma omp parallel firstprivate(i,x,y) reduction(+:z) num_threads(threads)
        for (i=0; i<iters; ++i) 
            #pragma omp critical
            
                x = rng_doub(1.0);
                y = rng_doub(1.0);
            
            if ((x*x+y*y) <= 1.0)
                z++;
        

    pi = ((double) z / (double) (iters*threads))*4.0;
    printf("Pi: %lf\n", pi);;
    return 0;

这个RNG其实是一个包含的文件,但是由于我不确定我创建的头文件是否正确,所以我将它集成到另一个程序文件中,所以我只有一个.c文件。

#define RNG_MOD 741025

int rng_int(void) 
    static int state = 0;

    return (state = (1366 * state + 150889) % RNG_MOD);


double rng_doub(double range) 
    return ((double) rng_int()) / (double) ((RNG_MOD - 1)/range);

我也尝试过将静态 int 状态设为全局,但它不会改变我的结果,也许我做错了。所以请你能帮我做出正确的改变吗?非常感谢!

【问题讨论】:

能否请您澄清一下这些词的含义:“但是 Pi 解决方案无法随着更高的值扩展。” 除了关键部分将您的序列化这一事实之外线程,你不会得到任何加速,代码对我来说看起来是正确的。 是的,当然。我的意思是,如果我运行更多的迭代,计算出的 pi 应该更接近真实的 pi 值。但是使用这个随机数生成器,我一般看不到这种行为。讲解员说这是因为状态变量的线程不安全。我应该将其设置为全局并使用一个或多个正确的#pragma omp 语句来处理它。但是我尝试了很多组合,但没有任何改变。不知道,如果我将状态变量设为全局变量,而不是静态变量?在这个地方很关键吗?需要共享状态()?还是更好的线程私有(状态)?已经尝试了很多。 我用你的随机函数更新了我的答案。 您的讲解员可能提到的 OpenMP 结构是 threadprivate。请参阅下面的半答案,了解为什么它不会大大改善解决方案。 @DannyArcher,我使用 Hristo 的一些建议更新了我的答案。 【参考方案1】:

试试下面的代码。它为每个线程创建一个私有状态。我对 rand_r 函数 Why does calculation with OpenMP take 100x more time than with a single thread? 做了类似的事情

编辑:我使用 Hristo 的一些建议更新了我的代码。我使用了 threadprivate(第一次)。我还使用了更好的 rand 函数,它可以更好地估计 pi,但仍然不够好。

一个奇怪的事情是我必须在 threadprivate 之后定义函数 rng_int 否则我得到一个错误“错误:'state' 在第一次使用后声明为 'threadprivate'”。我可能应该问一个关于这个的问题。

//gcc -O3 -Wall -pedantic -fopenmp main.c
#include <omp.h>
#include <stdio.h>

#define RNG_MOD 0x80000000
int state;

int rng_int(void);
double rng_doub(double range);

int main() 
    int i, numIn, n;
    double x, y, pi;

    n = 1<<30;
    numIn = 0;

    #pragma omp threadprivate(state)
    #pragma omp parallel private(x, y) reduction(+:numIn) 
    

        state = 25234 + 17 * omp_get_thread_num();
        #pragma omp for
        for (i = 0; i <= n; i++) 
            x = (double)rng_doub(1.0);
            y = (double)rng_doub(1.0);
            if (x*x + y*y <= 1) numIn++;
        
    
    pi = 4.*numIn / n;
    printf("asdf pi %f\n", pi);
    return 0;


int rng_int(void) 
   // & 0x7fffffff is equivalent to modulo with RNG_MOD = 2^31
   return (state = (state * 1103515245 + 12345) & 0x7fffffff);


double rng_doub(double range) 
    return ((double)rng_int()) / (((double)RNG_MOD)/range);

您可以在http://coliru.stacked-crooked.com/a/23c1753a1b7d1b0d查看结果(并编辑和运行代码)

【讨论】:

非常感谢。到目前为止,这有效。但是如果我增加 n,结果并没有变得更接近 pi。尝试了10000000。而且如果我整合线程数,我也应该增加,结果也不同,但不是正确的方式。认为这取决于 rng 函数本身,所以不是你的错。但现在我想我明白如何处理状态变量了,所以到目前为止我知道的就足够了。再次非常感谢您。 这可能是使用自定义随机数生成器的问题。我对伪随机数生成器知之甚少。我觉得使用rand_r 更舒服。但重点是您希望为每个线程使用不同的私有种子,而不是共享种子。 也可能存在计算状态的溢出。在计算状态时,您可能希望使用 64 位整数。 使用线程私有全局状态而不是将其作为参数传递给每个调用怎么样? @HristoIliev,我使用 threadprivate 编辑了代码(我第一次使用它)并按照您的建议更改了 rand 函数。我遇到了一个奇怪的错误,我解决了“错误:'state'在第一次使用后声明了'threadprivate'”。也许我会问一个关于它的问题。【参考方案2】:

您的原始线性全等 PRNG 的循环长度为 49400,因此您仅获得 29700 个唯一测试点。这是一个糟糕的生成器,可用于任何类型的蒙特卡洛模拟。即使您进行了 100000000 次试验,您也不会更接近 Pi 的真实值,因为您只是一遍又一遍地重复相同的点,因此 ziters 的最终值都只是乘以相同的常数,最后在除法中取消。

Z boson 引入的每线程种子稍微改善了这种情况,唯一点的数量随着 OpenMP 线程总数的增加而增加。增加不是线性的,因为如果一个 PRNG 的种子落在另一个 PRNG 的序列中,则两个 PRNG 都会产生相同的序列,但不超过 49400 个元素。给定周期长度,每个 PRNG 覆盖 49400/RNG_MOD = 总输出范围的 6.7%,即两个 PRNG 同步的概率。总共有 RNG_MOD/49400 = 15 个可能的唯一序列。这基本上意味着在最好的播种情况下,您将无法超过 30 个线程,因为任何其他线程只会重复其他一些线程的结果。乘数 2 来自这样一个事实,即每个点使用序列中的两个元素,因此如果将序列移动一个元素,就有可能得到一组不同的点。

最终的解决方案是完全放弃你的 PRNG 并坚持使用类似 Mersenne twister MT19937 的东西,它的循环长度为 219937 - 1 和非常强大的播种算法。如果您无法使用问题中所述的其他 PRNG,至少修改 LCG 的常量以匹配 rand() 中使用的常量:

int rng_int(void) 
   static int state = 1;

   // & 0x7fffffff is equivalent to modulo with RNG_MOD = 2^31
   return (state = (state * 1103515245 + 12345) & 0x7fffffff);

请注意,rand() 不是一个好的 PRNG - 它仍然很糟糕。它比您代码中使用的要好一点。

【讨论】:

这是很好的信息 Hristo (+1)。这就解释了为什么即使使用rand_r 也会产生不好的结果。我会说这是你的错,因为我刚刚复制了你openmp-program-is-slower-than-sequential-one,但是我在另一个答案中看到你为rand_r 写的“请注意,这是一个非常糟糕的蒙特卡罗模拟生成器,erand48 更好,最好的是如果可用,请使用 GNU Scientific Library 中的“Mersenne Twister”类型生成器。我应该阅读更多。 如果你对并行随机数生成感兴趣,你也应该看看在 SC11 上获得最佳论文奖的thesalmons.org/john/random123/papers/random123sc11.pdf。 感谢您的论文。我把它打印出来并阅读它。 Broadwell 处理器将在几个月后问世,并将具有 RDSEED 指令,该指令将生成真正的随机数。我想知道这会产生什么影响。

以上是关于使用非线程安全随机数生成器在 C 中为 pi monte carlo 更正 OpenMP 编译指示的主要内容,如果未能解决你的问题,请参考以下文章

随机数生成器的 C++11 线程安全

在MPI C中为每个进程生成随机数

在 C# 中创建加密随机数的最快、线程安全的方法?

如何在 Ruby on Rails 4 中为 post 生成唯一的随机 id?

多线程环境下生成随机数

为什么 Random.Shared 是线程安全的