高性能计算期末复习

高性能计算复习

并行计算

  • 为什么需要并行计算?
    总体来说,并行计算的目的是提高计算性能。为了达到这个目的,在计算机发展的各个阶段有着不同的手段。

    • 提高处理器字长
    • 提高芯片集成度
    • 微体系结构技术革新
    • 提高处理器频率
      此后,由于集成度、指令集并行度、存储器速度、功耗和散热等等问题的限制,单核CPU性能达到极限。于是进入了多核、众核CPU时代。

    与此同时在应用领域计算规模和复杂度也大幅提高,在此种情况下,唯有并行计算才能满足需求。
    答:在计算机处理器单核性能达到极限后发展出了多核、众核系统,在应用领域计算规模和复杂度大幅提升的情况下,唯有将并行计算技术与多核、众核系统结合,才能满足计算需求。

  • 并行计算技术的分类

    • 按照数据和指令处理分类:
      SISD、SIMD、MIMD (I for instructions、 D for data)
    • 按照并行类型分类:
      位级并行、指令级并行、★线程级并行
      其中线程级并行又分为:
      • 数据级并行:一个大的数据块划分为小块,分别由不同的处理器/线程处理
      • 任务级并行:一个大的计算任务划分为子任务分别由不同的处理器/线程来处理
    • 按照存储访问结构分类:
      • 共享内存(所有处理器通过总线共享内存)
      • 分布共享内存(每个处理器有本地独享存储器再共享一个全局存储器)
      • 分布式内存(每个处理器都使用本地独立的存储器)
    • 按照计算特征分类:
      • 数据密集型并行计算:数据量极大、但计算相对简单的并行处理 如:大规模Web 信息搜索
      • 计算密集型并行计算:数据量相对不是很大、但计算较为复杂的并行处理 如:3-D建模与渲染,气象预报,科学计算……
      • 混合型并行计算:如3D电影渲染
    • 按照并行程序设计模型/方法分类:
      • 共享内存型方式:需要提供数据访问同步控制机制以保证并发安全。
      • 消息传递方式:需要提供并行系统中计算节点间数据通信的方法。
      • MapReduce方式:为解决前二者在并行程序设计上的缺陷,为程序员提供了一种简易便捷的并行程序框架。
  • 本课程的核心内容就是围绕以上三种设计模型/方法的技术问题:

    • 共享内存型:共享资源同步机制
    • 分布存储型:消息传递机制、并行计算的同步控制(barrier、同步障)
    • 共同问题:可靠性与容错技术

    而并行计算软件框架平台,如MapReduce就把这些技术问题交由软件实现,将并行程序的底层细节隐藏,大大提高了并行程序设计的便捷性,同时还提供了极高的拓展性和因此带来的系统性能提升。

  • 系统性能和并行度评估

    • 用标准性能评估(Benchmark)方法评估一个并行计算系统的浮点计算能力。
      High-Performance Linpack Benchmark是最为知名的评估工具,TOP500用其进行评估和排名
    • 经典的程序并行加速评估公式Amdahl定律:
      在这里插入图片描述

    其中S是加速比、P为程序可并行比例、N为处理器数目

    • 根据Amdahl定律:一个并行程序可加速程度是有限制的,并非可无限加速,并非处理器越多越好
    • 并行比例vs加速比
      • 50%=>最大2倍
      • 75%=>最大4倍
      • 90%=>最大10倍
      • 95%=>最大20倍

MPI

Message Passing Interface,基于消息传递的高性能并行计算编程接口。

主要功能:

  • 点对点通信
    • 阻塞通信
    • 非阻塞通信
  • 节点集合通信
    • 一对多广播通信
    • 多点计算同步控制
    • 提供对结果的规约(Reduce)计算功能
  • 提供用户自定义类型的数据传输

MPI程序结构

#include <mpi.h>        //MPI程序头文件

main(int argc, char **argv){
  int numtasks, rank;

  MPI_Init(&argc, &argv);       //初始化MPI程序
  
  

  //并行计算程序体                
  

  MPI_Finalize();               //关闭MPI环境
  exit(0);
}

