并行計算簡介(1)

研一上學期上了多核軟件設計,以及算法設計與分析的并行算法部分,其中算法的課程大作業是要使用MPIopenmp以及pthread實現大型稀疏矩陣的求解算法。下學期上了分布式與并行計算,主要了解了分布式計算的內容,學了MapReduceHadoopSpark以及了解了虛擬化等相關技術,動手搭建了Hadoop集群,OpenStack云平臺,算是對云計算有了初步的了解。

當前利用并行技術開發軟件是一個趨勢,無論是在移動的APP,桌面應用,還是在云服務應用領域,并行計算越來越得到開發者的關注。下面總結一下并行計算,也算是對這一段時間學習的總結,以下內容是作者學習中的總結,如果有錯誤請在評論區指出。

什么是并行計算


并行計算是相對于串行計算而言,比如一個矩陣相乘的例子,下面給出串行程序的代碼

void matrixMultiplication(int a[][SIZE], int b[][SIZE])
{
    int i,j,k;
    for(i = 0; i < c_row; i++) 
    {
        for(j = 0; j < c_col; j++) 
        {
            for(k = 0; k < a_col; k++) 
            {
                c[i][j] += a[i][k] * b[k][j];
            }
        }
    }
}

在上面的程序中,程序編譯運行之后以一個進程(注意區分進程線程這兩個概念)的方式是按照for循環迭代順序執行。那怎么并行矩陣相乘的代碼呢?這里需要使用高級語言級別的并行庫,常見的并行庫有opemppthreadMPICUDA等,這些并行庫一般都支持C/C++,程序員可以直接調用并行庫的函數而不需要實現底層的CPU多核調用。下面給出opemmp版本的矩陣相乘程序。

void matrixMultiplication(int a[][SIZE], int b[][SIZE])
{
    int i,j,k;
    #pragma omp parallel shared(a,b,c) private(i,j,k)  
    {  
         #pragma omp for schedule(dynamic)  
         for(i = 0; i < c_row; i++) 
         {
            for(j = 0; j < c_col; j++) 
            {
                for(k = 0; k < a_col; k++) 
                {
                    c[i][j] += a[i][k] * b[k][j];
                }
            }
        }
    }
}  

opemmp在沒有改動原本代碼的基礎上實現了矩陣相乘的并行化,實現的辦法僅僅是添加了兩條openmp編譯制導語句,在程序運行到并行代碼時,程序的主線程會啟動多線程把任務分成n份(n=CPU核心數),然后在多核心上同時計算,最后主線程得到結果。當然除了多線程,程序也可以啟動多進程進行并行計算,關于多進程,Linux下的fork()想必很多人都有了解,而mpich是目前很流行的多進程消息傳遞庫。并行化看起來很簡單不是么,但是,要設計高效能解決復雜問題的并行程序可不那么容易。

上面提到了多核,這里聊聊多核吧。

多核


多核,就是多核處理器,我們知道,目前的計算機的CPU一般是雙核的,有的甚至更多,下圖是我的電腦的處理器

CPU核心數

我的電腦是8個核心的,其實并行計算就是利用CPU的多核處理器的特點,把之前在單核心上串行的代碼并行化,從而縮短得到計算結果的時間。比如上文中提及的矩陣相乘的并行程序,就是把計算任務平分到每個處理器,然后把結果返回,這樣相比串行執行時間上要高效。那怎么判斷并行程序的效率呢?那就是加速比了,下文會介紹加速比。

關于多核,需要介紹摩爾定律(Moore’s Law)

摩爾定律由英特爾聯合創始人之一的Gordon Moore于1965提出,摩爾定律的內容為:

The number of transistors per square inch on integrated circuits had doubled
every year since the integrated circuit was invented.

其中文意思為

當價格不變時,集成電路上每平方英寸可容納的晶體管的數目,約每隔一年便會增加一倍,性能也將提升一倍。

這意味著沒有摩爾定律,就沒有如今廉價的處理器。而隨著集成電路上的晶體管數據量越來越多,功耗的增加以及過熱問題,使得在集成電路上增加更多的晶體管變得更加困難,摩爾定律所預言的指數增長必定放緩。因此,摩爾定律失效。

當前和未來五年,微處理器技術朝著多核方向發展,充分利用摩爾定律帶來的芯片面積,放置多個微處理器內核,以及采用更加先進的技術降低功耗。

當然,多核并行計算不僅僅可以使用CPU,而且還可以使用GPU(圖形處理器),一個GPU有多大上千個核心,可以同時運行上千個線程。那怎么利用GPU做并行計算呢?可以使用英偉達的CUDA庫,不過前提是計算機安裝了英偉達的顯卡。

并行計算的分類


雖然并行計算可分為時間上的并行和空間上的并行, 時間上的并行是指流水線技術,而空間上的并行則是指多核心并發的執行計算。但是目前并行計算主要研究空間上的并行問題。

