CUDA个人入坟笔记

CUDA是建立在NVIDIA的GPUs上的一个通用并行计算平台和编程模型,基于CUDA编程可以利用GPUs的并行计算引擎来更加高效地解决比较复杂的计算难题。近年来,GPU最成功的一个应用就是深度学习领域,基于GPU的并行计算已经成为训练深度学习模型的标配。目前,最新的CUDA版本为CUDA 10

并行计算

首先我们先谈一谈串行计算和并行计算。我们知道,高性能计算的关键是利用多核处理器进行并行计算。

当我们求解一个计算机程序任务时,我们很自然的想法就是将该任务分解成一系列小任务,把这些小任务一一完成。在串行计算时,我们的想法就是让我们的处理器每次处理一个计算任务,处理完一个计算任务后再计算下一个任务,直到所有小任务都完成了,那么这个大的程序任务也就完成了。如下图所示:

CUDA个人入坟笔记_第1张图片

但是串行计算的缺点非常明显,如果我们拥有多核处理器,我们可以利用多核处理器同时处理多个任务时,而且这些小任务并没有关联关系,那我们为什么还要使用串行编程呢?为了进一步加快大任务的计算速度,我们可以把一些独立的模块分配到不同的处理器上进行同时计算(这就是并行),最后再将这些结果进行整合,完成一次任务计算。下图就是将一个大的计算任务分解为小任务,然后将独立的小任务分配到不同处理器进行并行计算,最后再通过串行程序把结果汇总完成这次的总的计算任务。

CUDA个人入坟笔记_第2张图片

所以,一个程序可不可以进行并行计算,关键就在于我们要分析出该程序可以拆分出哪几个执行模块,这些执行模块哪些是独立的,哪些又是强依赖强耦合的,独立的模块我们可以试着设计并行计算,充分利用多核处理器的优势进一步加速我们的计算任务,强耦合模块我们就使用串行编程,利用串行+并行的编程思路完成一次高性能计算。

CPU和GPU

首先CPU是专为顺序串行处理而优化的几个核心组成。而GPU则由数以千计的更小、更高效的核心组成,这些核心专门为同时处理多任务而设计,可高效地处理并行任务。总而言之,CPU遵循的是冯诺依曼架构,其核心就是:存储程序,顺序执行。但你也可以看出在这个结构图中,负责计算的绿色区域占的面积似乎太小了,而橙色区域的缓存Cache和黄色区域的控制单元占据了大量空间。

结构决定性质,CPU需要很强的通用性来处理各种不同的数据类型,比如整型、浮点数等,同时它又必须擅长处理逻辑判断所导致的大量分支跳转和中断处理,因此CPU的架构中需要大量的空间去放置存储单元(橙色部分)和控制单元(黄色部分),相比之下计算单元(绿色部分)只占据了很小的一部分,所以它在大规模并行计算能力上极受限制,而更擅长于逻辑控制。

CUDA个人入坟笔记_第3张图片

反观GPU,为什么GPU特别擅长处理图像数据呢?这是因为图像上的每一个像素点都有被处理的需要,而且每个像素点处理的过程和方式都十分相似,所以GPU需要很高的并行计算能力。虽然他的每个核心的计算能力不算强,但他胜在核心非常多,可以同时处理多个计算任务,在并行计算的支持上做得很好。

但GPU无法单独工作,必须由CPU进行控制调用才能工作。CPU可单独作用,处理复杂的逻辑运算和不同的数据类型,但当需要大量的处理类型统一的数据时,则可调用GPU进行并行计算。

CUDA个人入坟笔记_第4张图片

GPU和CPU的不同硬件特点决定了他们的应用场景,CPU是计算机的运算和控制的核心,GPU主要用作图形图像处理。图像在计算机呈现的形式就是矩阵,我们对图像的处理其实就是操作各种矩阵进行计算,而很多矩阵的运算其实可以做并行化,这使得图像处理可以做得很快,因此GPU在图形图像领域也有了大展拳脚的机会。

在处理简单计算任务时呈现出“人多力量大”的优势,这就是并行计算的魅力。