六个API

  1. MPI_Init (argc, argv) : 初始化MPI,开始MPI并行计算程序体
  2. MPI_Finalize: 终止MPI并行计算
  3. MPI_Comm_Size(comm, &size): 确定指定范围内处理器/进程数目
  4. MPI_Comm_Rank(comm, &rank) : 确定一个处理器/进程的标识号
  5. MPI_Send (buf, count, datatype, dest, tag, comm) : 发送一个消息
  6. MPI_Recv (buf, count, datatype, source, tag, comm, status) : 接受消息
  • 注意到,上述的Size、Rank函数是通过传入变量指针的方式来返回结果,函数没有显式的返回值。
  • 上述的comm是一个通信组(communicator)的标识符,一个process可以从属于多个通信组,MPI中对整个系统有一个默认的缺省通信组MPI_COMM_WORLD
  • 一个通信组中的process数量可以通过MPI_Comm_Size(comm, &size)获得。
  • 一个通信组中的某一个process的编号可以通过MPI_Comm_Rank(comm, &rank)获得。
  • 在MPI的样例代码中似乎没有提到会开启的process数量,查阅资料发现MPI程序一般是在编译之后通过mpiexec.exe -n 4 ./yourprogram.exe的方式执行,这里的-n参数就可以指定在本机上开启的process数量。如果需要在多机上部署,则按照以下指令运行mpiexec.exe –hosts n serverip_1 procnum_1 serverip_2 procnum_2 …serverip_n procnum_n –noprompt yourprogram.exe

非阻塞式消息的Send、Recv方法与阻塞式的有差别,需要引入一个request变量,这里不是考试重点,就不细说。

两个示例代码:

  1. 计算大数组元素平方根的和
#include <stdio.h>    
#include <mpi.h>   
#include<math.h>    
#define N=1002
int main(int argc, char** argv){
   int myid, P, source, C=0double  data[N],  SqrtSum=0.0;
   MPI_Status status;   
   char message[100];
   MPI_Init(&argc, &argv);MPI_Comm_rank(MPI_COMM_WORLD, &myid);  
   MPI_Comm_size(MPI_COMM_WORLD,&numprocs);   - -numprocs; /*数据分配时除去0号主节点*/ 
   /*0号主节点,主要负责数据分发和结果收集*/
   if (myid== 0){   
       /*数据分发: 0, */
       for (int  i = 0;  i < N; ++i; ) ) {
           MPI_Send(data[i], 1, MPI_DOUBLE, i%numprocs+1, 1 ,MPI_COMM_WORLD);
       }         
       /*结果收集*/
       for (int  source = 1;  source <= numprocs;  ++source; )  {  
           MPI_Recv(&d, 1, MPI_DOUBLE, source, 99,MPI_COMM_WORLD, &status);  
           SqrtSum += d;  
       }
   } else {    
       /*各子节点接受数据计算开平方,本地累加*/
       for ( i = myid-1; i < N; i=i+numprocs ; ){
           MPI_Recv(&d, 1, MPI_DOUBLE, 0, 1,MPI_COMM_WORLD, &status); SqrtSum+=sqrt(d);
           C++;  
       }
       /*本地累加结果送回主节点*/    
       MPI_Send(SqrtSum, 1, MPI_DOUBLE, 0, 99,MPI_COMM_WORLD); 
       printf("I am process %d. I recv total %d from process 0, and SqrtSum=%f.\n", myid, C, SqrtSum);
   }
   MPI_Finalize();
} 

  1. 利用规约函数计算∫(0~10)x²dx
