1. 程式人生 > >CUDA學習二:CUDA C並行程式設計

CUDA學習二:CUDA C並行程式設計

文章連結: http://blog.csdn.net/u013108511/article/details/77096661
郵箱:[email protected]
  
  上一個部落格中我們瞭解到了在函式前面加__ global_ _修飾符來定義核函式並通過一個尖括號來呼叫就可以在GPU上執行這個函式的功能。方法簡單但是低效,因為經過優化的圖形處理器可以並行執行數百次的運算,然而在例項中只調用一個核函式且是序列執行。下面來了解運算的並行性。

##1、向量求和運算
  向量的求和運算,這裡對運算的模型進行簡單化,就理解成兩個陣列對應下標的資料元素相加,並將結果儲存到第三個陣列中。

###1.1、基於CPU的向量求和

  下面用傳統的標準的C方法實現兩個陣列相加的小程式.

#include <stdio.h>
#define N 10

void add(int *a, int *b, int *c) {
    int tid = 0;//第0個CPU,因此索引從0開始

    while(tid < N) {
        c[tid] = a[tid] + b[tid];
        tid ++;//單執行緒程式設計,由於只有一個CPU,因此每次遞增一
    }
}

int main( void ) {

    int a[N],b[N],c[N];

    //在CPU上位陣列a和b賦值
    for(int i = 0; i < N; i++) {
        a[i] = -i;
        b[i] = i * i;
    }

    add(a,b,c);

    //顯示結果
    for(int i = 0; i < N; i++) {
        printf("%d + %d = %d\n", a[i], b[i], c[i]);
    }

    return 0;
}

更簡單的相加函式可以通過for迴圈實現,同時如果想使用兩個CPU並行執行的話,策略是一個CPU執行下標為奇數的加法運算,一個CPU執行下標為偶數的加法運算。程式碼實現如下:

//第一個CPU核心
void add (int *a, int *b, int *c){
	int tid = 0;
	while(tid < N) {
		c[tid] = a[tid] + b[tid];
		tid += 2;
	}
}

//第二個CPU核心
void add (int *a, int *b, int *c){
	int tid = 1;
	while(tid < N) {
		c[tid] = a[tid] + b[tid];
		tid += 2;
	}
}

  然而即便如此,CPU按以上執行仍然需要增加更多的程式碼,如多執行緒的定義和控制等,但通常執行緒的實際執行情況並非如此。

##1.2、基於GPU的向量求和
  實現加法運算只需要將add()編寫為裝置函式即可。

#include "../common/book.h"

#define N 65500

__global__ void add(int *a, int *b, int *c) {
    int tid = blockIdx.x;   
    if(tid < N)
        c[tid] = a[tid] + b[tid];
}

int main( void ) {
    int a[N], b[N], c[N];
    int *dev_a, *dev_b, *dev_c;
    
    //在GPU上分配記憶體
    HANDLE_ERROR(cudaMalloc ((void **)&dev_a, N*sizeof(int)));
    HANDLE_ERROR(cudaMalloc ((void **)&dev_b, N*sizeof(int)));
    HANDLE_ERROR(cudaMalloc ((void **)&dev_c, N*sizeof(int)));

    //CPU上位陣列a和b賦值
    for(int i = 0; i < N; i++) {
        a[i] = -i;
        b[i] =  i* 2;
    }

    //將a和b陣列複製到GPU中
    //注意:至於為什麼不採用直接在裝置上賦值以獲得更快的執行速度,這裡只是假設,後面會繼續討論
    HANDLE_ERROR(cudaMemcpy (dev_a, a, N * sizeof(int), cudaMemcpyHostToDevice));
    HANDLE_ERROR(cudaMemcpy (dev_b, b, N * sizeof(int), cudaMemcpyHostToDevice));

    add<<<N, 1>>> (dev_a, dev_b, dev_c);

    //將陣列c從GPU拷貝到CPU
    HANDLE_ERROR(cudaMemcpy (c, dev_c, N *sizeof(int), cudaMemcpyDeviceToHost));

    //顯示結果
    for(int i = 0; i < N; i++) {
        printf("%d + %d = %d\n", a[i], b[i], c[i]);
    }

    //釋放在GPU上分配的記憶體
    cudaFree(dev_a);
    cudaFree(dev_b);
    cudaFree(dev_c);

    return 0;
}