但是GPU并不是一个独立运行的计算平台,而需要与CPU协同工作,可以看成是CPU的协处理器,因此当我们在说GPU并行计算时,其实是指的基于CPU+GPU的异构计算架构。在异构计算架构中,GPU与CPU通过PCIe总线连接在一起来协同工作,CPU所在位置称为为主机端(host),而GPU所在位置称为设备端(device)

CUDA个人入坟笔记_第5张图片

CUDA是NVIDIA公司所开发的GPU编程模型,它提供了GPU编程的简易接口,基于CUDA编程可以构建基于GPU计算的应用程序。CUDA提供了对其它编程语言的支持,如C/C++,Python,Fortran等语言,这里我们选择CUDA C/C++接口对CUDA编程进行讲解。

CUDA线程模型

下面我们介绍CUDA的线程组织结构。首先我们都知道,线程是程序执行的最基本单元,CUDA的并行计算就是通过成千上万个线程的并行执行来实现的。下面的结构图说明了GPU的不同层次的结构。

  • 首先GPU上很多并行化的轻量级线程 Thread,是并行的基本单位;
  • GPU核心程序 kernel 在 device上 执行时启动很多 Thread;
  • 一个 kernel 所启动的所有线程称为一个网格 grid,以1维、2维组织,同一个 grid 上的线程共享相同的全局内存空间,grid 是线程结构的第一层次;
  • 而 grid 又可以分为很多线程块 block,一个 block 里面包含很多 Thread,是线程结构的第二个层次。

CUDA个人入坟笔记_第6张图片

如上图所示,这是一个 gird 和 block 均为 2-dim 的线程组织

dim3 grid(2, 2);
dim3 block(4, 2, 2);
kernel_fun<<< grid, block >>>(prams...);

grid 和 block 都是定义为 dim3 类型的变量,dim3 可以看成是包含三个无符号整数(x,y,z)成员的结构体变量,在定义时,缺省值初始化为1。因此 grid 和 block 可以灵活地定义为 1-dim , 2-dim 以及 3-dim 结构,kernel 调用时也必须通过执行配置 <<>> 来指定 kernel 所使用的网格维度和线程块维度。

CUDA的这种 <<>> 其实就是一个多级索引的方法,第一级索引是 (grid.xIdx, grid.yIdy),对应上图选中的 Block 就是(1, 1),通过它我们就能找到了这个线程块的位置,然后我们启动二级索引 (block.xIdx, block.yIdx, block.zIdx) 来定位到指定的 Thread。这就是CUDA的线程组织结构。

从硬件上来看,GPU上有:

  • SP:最基本的处理单元,streaming processor,也称为CUDA core。最后具体的指令和任务都是在SP上处理的。GPU进行并行计算,也就是很多个SP同时做处理。GTX1050Ti上有768个SP。
  • SM:多个SP加上其他的一些资源组成一个streaming multiprocessor。也叫GPU大核,其他资源如:warp scheduler,register,shared memory等。SM可以看做GPU的心脏(对比CPU核心),register 和 shared memory 是SM的稀缺资源。CUDA将这些资源分配给所有驻留在SM中的 threads 。因此,这些有限的资源就使每个SM中 active warps 有非常严格的限制,也就限制了并行能力。GTX1050Ti有6组SM单元。

需要指出,每个SM包含的SP数量依据GPU架构而不同,Fermi架构GF100是32个,GF10X是48个,Kepler架构都是192个,Maxwell都是128个。

简而言之,SP是线程执行的硬件单位,SM中包含多个SP,一个GPU可以有多个SM(GTX1050Ti有6组SM单元),最终一个GPU可能包含有上千个SP。这么多核心“同时运行”,速度可想而知,这个引号只是想表明实际上,软件逻辑上是所有SP是并行的,但是物理上并不是所有SP都能同时执行计算(比如我们只有8个SM却有1024个线程块需要调度处理),因为有些会处于挂起,就绪等其他状态,这有关GPU的线程调度。

CUDA个人入坟笔记_第7张图片

  • 每个 Thread 由每个 SP 执行
  • block 由 SM 执行
  • 一个 kernel 由一个 grid 来执行,一个 kernel 一次只能在一个 GPU 上执行

程序员在开发时,通过设定block的属性,告诉 GPU:我有多少个 Thread ,Thread 怎么组织。而具体怎么调度由sm的 warps scheduler 负责,block 一旦被分配好 SM,该 block 就会一直驻留在该 SM 中,直到执行结束。一个 SM 可以同时拥有多个 blocks,但需要序列执行。