#define N 100000000
#defined  a 0
#defined  b 10
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include “mpi.h”
int main(int argc, char** argv){  
    int myid,numprocs;  int i;   
    double local=0.0, dx=(b-a)/N; /*  小矩形宽度  */   
    double inte, x; 
   MPI_Init(&argc, &argv);
   MPI_Comm_rank(MPI_COMM_WORLD, &myid);  
   MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
   /* 根据节点数目将N个矩形分为图示的多个颜色组 */
   for(i=myid;i<N;i=i+numprocs)    {  
       // 每个节点计算一个颜色组的矩形面积并累加   
       x = a + i*dx +dx/2; //  以每个矩形的中心点x值计算矩形高度       
       local +=x*x*dx;  // 矩形面积 = 高度x宽度=y*dx  
   }  
    /* 规约所有节点上的累加和并送到主节点0 */
   MPI_Reduce(&local,&inte,1,MPI_DOUBLE,MPI_SUM, 0, MPI_COMM_WORLD);    
   /* 主节点打印累加和*/ 
   if(myid==0){         
       printf("The integal of x*x in region [%d,%d] =%16.15f\n", a, b, inte); 
   }  
   MPI_Finalize();
 } 

MPI优缺点

  • 优点
    • 灵活性高,适用于各种并行计算任务
    • 有完整的规范,可移植性好
    • 有多家厂商和机构实现
  • 缺点
    • 对任务和数据没有良好的划分支持
    • 不支持分布式文件系统,进而无法进行分布式数据存储管理
    • 通信开销大,当任务复杂、节点数量多时难以处理
    • 容灾性差,没有节点恢复机制
    • 缺少良好的架构支持,需要程序员考虑很多实现细节

Pthread并发控制

  • 自旋锁(忙等待) --能够控制线程进入临界区的顺序
   y = Compute ( my_rank );
   while ( flag != my_rank );  //通过对flag的等待,保证多个线程对x这个累加值的线性访问
   x= x+y ;
   flag++;
//  flag initialized to 0 by main thread