GPU程式設計有一些通用的模式,初學CUDA程式設計的時候熟悉模式可以快速上手程式設計,模式有一下幾點:
  1、GPU上的變數使用前都要為其分配記憶體,本例子中呼叫cudaMalloc()函式為三個陣列分配記憶體,兩個陣列包含輸入值,一個數組包含結果;
  2、為了避免記憶體洩漏,使用結束後通過cudeFree()函式釋放記憶體;
  3、通過cudaMemcpy()進行主機和記憶體之間資料的拷貝,通過指定引數cudaMemcpyHostToDevice將資料從主機拷貝到裝置中,通過制定引數cudaMemcpyDeviceToHost將資料從裝置拷貝到主機中。
  4、通過尖括號語法,在主機程式碼的main()函式中執行add()中的裝置程式碼。
  5、而編寫一個在裝置上執行的函式,只需要在函式的前面宣告一個修飾符__ global_ _
注意:至於為什麼不採用直接在裝置上賦值以獲得更快的執行速度,這裡只是假設,後面會繼續討論

上一篇部落格出現一下形式來啟動核函式:
  kernel<<<1,1>>>(param1,param2,…)
本例項中出現的核函式:
  add<<< N,1>>>(dev_a, dev_b, dev_c)

注意尖括號中的兩個引數,其中第一個引數表示裝置在執行核函式的時候使用的並行執行緒塊的數量。這個例項中指定這個引數為N。

###獲取程式碼當前執行的執行緒塊:變數blockIdx.x
  blockIdx是一個內建的變數,所以不需要定義,CUDA執行時已經預先定義,變數中包含的值就是當前執行裝置程式碼的執行緒塊的索引。使用blockIdx.x是因為CUDA支援二維的執行緒塊的陣列,使用二維的索引會帶來很大的便利。N個執行緒塊組成一個執行緒格(Grid),本例中一個一維的執行緒格包含N個執行緒塊,每個執行緒塊的blockIdx.x都不相同,第一個執行緒塊的blockIdx.x為0.最後一個執行緒塊的blockIdx.x為N-1。
  在啟動執行緒塊陣列時,陣列的每一維的最大數量都不能超過65535,這是一種硬體的限制,如果啟動的硬體的執行緒塊超過了這個限制,程式將會執行失敗。

#2、一個有趣的例子:繪製Julia集的曲線
  Julia集曲線是滿足某個複數計算函式的所有點構成的邊界。Julia集的基本演算法是,通過一個簡單的迭代等式對複平面中的點進行求值,如果早計算某一點時,迭代式的結果是發散的那麼這個點就不屬於Julia集合,相反如果迭代等式中計算的一系列的值位於某個邊界範圍以內,那麼這個點就是Julia集。
  具體的公式如下:
                   這裡寫圖片描述

##2.1、CPU的程式設計實現

#include "stdio.h"
#include "../common/cpu_bitmap.h"

//定義了畫素點長和寬的大小
#define DIM 1000

//複數的資料結構的定義以及對這個資料結構可以執行的操作。
struct cuComplex {
    float r;
    float i;
    cuComplex(float a, float b) {
        r = a;
        i = b;
    }
	//計算向量的長度的平方
    float magnitude2(void) {
        return r * r + i * i;
    }
	//乘法運算子過載
    cuComplex operator *(const cuComplex& a) {
        return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
    }
	//加法的運演算法的過載
    cuComplex operator +(const cuComplex& a) {
        return cuComplex(r+a.r, i+a.i);
    }
};

//判斷複數x+yi是否屬於Julia集,執行上面的操作
int julia(int x, int y) {
    const float scale = 1.5;

    float jx = scale * (float)(DIM/2 - x)/(DIM/2);

    float jy = scale * (float)(DIM/2 - y)/(DIM/2);

    cuComplex c(-0.8, 0.156);
    cuComplex a(jx, jy);

    int i = 0;
    for(i = 0; i < 200; i++) {
        a = a * a + c;
        if(a.magnitude2() > 1000)
            return 0;
    }
    return 1;
}