有时候,我们要知道一个线程在blcok中的全局ID,此时就必须还要知道 block 的组织结构,这是通过线程的内置变量 blockDim 来获得。它获取线程块各个维度的大小。对于一个 2-dim 的 block ,线程 的ID值为 ,如果是3-dim的block ,线程 的ID值为 。另外线程还有内置变量 gridDim,用于获得网格块各个维度的大小。

kernel 的这种线程组织结构天然适合 vector , matrix 等运算,如我们将利用 2-dim 结构实现两个矩阵的加法,每个线程负责处理每个位置的两个元素相加,代码如下所示。线程块大小为 (16, 16) ,然后将N*N大小的矩阵均分为不同的线程块来执行加法运算。

// Kernel定义
__global__ void MatAdd(float A[N][N], float B[N][N], float C[N][N]) 
{ 
    int i = blockIdx.x * blockDim.x + threadIdx.x; 
    int j = blockIdx.y * blockDim.y + threadIdx.y; 
    if (i < N && j < N) 
        C[i][j] = A[i][j] + B[i][j]; 
}
int main() 
{ 
    ...
    // Kernel 线程配置
    dim3 threadsPerBlock(16, 16); 
    dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
    // kernel调用
    MatAdd<<>>(A, B, C); 
    ...
}

CUDA内存模型

CUDA中的内存模型分为以下几个层次:

  • 每个 Thread 都用自己的 registers(寄存器)
  • 每个 Thread 都有自己的 local memory(局部内存)
  • 每个 block 内都有自己的 shared memory(共享内存),所有线程块内的所有线程共享这段内存资源
  • 每个 grid 都有自己的 global memory(全局内存),不同线程块的线程都可使用
  • 每个 grid 都有自己的 constant memory(常量内存)和 texture memory(纹理内存),不同线程块的线程都可使用

线程访问这几类存储器的速度是 register > local memory >shared memory > global memory

下面这幅图表示就是这些内存在计算机架构中的所在层次。

CUDA个人入坟笔记_第8张图片

CUDA编程模型

上面讲了这么多硬件相关的知识点,现在终于可以开始说说CUDA是怎么写程序的了。

我们先捋一捋常见的CUDA术语:

CUDA个人入坟笔记_第9张图片

我们要解决的第一个问题是:我们怎么写一个能在GPU跑的程序或函数呢?

在使用DirectX和OpenGL时,我们需要用shader程序调用GPU上不同的着色器,但在CUDA架构中,我们通过关键字就可以表示某个程序在CPU上跑还是在GPU上跑,如下表所示:

CUDA个人入坟笔记_第10张图片

比如我们用__global__定义一个kernel函数,就是CPU上调用,GPU上执行,注意__global__函数的返回值必须设置为void。

第二个问题是:CPU和GPU间的数据传输怎么实现

图形库DirectX需要。。。。。OpenGL则是建立缓冲对象和索引,将大量的数据暂存到显存上,而在CUDA架构中也有在GPU内存分配回收内存的函数接口:

  • cudaMalloc(): 在设备端分配global memory
  • cudaFree(): 释放存储空间

CPU的数据和GPU端数据做数据传输的函数接口是一样的,他们通过传递的函数实参(枚举类型)来表示传输方向:

cudaMemcpy(void *dst, void* src, size_t nbytes, enum cudaMemcpyKind direction)

enum cudaMemcpyKind:

  • cudaMemcpyHostToDevice(CPU到GPU)
  • cudaMemcpyDeviceToHost(GPU到CPU)
  • cudaMemcpyDeviceToDevice(GPU到GPU)

第三个问题是:怎么用代码表示线程组织模型

我们可以用dim3类来表示网格和线程块的组织方式,网格grid可以表示为一维和二维格式,线程块block可以表示为一维、二维和三维的数据格式。

dim3 dimGrid(100, 50); 	 //5000个线程块,维度是100*50
dim3 dimBlock(4, 8, 8);  //每个线层块内包含256个线程,线程块内的维度是4*8*8

接下来介绍一个非常重要又很难懂的一个知识点,我们怎么计算线程号呢?

1.N个线程块,每一个线程块只有一个线程

