• OpenMp并行提升时间为什么不是线性的?


    最近在研究OpenMp,写了一段代码,如下:

    #include<time.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<omp.h>
    
    #define THREAD_NUM 8
    int main()
    {
     clock_t start,finish;
    int n=80000000;
    
    int sum;
    start=clock();
    for(int i=0;i<n;i++)
    {
     sum+=2;
     sum-=1;
    }
    
    finish=clock();
    printf("Serial computation
    ");
    printf("time:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
    printf("Parallel computation
    ");
    start=clock();
    #pragma omp parallel num_threads(THREAD_NUM)
    {
     int nth=omp_get_num_threads();
     int me=omp_get_thread_num();
     int mysum=0;
     clock_t t1,t2;
     
     t1=clock();
     for(int i=me;i<n;i+=nth)
     {
      mysum+=2;
      mysum-=1; 
     }
     t2=clock();
     printf("time:%lf	%d
    ",(double)(t2-t1)/CLOCKS_PER_SEC,mysum);
    }
    
    finish=clock();
    printf("Total time:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
     return 0;
    }

    输出结果:

    Serial computation
    time:0.356796
    Parallel computation
    time:0.154885    10000000
    time:0.221016    10000000
    time:0.284257    10000000
    time:0.253218    10000000
    time:0.296142    10000000
    time:0.269889    10000000
    time:0.312325    10000000
    time:0.275955    10000000
    Total time:0.322763

    上面的结果很奇怪,程序开了8个线程,可是得到的结果却不是1/8,考虑到线程的创建等等开销,提升的幅度达不到8倍,但是也不至于就1.1倍左右啊;而且每个线程只做了计算的1/8次迭代,消耗的时间远远大于1/8的时间。

    思考一下可能存在以下原因:

    1) 线程中的printf这种函数并不是并行安全的,所以各个线程在最后快要结束的时候会争抢控制台资源,不过占用不了太多时间。

    2) 线程的创建和撤销存在一定的消耗,不过个人觉得这部分也不会占用太多时间,如果这个结论成立,那么增加线程的计算时间,是不是可以提升幅度呢?

    3) false sharing(参见我的上一篇博文),目测并不是false sharing的原因。

    4) 存在其他冲突的资源,导致了线程之间存在关联,并不能完全并行。

    5) 代码中的for循环在执行时的问题。

    对上面的几点疑问,逐个进行了探讨。


    线程的创建核撤销的消耗

    增加线程的计算时间,那么提升的幅度会不会增加呢?考虑到此,做了如下的实验,将代码中的n改成160000000,那么得到的运行结果如下:

    Serial computation
    time:0.640447
    Parallel computation
    time:0.319365    20000000
    time:0.503179    20000000
    time:0.579748    20000000
    time:0.581418    20000000
    time:0.629072    20000000
    time:0.592573    20000000
    time:0.634568    20000000
    time:0.609349    20000000
    Total time:0.646393

    这次的效果更糟糕,而且总的并行时间是比串行的还要慢,再看看单个线程的时间,虽然计算了1/8的迭代,可是时间除了第一个线程使用原先1/2时间外,剩下的几乎等于串行的时间。从实验的结果上来看,增加一倍迭代次数后,单个线程消耗的时间大致也会提高一倍,因此线程的创建和撤销的因素基本可以忽略。一定是某个原因导致了计算时间的快慢。


    false sharing

    程序代码中并行的部分全是私有化的变量,甚至都没有将mysum累加到主线程中,不会发生false sharing,这一点可以排除。


    for循环

    for循环会不会出现猫腻呢?为此也做了以下的实验:

    #include<stdio.h>
    #include<stdlib.h>
    #include<time.h>
    int main()
    {
     int n=10000;
     int sum=0;
     clock_t start,finish;
     start=clock();
     for(int i=0;i<n;i++)
      for(int j=0;j<n;j++)
       {
          sum++;
          sum--;
       }
     finish=clock();
     printf("time1:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
     start=clock();
     for(int i=0;i<n/100;i++)
      for(int j=0;j<n*100;j++)
       {
          sum++;
          sum--;
       }
     finish=clock();
     printf("time2:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
    
     start=clock();
     for(int i=0;i<n*100;i++)
      for(int j=0;j<n/100;j++)
       {
          sum++;
          sum--;
       }
     finish=clock();
     printf("time3:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
     start=clock();
      for(int j=0;j<n*n;j++)
       {
          sum++;
          sum--;
       }
     finish=clock();
     printf("time4:%lf
    ",(double)(finish-start)/CLOCKS_PER_SEC);
    
    }

    输出结果:

    time1:0.431033
    time2:0.377387
    time3:0.383699
    time4:0.372852

    结果是循环相同的次数,单层是最快的,而外层和里层次数一样是最慢的,因为CPU 跨切循环层。

    另外插一个题外话,for循环遍历不当也会引起false sharing,我们看下面的例子:

    http://blog.chinaunix.net/attachment/201109/17/25923232_13162294504q9g.jpg

     右边的循环之所以比左边的效率高,与程序访问的局部性和Cache命中率有关。数组在计算机中是行优先存储的,左边的循环中,依次访问的是变量a[0][0],a[1][0],a[2][0],...,a[99][0],a[0][1],a[1][1],a[2][1],……,a[99][1],……这实际上是按照列优先的原则在访问数组元素。如果Cache容量相对于数组容量而言不够大,考虑一个极端情况,假设Cache只有一个块,只能存储一行数据,则每访问一个元素就会发生一次Cache失效,就需要访问一次主存,读入一块数据,导致存储系统效率低下,明显影响操作延迟。而右边的循环采用的是行优先访问原则,与元素存储顺序一致。基于同样的假设,此时只有访问新一行的第一个数据时才发生Cache失效,通过访问主存读入一块连续的数据(恰为数组的一行),此后访问同行数据便可直接使用Cache中缓存的数据,直到访问下一行的第一个数据。Cache失效率降低了,整个存储系统的平均访问延迟降低了,显然程序执行效率较高。

    言归正传,从for循环的实验中可以看出并不是计算时间的问题。


    以上的几个方面都做了实验,都找不到问题所在,研究了几天了,姑且放在这吧。知道问题所在的,往告知,不甚感谢~~

  • 相关阅读:
    js获取url参数值的方法总结
    Tomcat的配置与在IDEA上使用Tomcat
    windows配置并启动apache的方法
    图解 | 你管这破玩意叫计算机?
    【.NET 与树莓派】PWM 调节LED小灯的亮度
    【.NET 与树莓派】i2c(IIC)通信
    【.NET 与树莓派】矩阵按键
    【.NET 与树莓派】使用 GPIO 库
    【.NET 与树莓派】上手前的一些准备工作
    《红楼梦》最经典的12首诗词,读懂了才是人生
  • 原文地址:https://www.cnblogs.com/wzyj/p/4504772.html
Copyright © 2020-2023  润新知