自旋会大量消耗CPU时间,造成资源浪费。

  • pthread_mutex_t(互斥锁) --无法控制线程进入临界区的顺序

    • 创建和初始化
    pthread_mutex_t mutex; 
    pthread_mutex_init(&mutex, NULL)
    
    • 加锁
    pthread_mutex_lock(&mutex)
    
    • 解锁
    pthread_mutex_unlock(&mutex)
    
    • 释放资源
    pthread_mutex_destroy(&mutex)
    
    //声明线程
    pthread_t tid  
    //tfn为线程执行的函数 创建后立刻开启线程 
    pthread_create(&tid, NULL, tfn, NULL); 
    //等待某一个线程返回 参数为线程标识符、用来接收线程返回值的变量指针
    int pthread_join(pthread_t __th, void **__thread_return);
    
  • Semaphores 信号量

        #include <semaphore.h>
    
        void main{
            sem_t sem;  //声明一个信号量
            sem_init(&sem,10,10);   //初始化信号量 规定最大数值为10 初始大小为10
    
            sem_wait(&sem);     //阻塞式地请求一个资源,如果当前sem数值大于0,则会立即返回
    
            sem_post(&sem);     //归还一个资源,给当前的sem数值加一
    
            sem_destroy(&sem);  //释放资源
        }
    
        //信号量通常是配合mutex一起使用,作用是控制对mutex进行竞争的线程数量
        //例如经典的生产者、消费者问题
        /*
        在不使用信号量的情况下,对于生产者和消费者线程,都可以竞争mutex,进而可能导致库存为0时请求消费以及库存满时请求加货,这都可以在临界区内通过业务逻辑进行额外判断,但是会造成不必要的开销甚至极端情况下的库存"饥饿"或"溢出"的情况。
        在引入信号量后,就可以控制生产者和消费者线程对mutex的竞争,进而保证库存满时不让生产者进来,库存为0时不让消费者进来。
        */
    
        sem_t full,empty;
        pthread_mutex_t mutex;
        sem_init(&full,10,10);
        sem_init(&empty,10,0);
        pthread_mutex_init(&mutex,NULL);
        int stock=10;   //库存变量
    
        //消费者线程
        void Consumer(){
            while(1){
                sem_wait(full);
                pthread_mutex_lock(&mutex);
                stock--;
                pthread_mutex_unlock(&mutex);
                sem_post(empty);
            }
        }
    
        //生产者线程
        void Producer(){
            while(1){
                sem_wait(empty);
                pthread_mutex_lock(&mutex);
                stock++;
                pthread_mutex_unlock(&mutex);
                sem_post(full);
            }
        }
        
    
  • 条件变量
    条件变量通常结合互斥锁来使用,作用是让线程在进入临界区且某条件不成立时,进入睡眠状态、释放当前锁,并在其他线程使得该条件成立时被唤醒、重新获得锁。

        int WaitForPredicate()
        {
            // lock mutex (means:lock access to the predicate)
            pthread_mutex_lock(&mtx);
    
            // we can safely check this, since no one else should be
            // changing it unless they have the mutex, which they don't
            // because we just locked it.
            
            //因为pthread_cond_wait返回0并不一定意味着predicate为true,也有可能是意外的signal、broadcast导致的,所以需要while再次判断。
            while (!predicate)  
            {
                // predicate not met, so begin waiting for notification
                // it has been changed *and* release access to change it
                // to anyone wanting to by unlatching the mutex, doing
                // both (start waiting and unlatching) atomically
                pthread_cond_wait(&cv,&mtx);
            }
    
            // we own the mutex here. further, we have assessed the
            //  predicate is true (thus how we broke the loop).
    
            // You *must* release the mutex before we leave.
            pthread_mutex_unlock(&mtx);
        }
    
  • barrier

    • 用忙等待和mutex实现
        int counter=0;
        int tread_count;
        pthread-mutext_t mutex;
        void* Tread_Work(){
            pthread_mutex_lock(&mutex);
            count++;
            pthread_mutex_unlock(&mutex);
            while(counter<tnread_count); //忙等待
        }
    
    
    • 用信号量实现
        int counter=0;
        int thread_count;
        sem_t coming_sem;   //init to 1
        sem_t blocked_sem;  //init to 0
    
        void* Thread_Work(){
            //当coming_sem为1时,线程能够进入临界区
            sem_wait(coming_sem);
            //是最后一个到达的线程
            if(counter==thread_count-1){
                //将counter和coming_sem恢复到初始状态
                counter=0;
                sem_post(coming_sem);
                //循环放行之前阻塞在blocked_sem上的线程
                for(int i=0;i<thread_count-1;i++){
                    sem_post(blocked_sem);
                }
            }else{
                //不是最后一个线程
                counter++;
                sem_post(coming_sem);   //让下一个线程能够进入临界区
                sem_wait(blocked_sem);  //本线程在这(barrier处)等待
            }
        }
    
    • 用条件变量实现
        int counter=0;
        int thread_count;
        pthread_mutex_t mutex;
        pthread_cond_t cond;
    
        void* Work_Thread(){
            thread_mute_lock(&mutex);
            counter++;
            //最后一个线程到达  条件成立
            if(counter==thread_count){
                counter=0;
                pthread_cond_broadcast(&cond);
            }else{
                //线程没全到  条件不成立
                while(pthread_cond_wait(&cond,&mutex)!=0);
            }
            pthread_mutex_unlock(&mutex);
        }
    

MapReduce

  • 对付大数据处理:分而治之
  • 将处理过程抽象为-Map和-Reduce两个部分
  • 上升到架构,隐藏并行计算的底层细节

  • 什么样的任务适用于并行计算?

    • 整个计算任务可以划分成没有依赖关系、可以独立进行的数据块或者子任务。
    • 不可分拆的计算任务或相互间有依赖关系的数据无法进行并行计算!
  • MapReduce模型
    在这里插入图片描述

  • 需要实现的接口

        map(String input_key, String input_value):
        // input_key: document name 
        // input_value: document contents 
        for each word w in input_value: 
            EmitIntermediate(w, "1"); 
    
        reduce(String output_key, Iterator intermediate_values): 
        // output_key: a word 
        // output_values: a list of counts 
        int result = 0; 
        for each v in intermediate_values: 
            result += ParseInt(v);
        Emit(output_key, result);
    