dim3 dimGrid(N);
dim3 dimBlock(1);

此时的线程号的计算方式就是

threadId = blockIdx.x;

其中threadId的取值范围为0到N-1。对于这种情况,我们可以将其看作是一个列向量,列向量中的每一行对应一个线程块。列向量中每一行只有1个元素,对应一个线程。

2.M×N个线程块,每个线程块1个线程

由于线程块是2维的,故可以看做是一个M*N的2维矩阵,其线程号有两个维度,即:

dim3 dimGrid(M,N);
dim3 dimBlock(1);

其中

blockIdx.x //取值0到M-1
blcokIdx.y //取值0到N-1

这种情况一般用于处理2维数据结构,比如2维图像。每一个像素用一个线程来处理,此时需要线程号来映射图像像素的对应位置,如

pos = blockIdx.y * blcokDim.x + blockIdx.x; //其中gridDim.x等于M

3.一个线程块,该线程具有N个线程,

dim3 dimGrid(1);
dim3 dimBlock(N);

此时线程号的计算方式为

threadId = threadIdx.x;

其中threadId的范围是0到N-1,对于这种情况,可以看做是一个行向量,行向量中的每一个元素的每一个元素对应着一个线程。

4.M个线程块,每个线程块内含有N个线程

dim3 dimGrid(M);
dim3 dimBlock(N);

这种情况,可以把它想象成二维矩阵,矩阵的行与线程块对应,矩阵的列与线程编号对应,那线程号的计算方式为

threadId = threadIdx.x + blcokIdx*blockDim.x;

上面其实就是把二维的索引空间转换为一维索引空间的过程。

5.M×N的二维线程块,每一个线程块具有P×Q个线程

dim3 dimGrid(M, N);
dim3 dimBlock(P, Q);

这种情况其实是我们遇到的最多情况,特别适用于处理具有二维数据结构的算法,比如图像处理领域。

其索引有两个维度

threadId.x = blockIdx.x*blockDim.x+threadIdx.x;
threadId.y = blockIdx.y*blockDim.y+threadIdx.y;

上述公式就是把线程和线程块的索引映射为图像像素坐标的计算方法。

CUDA应用例子

我们已经掌握了CUDA编程的基本语法,现在我们开始以一些小例子来真正上手CUDA。

首先我们编写一个程序,查看我们GPU的一些硬件配置情况。

#include "device_launch_parameters.h"
#include 