按照Flynn分類(1966年)的標準,根據指令流和數據流的多倍性(機器瓶頸部件所能支持最多指令條數或數據個數)可以分成以下幾類

  • 單指令流單數據流機SISD,即傳統的單處理機
  • 單指令流多數據流機SIMD
  • 多指令流單數據流機MISD
  • 多指令流多數據流機MIMD

下圖是并行計算四種分類的計算機體系結構


并行計算的分類

既然有了并行計算,那少不了并行計算機,大家比較熟悉的并行計算機應該是天河一號,天河二號以及最近大家熟知的神威·太湖之光超級計算機。超級計算機涉及CPU,內存,存儲,緩存,通信,I/O,網絡,制冷等多個領域的知識,由于本文主要介紹并行計算,因此超級計算機的內容不在本文中詳述。

并行程序的評價指標


評價并行計算程序效率,不單單從并行程序的執行時間來考慮,而是從與串行程序的對比來去評價。最常用的指標是加速比(speedup

加速比=串行執行時間/并行執行時間

還有效率

效率=加速比/處理器個數

以及成本

成本=并行執行時間×所用處理器的數目

舉個例子:用N個處理器計算N個數的和(N為2的整數次冪)

  • 串行計算需要O(N)的時間
  • 并行方法:每個處理器獲得一個數,兩個處理器之一將其疊加,遞歸上述步驟。需要O(logN)的時間
  • 加速比S = O(N/logN)
  • 成本C = O(N*logN)

并行計算模型


并行計算模型是并行算法設計與分析的模型,不涉及并行算法實現的程序設計模型和并行算法執行的程序執行模型,如PRAM模型(SIMD模型)。

并行程序設計模型側重于并行算法如何使用某種程序設計語言正確地編程實現,如MPIOpenMPpthread

并行程序執行模型側重于并行算法如何在具體的并行機上運行并優化性能,如指令級并行程序執行模型ILPPEM

并行計算模型是算法設計者與體系結構之間的橋梁,并行計算模型屏蔽不同并行機的具體差異,只抽取若干能反映計算特性模型參數,按照模型所定義的計算行為構造成本函數,以此分析時空復雜度。機器參數(如CPU性能參數、存儲器參數、通信網絡參數)、計算行為(同步或異步)和成本函數(機器參數構成自變量)構成并行計算模型的三要素

寫著寫著,好像越寫越底層了,下面說說并行計算與大數據吧。

并行計算與大數據


大數據不僅給我們帶來了大量的數據,而且還帶來了計算和查詢的麻煩。在數據挖掘領域,對大數據集進行logistic回歸,如果不使用并行計算,可能需要好幾個月,如果使用分布式的計算平臺,計算結果可能只需要幾個小時。而在深度學習,人工智能,科學計算領域同樣也需要使用并行計算,并行計算縮短了得到結果的時間。

在大數據環境下的并行計算,Hadoop MapReduceSpark大家一定不會陌生,Hadoop MapReduce把任務分成MapReduce兩個部分,把大型任務切分成子任務,而Spark提供一種新的存儲方式——resilient distributed datasets (RDDs),彈性分布式數據集。RDD是一個容錯的、并行的數據結構。RDD只讀,可分區,這個數據集的全部或部分可以緩存在內存中,在多次計算間重用。所謂彈性,是指在內存不夠時可以與磁盤進行交互。這涉及到RDD的另一個特性:內存計算,就是將數據集存到內存中。
同時為了解決內存容量限制的問題,Spark為我們提供了最大的自由度,將所有數據均可以由用戶進行cache的設置。RDD還提供了一組豐富的操作來操作這些數據,如mapflatMapfilterreducereduceBygroupBy等。

下面以一個統計網頁中詞頻的方式來介紹Hadoop MapReduceSpark,數據存儲在MongoDB中,下面是數據的樣例。

{ "doc" : "good good day", "url" : "url_1" }
{ "doc" : "hello world good world", "url" : "url_2" }

Hadoop MapReduce


下面給出MapReduce的代碼

  • Map
public void map(Object key, BSONObject value, Context context)
                 throws IOException, InterruptedException {
    ArrayList<String> tags = (ArrayList<String>) value.get("tag");
    for (int i = 0; i < tags.size(); i++) {
        tagSet.add(tags.get(i));
    }
    String url = value.get("url").toString();
    String doc = value.get("summary").toString().replaceAll("\\p{Punct}|\\d", "")
                       .replaceAll("\r\n", " ").replace("\r", " ").replace("\n", " ").toLowerCase();
    StringTokenizer itr = new StringTokenizer(doc);
    HashMap<String, Integer> word_count = new HashMap<String, Integer>();
    while (itr.hasMoreTokens()) {
        String kk = itr.nextToken();
        if (tagSet.contains(kk)) {
            if (word_count.containsKey(kk)) {
                word_count.put(kk, word_count.get(kk)+1);
            } else {
                word_count.put(kk, 1);
            }
        }
    }
    // get word and counts (url, counts)
    for (Map.Entry<String, Integer> entry : word_count.entrySet())  {
        BasicBSONObject counts = new BasicBSONObject();
        counts.put(url, entry.getValue());
        Text myword = new Text(entry.getKey());
        context.write(myword, new BSONWritable(counts));
    }
}
  • Reduce
public void reduce(Text key, Iterable<BSONWritable> values, Context context)  
                              throws IOException, InterruptedException {
    HashMap<String, Integer> mymap = new HashMap<String, Integer>();
    BasicBSONObject result = new BasicBSONObject();
    for (BSONWritable val : values) {
        @SuppressWarnings("unchecked")
        HashMap<String, Integer> temp = (HashMap<String, Integer>) val.getDoc().toMap();
        for (Map.Entry<String, Integer> entry : temp.entrySet()) {
            mymap.put(entry.getKey(), entry.getValue());
        }
    }
    result.putAll(mymap);
    try {
        context.write(new Text(key.toString()), new BSONWritable(result));
    } catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
        System.out.println(" \n\n ERROR HERE BulkWriteException \n\n");
    } 
}