//核函式,對於設定的1000*1000畫素的區域,每個點是否是Julia集進行判斷
void kernel( unsigned char *ptr) {
    for(int y=0; y<DIM; y++) {
        for(int x=0; x<DIM; x++) {

            int offset = x + y * DIM;

            int juliaValue = julia(x, y);

            ptr[offset*4 + 0] = 255 *juliaValue;
            ptr[offset*4 + 1] = 0;
            ptr[offset*4 + 2] = 0;
            ptr[offset*4 + 3] = 255;
        }
    }
}

//主函式,由於是面向CPU的程式設計,程式全部在CPU上執行
int main( void ) {

    CPUBitmap bitmap( DIM, DIM );
    
    unsigned char *ptr = bitmap.get_ptr();

    kernel( ptr );
    
    bitmap.display_and_exit();

    return 0;
}

注意:由於一些庫和工具的引入,程式的編輯可能出現編譯錯誤的情況,CPU程式碼編譯指令如下:

g++ CPUJulia.c -lGL -lGLU -lglut -o CPUJulia

之所以用g++,應為長鬚中有一些c不支援的庫,g++編譯沒有問題。

##2.2、GPU的程式設計實現

#include "cuda.h"
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include "../common/book.h"
#include "../common/cpu_bitmap.h"

#define DIM 1000

//複數的資料結構,__device__表示在裝置上執行的程式
//同時要注意的是:在裝置上執行的額程式在執行函式呼叫的時候也只能呼叫在裝置上執行的程式部分,而不能呼叫在主機上執行的函式部分
struct cuComplex {

    float r;
    float i;
    __device__ cuComplex(float a, float b) : r(a), i(b){}
    __device__ float magnitude2(void) {
        return r * r + i * i;
    }

    __device__ cuComplex operator *(const cuComplex& a) {
        return cuComplex(r*a.r - i*a.i, i*a.r + r*a.i);
    }

    __device__ cuComplex operator +(const cuComplex& a) {
        return cuComplex(r+a.r, i+a.i);
    }
};

__device__ int julia(int x, int y) {
    const float scale = 1.5;

    float jx = scale * (float)(DIM/2 - x)/(DIM/2);

    float jy = scale * (float)(DIM/2 - y)/(DIM/2);

    cuComplex c(-0.8, 0.156);
    cuComplex a(jx, jy);

    int i = 0;
    for(i = 0; i < 200; i++) {
        a = a * a + c;
        if(a.magnitude2() > 1000)
            return 0;
    }
    return 1;
}

__global__ void kernel( unsigned char *ptr) {

    //將threadIdx/BlockIdx對映到畫素位置
    int x = blockIdx.x;
    int y = blockIdx.y;
    int offset = x + y * gridDim.x;
    
    //現在計算這個位置上的俄值
    int juliaValue = julia(x, y);
    ptr[offset*4 + 0] = 255 *juliaValue;
    ptr[offset*4 + 1] = 0;
    ptr[offset*4 + 2] = 0;
    ptr[offset*4 + 3] = 255;
}
struct DataBlock {  
    unsigned char   *dev_bitmap;  
}; 

int main( void ) {  
    DataBlock   data;  
    CPUBitmap bitmap( DIM, DIM, &data );  
    unsigned char    *dev_bitmap;  
  
    HANDLE_ERROR( cudaMalloc( (void**)&dev_bitmap, bitmap.image_size() ) );  
    data.dev_bitmap = dev_bitmap;  
  
    dim3    grid(DIM,DIM);  
    kernel<<<grid,1>>>( dev_bitmap );  
  
    HANDLE_ERROR( cudaMemcpy( bitmap.get_ptr(), dev_bitmap,  
                              bitmap.image_size(),  
                              cudaMemcpyDeviceToHost ) );  
                                
    HANDLE_ERROR( cudaFree( dev_bitmap ) );  
                                
    bitmap.display_and_exit();  
}  

注意:由於一些庫和工具的引入,程式的編輯可能出現編譯錯誤的情況,GPU程式碼編譯指令如下:

nvcc GPUJulia.c -lGL -lGLU -lglut -o GPUJulia

同時還要注意,CPU高效能程式設計CUDA實戰中的程式碼編譯無法通過,要在cuComplex的建構函式前面加上_ _ device_ _

  對於上面的程式,前面對功能的理解完成之後,我們將學習重點放在GPU的並行程式設計的實現上面,上面的例子主要用到了GPU並行程式設計的一下知識點,逐漸的積累過程來加深對GPU並行程式設計的認識:
 1、整個程式執行的總的思路:main()函式通過一個工具庫建立一個DIMDIM大小的點陣圖影象。
 2、對於1000