Hadoop

Hadoop是一个项目的总称,其中包含HDFS、MapReduce、HBase。
其中HDFS是Google File System (GFS)的开源实现。  
MapReduce是Google MapReduce的开源实现。
Hbase是Google BigTable的开源实现。
  • Hadoop的特点

    • 扩容能力 Scalable :能科考地存储和处理PB级别的数据
    • 成本低 Economical :可以用普通机器组成的服务器群来分发和处理数据,节点数可达数千个。
    • 高效率 Efficient :通过分发数据,hadoop可以在所有节点上并行地处理,速度非常快。
    • 可靠性 Reliable :hadoop能自动地维护数据的多份复制,在失败后能自动重新部署计算任务。
  • Hadoop的工作流程

    • Hadoop把client节点接收到的计算任务成为Job
    • JobTracker节点把client节点处的Job取过来,将其继续划分为两类task,分别为MapTask和ReduceTask,并且交由TaskTracker节点进行处理。
    • TaskTracker会执行一个具体的task,并且向JobTracker汇报进度信息。
    • JobTracker作为整个系统中的任务调度中心,会按照以下顺序进行Job的分配:还未分配过的任务,执行失败的任务,需要进行推测执行的任务。
  • HDFS
    HDFS(Hadoop Distributed File System
    )是 Hadoop应用中主要有用到的分布式文件系统,它为了可靠性,将数据块进行多块的复制,并且分布到整个服务器集群中,这样MapReduce就可以在其所在的服务器节点上直接拿到数据块并且进行处理。

    • 特性
      • 大容量
      • 高容错性
      • 高吞吐量
    • 数据模型
      • 文件
  • HBase
    HBase是一个分布式的、面向列的开源数据库,适合于海量非结构化数据存储。

    • Hbase表的特点

      • 大:一个表可以有上亿行、上百万列。
      • 面向列:面向列(族)进行存储。
      • 稀疏:对于为空的列,并不占用存储空间,因此表可以设计地非常稀疏。
    • Hbase的特点

      • 基于列式的高效存储
      • 强一致的数据访问
      • 高可靠
      • 高性能
      • 可伸缩,自动切分、迁移
      • Schema free
    • 为什么传统关系型数据库在大数据场景下不适用

      • 传统关系型数据库在高并发场景下,一般采用读写分离、分库分表的策略来降低单表的访问压力,但是会带来数据一致性难以保证的问题
      • 当面对海量数据时,同样只能采用分库分表的方式来拆分大表,这样才能保证局部数据查询时的效率,但是实现起来复杂、难以维护和迁移。不仅如此,当分表后规模依然很大时,按行存储的关系型数据库进行关联查询会产生非常大的笛卡尔积,时间和空间开销都非常高。
      • 为了高可用和可靠性,通常采用主备、主从、多主的方式进行数据备份存储,拓展性差,增加节点和节点宕机需要进行数据迁移。
    • Hbase的存储结构

      • HTable
        • 对于一张表,需要预先定义所有列族,而某一条具体的记录由<rowKey,column family,[column,]timestamp >:<value>的形式存储,其中的timestamp是为了区分一条记录的多个历史版本(即Hbase会存储一个记录的多个历史值)。
        • 一张表中的所有记录,会根据rowKey进行字典序排列存储。
      • HRegion
        • HTable在行方向上会被划分为多个HRegion,这是HBase中表的分布式存储的最小单位。当一个HRegion越来越大并达到阈值之后,就会被RegionServer给划分成两个更小的HRegion。
      • Store
        • HRegion是分布式存储的最小单位,但不是存储的最小单位。一个HRegion中有多个Store结构,这才是数据在DHFS上的最小存储单元。
        • 一个Store中包含一个memStore块和多个storeFile,storeFile以Hfile的格式存储在DHFS上。
    • HBase的逻辑结构

      • client:
        • 访问Hbase的入口,维护着Region位置的缓存信息。
      • zookeeper:
        • 维护集群中master的唯一存在。
        • 维护所有Region的寻址入口。
        • 实时监控regionServer的状态,向master通知节点上下线信息。
        • 存储Hbase的schema,包括有哪些table、table中有哪些列族。
      • master
        • 负责给regionServer分配Region。
        • 为regionServer集群做负载均衡。
        • 发现失效的regionServer时重新将其上的region分配到其他地方。
        • 负责DHFS上的垃圾回收。
        • 处理schema的更新请求
      • regionServer
        • 维护master分配的region。
        • 处理client对region的IO请求,这个过程不需要master参与。
        • 负责切分过大的region。