Hadoop MapReduce程序必須重新寫mapreduce類,在程序員自定義的mapreduce類添加計算邏輯。但是Spark卻沒有這種限制。

Spark


Spark代碼主要是對RDD進行操作,下面給出代碼

# 定義統計單詞和詞頻的函數
def f(record):
    """"""
    table = string.maketrans("", "")
    raw_summary = record[1]['summary']
    temp = raw_summary.encode("utf-8").translate(table, string.punctuation)
    temp_str = re.sub("[+——!,。?、~@#¥%……&*()]+".decode("utf8"),
                               "".decode("utf8"), temp).replace("\r\n", " ").replace("\n", " ")
    summary = re.sub(r'([\d]+)', ' ', temp_str).lower()
    url = record[1]['url']
    _temp = dict(collections.Counter(summary.split()))
    result = [(key,{url:value}) for key,value in _temp.items()]
    return result

# reduce操作函數
def reduce_values(record):
    """"""
    result = {}
    for element in record[1]:
        key, value = element.items()[0]
        result[key] = result.get(key, 0) + value
    return [(record[0], result)]

# 從MongoDB中讀取數據
rdd = sc.mongoPairRDD(
       "mongodb://localhost/testmr.test_in")
newrdd = rdd.flatMap(f) # map
sortrdd = newrdd.sortByKey() # sort
resultrdd = sortrdd.groupByKey()
                   .flatMap(reduce_values) # reduce
resultrdd.saveToMongoDB('
             mongodb://localhost:27017/testmr.test_out')

Spark中,雖然有mapreduce這兩個RDD的操作接口,但卻不是和HadoopMapReduce一一對應。在Spark中,可以通過兩個RDDflatMap操作來實現Hadoop MapReduce


Spark的速度比Hadoop更快。最后Hadoop ReduceSpark的結果(以下是樣例的結果)為

> db.stackout.find().limit(4)
{ "_id" : "world", "data" : [  {  "url_2" : 2 },  {  "url_3" : 1 },  {  "url_4" : 1 } ] }
{ "_id" : "good",  "data" : [  {  "url_1" : 2 },  {  "url_2" : 1 },  {  "url_3" : 2 } ] }
{ "_id" : "day",   "data" : [  {  "url_1" : 1 } ] }
{ "_id" : "hello",  "data" : [  {  "url_2" : 1 },  {  "url_3" : 2 },  {  "url_4" : 2 } ] }

延伸閱讀和推薦書籍


要進一步了解并行計算及并行程序的開發,大家除了需要具備基本的編程能力之外,還需要了解計算機體系結構及操作系統,特別是CPU,cache,內存,多進程,多線程,堆,棧等概念。就比如cache優化的問題吧,如果在實現矩陣相乘并行化程序時考慮cache優化的問題,那么運行的時間又會縮短,加速比又會提高。另外還需要使用一些常用的并行計算的庫,不過這些庫幾乎都比較容易上手使用。不過我覺得在開發并行程序時最重要的是并行算法的設計——把串行問題并行化,雖然矩陣相乘的問題很容易并行化,但是現實世界中的很多計算問題都很復雜,如何將串行問題并行化這也考驗著一個開發者的算法基礎和數學能力。

下面給出課堂上老師推薦的書,我目前在看《Hadoop 權威指南》。其實還有一本叫做《深入理解計算機系統》的書沒有列出,推薦給有經驗的程序員閱讀。

Distributed and Cloud Computing
計算機體系結構
virtual machines
并行程序設計導論
Hadoop權威指南

總結


并行計算其實是一個很大的概念,今天只是蜻蜓點水,接下來還要好好寫文章總結并行計算領域的技術知識。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容