c++ 正在寻找一种使用openMP和递归代码的好方法吗?

fjnneemd  于 2023-05-24  发布在  其他
关注(0)|答案(4)|浏览(150)

我想用openMP优化递归。所以,我从这个问题开始:best-way-to-parallelize-this-recursion-using-openmp
在优化递归函数时,我首先对openMP感兴趣。从下面的代码开始,我发现那里(https://en.cppreference.com/w/cpp/chrono):

#include <iostream>
#include <chrono>
 
long fibonacci(unsigned n)
{
    if (n < 2) return n;
    return fibonacci(n-1) + fibonacci(n-2);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    std::cout << "f(42) = " << fibonacci(42) << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

用g++编译,没有任何优化,它给出的结果是:

f(42) = 267914296
elapsed time: 1.88232s

然后我用类似的openMP的答案向上...但我不得不停止这个过程,因为它花了很长很长的时间。我不知道很多关于openMP递归并行,因为我只是用它来优化我的代码中的循环。另外,我在GCC文档中发现了一些东西:

__attributes__((const))

然后我把它添加到我的openMP“优化”版本,但我得到的只是一个核心转储!!!
所以我删除了我的omp杂注,以检查核心转储是由于我的代码或其他东西...
然后代码变成了:

#include <iostream>
#include <chrono>

__attribute__ ((const))
long fibonacci(unsigned n)
{
   
   int long r[2];

   if (n < 2) return n;
   r[0]=fibonacci(n-1);
   r[1]=fibonacci(n-2);
   return (r[0]+r[1]);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    std::cout << "f(42) = " << fibonacci(42) << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

我使用以下命令编译了它:

g++ fibonacci.cpp -o fibonacci -Wall -O2 -march=native

现在,执行42的Fibonacci计算所需的时间要少得多:

f(42) = 267914296
elapsed time: 0.00106504s

在保存模式和性能模式中,它给出

f(42) = 267914296
elapsed time: 0.000187806s

这里是我的计算机的lscpu(如果有人想比较结果):

Architecture:            x86_64
  CPU op-mode(s):        32-bit, 64-bit
  Address sizes:         39 bits physical, 48 bits virtual
  Byte Order:            Little Endian
CPU(s):                  8
  On-line CPU(s) list:   0-7
Vendor ID:               GenuineIntel
  Model name:            Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz
    CPU family:          6
    Model:               158
    Thread(s) per core:  2
    Core(s) per socket:  4
    Socket(s):           1
    Stepping:            9
    CPU(s) scaling MHz:  95%
    CPU max MHz:         4500,0000
    CPU min MHz:         800,0000
    BogoMIPS:            8403,00

现在,我不知道用openMP要花多少时间,因为我没能得到满意的结果...我希望有人能找到一种方法,将omp杂注添加到这种递归情况中。
根据需要,这里是这个案例的omp版本1:

#include <iostream>
#include <chrono>
#include <omp.h>

__attribute__ ((const))
long fibonacci(unsigned n)
{
   
    int long a, b;

    if (n < 2) return n;
    #pragma omp parallel
    #pragma omp single nowait
    {         
       #pragma omp task shared(a)
       a=fibonacci(n-1);
       #pragma omp task shared(b)
       b=fibonacci(n-2);
       #pragma omp taskwait
    }
    return (a+b);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    std::cout << "f(42) = " << fibonacci(42) << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

和版本,驱动我到一个“愚蠢的代码”:

#include <iostream>
#include <chrono>
#include <omp.h>

__attribute__ ((const))
long fibonacci(unsigned n)
{
   
   int long r[2];
   int i;
   if (n < 2) return n;
   #pragma omp parallel
   #pragma omp single
   for(i=0;i<2;++i) 
   {
       #pragma omp task shared(r)   
       r[i]=fibonacci(n-i-1);
   }      
    
    return (r[0]+r[1]);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    std::cout << "f(42) = " << fibonacci(42) << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

这些omp版本的构建命令为:

g++ omp_fibonacci_for.cpp -o omp_fibonacci_for -Wall -fopenmp

它们似乎都是“永远”循环的(幸运的是Ctrl-C工作得很好)。但是,在写这个问题的结尾时,第二个版本用“for”,处理成功:

f(42) = 267914296
elapsed time: 480.953s

需要说明的是,我不是在寻找“最佳性能”,我的目标是试图获得一个在合理时间内处理的omp版本,目的是找到在递归代码上使用openMP的方法。老实说,我期待一个更好的执行时间比480 s!我也很惊讶它在“差调试”版本中所花的时间,它花了更少的0. 001秒来处理...
现在的问题是,我想知道我使用openMP的方式是否可以接受,如果我将这个模型应用于“更复杂”的任务,或者如果我做错了什么。

iszxjhcz

iszxjhcz1#

如果你想快速计算斐波那契数,你通常希望从使用迭代而不是递归开始。

unsigned long long fib(unsigned input) {
    unsigned long long old1 = 0;
    unsigned long long old2 = 1;

    for (int i = 1; i < input; i++) {
        unsigned long long sum = old1 + old2;
        old1 = old2;
        old2 = sum;
    }
    return old2;
}

请注意,我已经对此进行了一些修改,使用64位无符号long long,所以我们可以使用稍大的数字进行测试。64位数足够大以计算F(93)。
做了一些测试,似乎high_resolution_clock(至少在我的系统上)可以测量的最短间隔大约是427纳秒。如果我在关闭优化的情况下编译它,然后计算F(93),它通常会说它在427 ns内运行,但有时会在855 ns内运行。如果我打开优化,那么时间意味着什么,它通常显示为0 ns,但偶尔显示为427 ns。
做一些计算,我猜它真的更像是30- 35 ns(大约每次迭代1个时钟,93次迭代/ 2.8 GHz)。在你的CPU上可能接近20- 25 ns。

OpenMP/线程

我发现在这个任务中使用线程有两个问题。
首先,当你有彼此独立的东西时,线程工作得最好(例如,一个循环中的每个迭代都执行相同的操作,但每个迭代都在不同的数据上)。斐波那契数的情况正好相反--每次计算都取决于前一次计算的结果,所以很难并行进行。
第二,使用线程会增加一些开销来创建线程、向线程传递数据、从线程收集数据等等。为了让线程工作,你需要做足够的工作,每个线程做的工作要比让它做这些工作的开销多得多。同样,这几乎与本案相反。在一个典型的系统上,创建一个线程所花费的时间比上面的代码计算F(93)所花费的时间要长得多。
尝试使用线程的这项工作就像试图写一个字在一张纸上的几个人每个人写一封信的一部分,然后把纸传给下一个人。一个人可以在更短的时间内写出整个单词,而不是把一张纸传来传去。

6l7fqoea

6l7fqoea2#

你可以从一个好的递归算法(无优化)开始,将你的原始结果提高几个数量级:

#include <iostream>
#include <chrono>

long fibonacci_recursive(unsigned n, unsigned result, unsigned next)
{
    if (n == 0)
    {
        return result;
    }

    return fibonacci_recursive(n - 1, next, result + next);
}

long fibonacci(unsigned n)
{
    return fibonacci_recursive(n, 0, 1);
}

int main()
{
    auto start = std::chrono::steady_clock::now();
    std::cout << "f(42) = " << fibonacci(42) << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

输出

% ./a.out
f(42) = 267914296
elapsed time: 4.8508e-05s
%
ivqmmu1c

ivqmmu1c3#

你的是一个愚蠢的实现。
电话

return fibonacci(n-1) + fibonacci(n-2);

应该是一个(未)有序集合的查找。

hfwmuf9z

hfwmuf9z4#

完美解决方案

因此,在考虑了所有的注解和答案之后,这个新代码似乎是在递归代码上应用openMP pragmas的更好方法:

#include <iostream>
#include <chrono>
#include <omp.h>

__attribute__ ((const))
long fibonacci(unsigned n)
{
   
    int long a, b;

    if (n < 2) return n;
    #pragma omp task shared(a)
    a=fibonacci(n-1);
    #pragma omp task shared(b)
    b=fibonacci(n-2);
    #pragma omp taskwait
    
    return (a+b);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    long res;
    #pragma omp parallel
    {
        #pragma omp single
        res = fibonacci(42);
    }
    std::cout << "f(42) = " << res << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

生成命令:

g++ omp_fibonacci.cpp -o omp_fibonacci -Wall -fopenmp

输出:

f(42) = 267914296
elapsed time: 255.225s

@DanielLangr的评论特别有用。

候选方案

这是我用openMP得到的最好的结果。
而且,随着@JerryCoffin的回答和@ThomasMatthiew的评论以及@VictorEijkhout的建议......我对并行化有不同的看法,它驱使我写这段代码:

#include <iostream>
#include <chrono>
#include <omp.h>

__attribute__ ((const))
long fibonacci(unsigned n)
{ 
    int long a, b;
    if (n < 2) return n;
        a=fibonacci(n-2);
        b=fibonacci(n-1);
    return (a+b);
}

long omp_fibonacci(unsigned n)
{
    int long a, b;
    if (n < 2) return n;
    #pragma omp task shared(a)
    a=fibonacci(n-1);
    #pragma omp task shared(b)
    b=fibonacci(n-2);
    #pragma omp taskwait
    
    return (a+b);
}
 
int main()
{
    auto start = std::chrono::steady_clock::now();
    long res;
    #pragma omp parallel 
    {
        #pragma omp single
        res = omp_fibonacci(42);
    }
    std::cout << "f(42) = " << res << '\n';
    auto end = std::chrono::steady_clock::now();
    std::chrono::duration<double> elapsed_seconds = end-start;
    std::cout << "elapsed time: " << elapsed_seconds.count() << "s\n";
}

输出:

f(42) = 267914296
elapsed time: 1.00274s

相关问题