Spark

Spark基于map reduce 算法模式实现的分布式计算,拥有Hadoop MapReduce所具有的优点;但不同于Hadoop MapReduce的是Job中间输出和结果可以保存在内存中,从而不再需要读写HDFS

Spark立足于内存计算,相比Hadoop MapReduce,Spark在性能上要高100X倍,而且Spark提供了比Hadoop更上层的API,同样的算法在Spark中实现往往只有Hadoop的1/10或者1/100的长度。

  • 几个关键的API

    //创建spark环境
    JavaSparkContext ctx = new JavaSparkContext("yarn-standalone", "JavaWordCount",System.getenv("SPARK_HOME"), JavaSparkContext.jarOfClass(mysparktest.class));
    
    //读入数据
    JavaRDD<String> lines = ctx.textFile(args[1], 1);
    lines.cache();   //cache,暂时放在缓存中,一般用于哪些可能需要多次使用的RDD,据说这样会减少运行时间
    
    //collect方法,用于将RDD类型转化为java基本类型,如下
        //一条数据转换成一个字符串
        List<String> line = lines.collect();
        for(String val:line)
                System.out.println(val);
        //一条数据转换成一个键值对
        List<Tuple2<String, Integer>> output = sort.collect();
        for (Tuple2<?,?> tuple : output) {
            System.out.println(tuple._1 + ": " + tuple._2());
        }
    
    //创建filter 将RDD中的每一条数据进行过滤 将只会返回那些过滤结果为true的记录
    JavaRDD<String> contaninsE = lines.filter(new Function<String, Boolean>() {
            @Override
            public Boolean call(String s) throws Exception {
    
    
               return (s.contains("they"));
            }
        });
    
    //根据自定义的方法将一条RDD数据转换为多条RDD数据 如将一个句子拆分成若干个单词
    //FlatMapFunction的两个泛型 分别指的是用来拆分的数据类型和拆分之后的数据集合元素的类型
    JavaRDD<String> words = lines.flatMap(new FlatMapFunction<String, String>() {
        @Override
        public Iterable<String> call(String s) {
            String[] words=s.split(" ");
            return Arrays.asList(words);
        }
    });
    
    //map方法   将一条rdd数据映射为一个<key,value>键值对
    JavaPairRDD<String, Integer> ones = words.map(new PairFunction<String, String, Integer>() {
            @Override
            public Tuple2<String, Integer> call(String s) {
                return new Tuple2<String, Integer>(s, 1);
            }
        });
    
    //reduceByKey方法  将map产生的JavaParRDD进行处理,相同key的数据分为一组,按照reduce的模式进行规约
    JavaPairRDD<String, Integer> counts = ones.reduceByKey(new Function2<Integer, Integer, Integer>() {
        @Override
        public Integer call(Integer i1, Integer i2) {  //reduce阶段,key相同的value怎么处理的问题
            return i1 + i2;
        }
    });
    
    //reduce方法,所有RDD的数据,第一个元素和第二个元素传入call方法,输出的结果与第三个元素构成参数再次传入call 直到最后两个元素传入call
    JavaPairRDD<String, Integer> counts = ones.reduce(new Function2<Integer, Integer, Integer>() {
        @Override
        public Integer call(Integer i1, Integer i2) {  //reduce阶段
            return i1 + i2;
        }
    });
    
    //对RDD中的数据进行排序
    JavaPairRDD<String,Integer> sort = counts.sortByKey();
    
  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值