1000的點陣圖影象,每個畫素點代表一個對應的複數的值,每個值是否屬於Julia集使我們要在裝置上進行判斷的,也是我們程式的核心部分;
 3、程式中指定了多個執行緒塊來執行函式Kennel,由於每個畫素點的計算和其他的點的計算相互的獨立,因此每個計算的點都執行該函式的副本,這是對於DIM*DIM個點,需要二維的執行緒索引,這時就有:

                dim3 grid(DIM,DIM)
這是一個3維的陣列,但是當只用兩個值來初始化dim3的時候,第三個維度會自動的初始化為1。NVIDIA將來可能會支援三維的索引,目前只能這樣做來滿足API的要求。
 4、kernel函式被宣告為一個_ global 的函式,這樣這個函式可以再主機上呼叫並且在裝置上執行。
 5、其他的裝置上執行的函式與kernel不同的是:都**宣告為一個
device_ 函式,這表示程式碼在裝置上執行,另外與 _global _不同的是聲明瞭 device __ 的函式只能從其他的 _device _或者 _global __ 函式呼叫而無法通過主機函式的呼叫。**

##2.3、顯示結果

這裡寫圖片描述

#3、總結
  通過本節,一個更復雜一點的例子,更深入的瞭解並行程式設計的實質。並行程式設計注意一下兩點:
(1)注意套用CUDA程式設計的模式,這樣可以將主要的注意力集中在並行執行緒的設計以及演算法的實現上來;
(2)注意程序的設定,加快程式理論執行的速率和效率。

相關推薦

CUDA學習CUDA C並行程式設計

文章連結: http://blog.csdn.net/u013108511/article/details/77096661 郵箱:[email protected]      上一個部落格中我們瞭解到了在函式前面加__ global_ _修飾符來定義

CUDA學習CUDA C簡介

#1、一個程式來了解CUDA C #include <stdio.h> #include "commom/book.h" //__global__表示函式在裝置而非主機上執行,add函式由編譯裝置程式碼的編譯器編譯 __global__ void

CUDA學習筆記CUDA Toolkit安裝與專案設定

0 環境 Win7 32bit VS2013 CUDA Toolkit 6.5 1 下載 2 安裝 只有一個安裝檔案,執行後首先會檢查是否安裝有支援cuda的NVIDIA顯示卡,然後自定義安裝,我用不到3D相關的就都沒選,預設路徑太長,改

C語言學習VS的使用

一、生成和重新生成“生成”的時候只對你改動過的檔案重新生成沒有改動過的檔案不會重新生成;“重新生成”是對所有的檔案都重新生成。以cpp為例當你只改動某些.cpp之類的檔案的時候可以用生成省了編譯沒有改動的那些些檔案的時間;但是改動了某些.h之類的檔案最好用重新生成,因為有可能能有些檔案包含.h檔案也需要重新編

CUDA學習日誌開發環境配置和學習資源

接觸CUDA的時間並不長,最開始是在cuda-convnet的程式碼中接觸CUDA程式碼,當時確實看的比較痛苦。最近得空,在圖書館借了本《GPU高效能程式設計 CUDA實戰》來看看,同時也整理一些部落

C++並行程式設計(): 利用C++標準庫實現Semaphore訊號量

    在上一節中,我們使用C++11標準庫中的提供的條件變數以及互斥變數封裝實現了兩個仿Windows核心事件類:ManualResetEvent和AutoResetEvent。在這一節中,我們將繼續使用標準庫中提供的類來實現高仿訊號量Semaphore。而後文的程式碼中,

WPF學習TextBlock和Label的區別

padding eight 前景 繼承 man ont blog led use TextBlock和Label都是用來顯示少量數據的。好多文章對Label存在的描述都是它允許使用"快速獲取"。"快速獲取"就是允許你用Alt加上其它的按鍵快速和UI界面的某個控件交互,比如你

學習筆記google c++ 編程風格指南

put rtu 操作符重載 同名 vmm foo 靜態數據成員 友元類 for 目錄:一、頭文件.................................................二、作用域...................................