int main()
{
    int deviceCount;
    cudaGetDeviceCount(&deviceCount);
    for(int i=0;i

我们利用nvcc来编译程序。

nvcc test1.cu -o test1

输出结果:因为我的服务器是8个TITAN GPU,为了省略重复信息,下面只显示两个GPU结果

使用GPU device 0: TITAN X (Pascal)
设备全局内存总量: 12189MB
SM的数量:28
每个线程块的共享内存大小:48 KB
每个线程块的最大线程数:1024
设备上一个线程块(Block)种可用的32位寄存器数量: 65536
每个EM的最大线程数:2048
每个EM的最大线程束数:64
设备上多处理器的数量: 28
======================================================
使用GPU device 1: TITAN X (Pascal)
设备全局内存总量: 12189MB
SM的数量:28
每个线程块的共享内存大小:48 KB
每个线程块的最大线程数:1024
设备上一个线程块(Block)种可用的32位寄存器数量: 65536
每个EM的最大线程数:2048
每个EM的最大线程束数:64
设备上多处理器的数量: 28
======================================================

.......

第一个计算任务:将两个元素数目为1024×1024的float数组相加。

首先我们思考一下如果只用CPU我们怎么串行完成这个任务。

CUDA个人入坟笔记_第11张图片

#include 
#include 
#include 
#include 

using namespace std;

int main()
{
    struct timeval start, end;
    gettimeofday( &start, NULL );
    float*A, *B, *C;
    int n = 1024 * 1024;
    int size = n * sizeof(float);
    A = (float*)malloc(size);
    B = (float*)malloc(size);
    C = (float*)malloc(size);

    for(int i=0;i

CPU方式输出结果

max_error is 0
total time is 22ms

如果我们使用GPU来做并行计算,速度将会如何呢?

CUDA个人入坟笔记_第12张图片

编程要点:

  1. 每个Block中的Thread数最大不超过512;
  2. 为了充分利用SM,Block数尽可能多,>100。
#include "cuda_runtime.h"
#include 
#include 
#include 

using namespace std;

__global__ void Plus(float A[], float B[], float C[], int n)
{
    int i = blockDim.x * blockIdx.x + threadIdx.x;
    C[i] = A[i] + B[i];
}

int main()
{
    struct timeval start, end;
    gettimeofday( &start, NULL );
    float*A, *Ad, *B, *Bd, *C, *Cd;
    int n = 1024 * 1024;
    int size = n * sizeof(float);

    // CPU端分配内存
    A = (float*)malloc(size);
    B = (float*)malloc(size);
    C = (float*)malloc(size);

    // 初始化数组
    for(int i=0;i>>(Ad, Bd, Cd, n);

    // 将在GPU端计算好的结果拷贝回CPU端
    cudaMemcpy(C, Cd, size, cudaMemcpyDeviceToHost);

    // 校验误差
    float max_error = 0.0;
    for(int i=0;i

GPU方式输出结果

max error is 0
total time is 1278ms

由上面的例子看出,使用CUDA编程时我们看不到for循环了,因为CPU编程的循环已经被分散到各个thread上做了,所以我们也就看到不到for一类的语句。从结果上看,CPU的循环计算的速度比GPU计算快多了,原因就在于CUDA中有大量的内存拷贝操作(数据传输花费了大量时间,而计算时间却非常少),如果计算量比较小的话,CPU计算会更合适一些。

下面计算一个稍微复杂的例子,矩阵加法,即对两个矩阵对应坐标的元素相加后的结果存储在第三个的对应位置的元素上。

值得注意的是,这个计算任务我采用了二维数组的计算方式,注意一下二维数组在CUDA编程中的写法。

CPU版本

#include 
#include 
#include 
#include 

#define ROWS 1024
#define COLS 1024

using namespace std;

int main()
{
    struct timeval start, end;
    gettimeofday( &start, NULL );
    int *A, **A_ptr, *B, **B_ptr, *C, **C_ptr;
    int total_size = ROWS*COLS*sizeof(int);
    A = (int*)malloc(total_size);
    B = (int*)malloc(total_size);
    C = (int*)malloc(total_size);
    A_ptr = (int**)malloc(ROWS*sizeof(int*));
    B_ptr = (int**)malloc(ROWS*sizeof(int*));
    C_ptr = (int**)malloc(ROWS*sizeof(int*));
    
    //CPU一维数组初始化
    for(int i=0;i

CPU方式输出

max_error is 0
total time is 29ms

GPU版本

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include  
#include 
#include 
#define Row  1024
#define Col 1024
 
 
__global__ void addKernel(int **C,  int **A, int ** B)
{
    int idx = threadIdx.x + blockDim.x * blockIdx.x;
    int idy = threadIdx.y + blockDim.y * blockIdx.y;
    if (idx < Col && idy < Row) {
        C[idy][idx] = A[idy][idx] + B[idy][idx];
    }
}
 
int main()
{
    struct timeval start, end;
    gettimeofday( &start, NULL );

    int **A = (int **)malloc(sizeof(int*) * Row);
    int **B = (int **)malloc(sizeof(int*) * Row);
    int **C = (int **)malloc(sizeof(int*) * Row);
    int *dataA = (int *)malloc(sizeof(int) * Row * Col);
    int *dataB = (int *)malloc(sizeof(int) * Row * Col);
    int *dataC = (int *)malloc(sizeof(int) * Row * Col);
    int **d_A;
    int **d_B;
    int **d_C;
    int *d_dataA;
    int *d_dataB;
    int *d_dataC;
    //malloc device memory
    cudaMalloc((void**)&d_A, sizeof(int **) * Row);
    cudaMalloc((void**)&d_B, sizeof(int **) * Row);
    cudaMalloc((void**)&d_C, sizeof(int **) * Row);
    cudaMalloc((void**)&d_dataA, sizeof(int) *Row*Col);
    cudaMalloc((void**)&d_dataB, sizeof(int) *Row*Col);
    cudaMalloc((void**)&d_dataC, sizeof(int) *Row*Col);
    //set value
    for (int i = 0; i < Row*Col; i++) {
        dataA[i] = 90;
        dataB[i] = 10;
    }
    //将主机指针A指向设备数据位置,目的是让设备二级指针能够指向设备数据一级指针
    //A 和  dataA 都传到了设备上,但是二者还没有建立对应关系
    for (int i = 0; i < Row; i++) {
        A[i] = d_dataA + Col * i;
        B[i] = d_dataB + Col * i;
        C[i] = d_dataC + Col * i;
    }
                                                                
    cudaMemcpy(d_A, A, sizeof(int*) * Row, cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, B, sizeof(int*) * Row, cudaMemcpyHostToDevice);
    cudaMemcpy(d_C, C, sizeof(int*) * Row, cudaMemcpyHostToDevice);
    cudaMemcpy(d_dataA, dataA, sizeof(int) * Row * Col, cudaMemcpyHostToDevice);
    cudaMemcpy(d_dataB, dataB, sizeof(int) * Row * Col, cudaMemcpyHostToDevice);
    dim3 threadPerBlock(16, 16);
    dim3 blockNumber( (Col + threadPerBlock.x - 1)/ threadPerBlock.x, (Row + threadPerBlock.y - 1) / threadPerBlock.y );
    printf("Block(%d,%d)   Grid(%d,%d).\n", threadPerBlock.x, threadPerBlock.y, blockNumber.x, blockNumber.y);
    addKernel << > > (d_C, d_A, d_B);
    //拷贝计算数据-一级数据指针
    cudaMemcpy(dataC, d_dataC, sizeof(int) * Row * Col, cudaMemcpyDeviceToHost);
                                                                                             
    int max_error = 0;
    for(int i=0;i

GPU输出

Block(16,16)   Grid(64,64).
max_error is 0
total time is 442 ms

从结果看出,CPU计算时间还是比GPU的计算时间短。这里需要指出的是,这种二维数组的程序写法的效率并不高(虽然比较符合我们的思维方式),因为我们做了两次访存操作。所以一般而言,做高性能计算一般不会采取这种编程方式。

最后一个例子我们将计算一个更加复杂的任务,矩阵乘法

回顾一下矩阵乘法:两矩阵相乘,左矩阵第一行乘以右矩阵第一列(分别相乘,第一个数乘第一个数),乘完之后相加,即为结果的第一行第一列的数,依次往下算,直到计算完所有矩阵元素。

img

CPU版本

#include 
#include 
#include 

#define ROWS 1024
#define COLS 1024

using namespace std;

void matrix_mul_cpu(float* M, float* N, float* P, int width)
{
    for(int i=0;i

CPU输出

total time is 7617ms

梳理一下CUDA求解矩阵乘法的思路:因为C=A×B,我们利用每个线程求解C矩阵每个(x, y)的元素,每个线程载入A的一行和B的一列,遍历各自行列元素,对A、B对应的元素做一次乘法和一次加法。

CUDA个人入坟笔记_第13张图片

GPU版本

#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include  
#include 
#include 
#define Row  1024
#define Col 1024

 
__global__ void matrix_mul_gpu(int *M, int* N, int* P, int width)
{
    int i = threadIdx.x + blockDim.x * blockIdx.x;
    int j = threadIdx.y + blockDim.y * blockIdx.y;
                
    int sum = 0;
    for(int k=0;k> > (d_dataA, d_dataB, d_dataC, Col);
    //拷贝计算数据-一级数据指针
    cudaMemcpy(C, d_dataC, sizeof(int) * Row * Col, cudaMemcpyDeviceToHost);
                                                                                             
    //释放内存
    free(A);
    free(B);
    free(C);
    cudaFree(d_dataA);
    cudaFree(d_dataB);
    cudaFree(d_dataC);

    gettimeofday( &end, NULL );
    int timeuse = 1000000 * ( end.tv_sec - start.tv_sec ) + end.tv_usec - start.tv_usec;
    printf("total time is %d ms\n", timeuse/1000);

    return 0;
}

GPU输出

Block(16,16)   Grid(64,64).
total time is 506 ms

从这个矩阵乘法任务可以看出,我们通过GPU进行并行计算的方式仅花费了0.5秒,但是CPU串行计算方式却花费了7.6秒,计算速度提升了十多倍,可见并行计算的威力

你可能感兴趣的:(CUDA个人入坟笔记)