python django學習 static文件處理與線上部署測試

image 運行 color 文件處理 收集 scrip 線上部署 ges sta static文件相關操作涉及:a. 文件位置與訪問路徑映射b. setting.py與static相關配置 STATIC_URLSTATIC_ROOTSTATICFILES_DIRS

Scala系統學習()Scala開發環境安裝配置

www 執行 posit 令行 完成後 version 繼續 environ ava Scala可以安裝在任何基於UNIX/Linux或基於Windows的系統上。在您的機器上開始安裝Scala之前,必須在計算機上安裝Java 1.8或更高版本。 下面請按照以下步驟安裝S

FFmpeg 結構體學習() AVStream 分析

rem hid version tin avd none internal hunk terms 在上文FFmpeg 結構體學習(一): AVFormatContext 分析我們學習了AVFormatContext結構體的相關內容。本文,我們將講述一下AVStream。 A

統計學習1.感知機

使用 itl cas erp 如果 所有 存儲 解釋 line 全文引用自《統計學習方法》(李航) 感知機(perceptron) 最早由Rosenblatt於1957年提出,是一種較為簡單的二分類的線性分類模型,其輸入為實例的特征向量,輸出為實例的類別,類別取值為+1

Android JNI 學習()JNI 設計概述

本章我們重點說明以下JNI設計的問題,本章中提到的大多數設計問題都與native方法有關。至於呼叫相關的API的設計,我們會在後面進行介紹。 一、JNI介面函式和指標 native 程式碼通過呼叫JNI函式來訪問Java VM功能。JNI函式可通過介面指標獲得。介面指標是指向指標的指標。該指標指向一個指標

課上補做C語言程式設計實現ls命令

課上補做:用C語言程式設計實現ls命令 一、有關ls ls :用來列印當前目錄或者制定目錄的清單,顯示出檔案的一些資訊等。 ls -l:列出長資料串,包括檔案的屬性和許可權等資料 ls -R:連同子目錄一同顯示出來,也就所說該目錄下所有檔案都會顯示出來 ls -a:可以將目錄下的全部檔案

C語言學習書籍推薦《C語言程式設計 現代方法(第2版)》下載

下載地址:點我 C語言仍然是計算機領域的通用語言之一,但現在的C語言已經和當初的時候大不相同了。本書主要的一個目的就是通過一種“現代方法”來介紹C語言,書中強調標準C,強調軟體工程,不再強調“手工優化”。這一版中緊密結合了C99標準,並與C89標準進行對照,補充了C99中的**新特性。本書分為C語言的基

es6學習變數的解構賦值

變數的解構賦值: 陣列的解構賦值: let [a,b,c] = [1,2,3]; // let [a,b,c] = [,123,]; // let [a=111,b,c] = [,123,]; console.log(a,b,c); // let [a,b,c] = [1,2,3]

node學習執行方式

命令列方式REPL js不是隻能執行在瀏覽器中的 REPL read-eval-print-loop 讀取程式碼-執行-列印結果-迴圈這個過程 在REPL環境中,_表示最後一次執行結果; .exit 可以退出REPL環境 執行檔案方式 使用terminal外掛可以在當前位置開

u-boot學習()u-boot簡要分析

(一) 以u-boot-1.1.6為例分析目錄結構如下: 1、平臺相關的或開發板相關的目錄:board、cpu、lib_i386類似 2、通用函式的目錄:include、lib_generic、common 3、通用的裝置驅動程式:disk、drivers、dtt、fs、nand_s

Python基礎學習列表,字典,深拷貝與淺拷貝

④使用pop()方法刪除元素:pop方法用於移出列表中的一個元素(預設是最後一個元素),可以指定元素索引,並且返回該元素的值。     使用del語句刪除元素:如果知道要刪除的元素在列表中的位置,可使用del語句刪除元素,元素一旦被刪除之後就再也無法訪問。  

cuda學習筆記五 cuda stream及 unified memory使用問題

      cuda通過多個stream可以降低host到Device的資料傳輸延時,這個沒問題。但是通過stream傳輸就需要通過cudaHostAlloc等重新分配記憶體,那麼這時候就有一個問題,就是這個記憶體需要重新賦值,問題就在於很有可能這段重新賦值的時間會超出