3.2 理解线程束执行的本质 启动内核时,从软件的角度你看到了什么?对于你来说,在内核中似乎所有的线程都是并行地运行的。在逻辑上这是正确的,但从硬件的角度来看,不是所有线程在物理上都可以同时并行地执行 。本章已经提到了把32个线程划分到一个执行单元中的概念:线程束。
3.2.1 线程束和线程块 线程束是SM中基本的执行单元。当一个线程块的网格被启动后,网格中的线程块分布在SM中 。一旦线程块被调度到一个SM上,线程块中的线程会被进一步划分为线程束 。一个线程束由32个连续的线程组成,在一个线程束中,所有的线程按照单指令多线程(SIMT)方式执行;也就是说,所有线程都执行相同的指令,每个线程在私有数据上进行操作。图3-10展示了线程块的逻辑视图和硬件视图之间的关系。
然而,从硬件的角度来看,所有的线程都被组织成了一维的,线程块可以被配置为一维、二维或三维的。 在一个块中,每个线程都有一个唯一的ID。
用x维度作为最内层的维度,y维度作为第二个维度,z作为最外层的维度,则二维或三维线程块的逻辑布局可以转化为一维物理布局。例如,对于一个给定的二维线程块,在一个块中每个线程的独特标识符都可以用内置变量threadIdx和blockDim来计算:
1 threadIdx.y * blockDim.x + threadIdx.x
对于一个三维线程块,计算如下:
1 threadIdx.z*blockDim.x*blockDim.y+threadIdx.y*blockDim.x+threadIdx.x
向正无穷取正就是例如9/8 = 2
如果线程块的大小不是线程束大小的偶数倍,那么在最后的线程束里有些线程就不会活跃。图3-11是一个在x轴中有40个线程、在y轴中有2个线程的二维线程块。从应用程序的角度来看,在一个二维网格中共有80个线程。硬件为这个线程块配置了3个线程束,使总共96个硬件线程去支持80个软件线程。注意,最后半个线程束是不活跃的。即使这些线程未被使用,它们仍然消耗SM的资源,如寄存器。
从逻辑角度来看,线程块是线程的集合,它们可以被组织为一维、二维或三维布局。
从硬件角度来看,线程块是一维线程束的集合。在线程块中线程被组织成一维布局,每32个连续线程组成一个线程束。
3.2.2 线程束分化 控制流是高级编程语言的基本构造中的一种。GPU支持传统的、C风格的、显式的控制流结构,例如,if…then…else、for和while。CPU拥有复杂的硬件以执行分支预测 ,也就是在每个条件检查中预测应用程序的控制流会使用哪个分支。如果预测正确,CPU中的分支只需付出很小的性能代价。如果预测不正确,CPU可能会停止运行很多个周期,因为指令流水线被清空了 。我们不必完全理解为什么CPU擅长处理复杂的控制流。这个解释只是作为对比的背景。GPU是相对简单的设备,它没有复杂的分支预测机制。一个线程束中的所有线程在同一周期中必须执行相同的指令,如果一个线程执行一条指令,那么线程束中的所有线程都必须执行该指令。如果在同一线程束中的线程使用不同的路径通过同一个应用程序,这可能会产生问题 。例如,思考下面的语句:
1 2 3 4 5 6 7 8 if (cond){ } else { }
假如上面的kernel函数有一半的线程束(16个线程)需要执行if语句块中的指令,而另一半需要执行else语句块中的指令。在同一线程束中的线程执行不同的指令,被称为线程束分化 。我们已经知道,在一个线程束中所有线程在每个周期中必须执行相同的指令。如果一个线程束中的线程产生分化,线程束将连续执行每一个分支路径,而禁用不执行这一路径的线程 ,也就是第一次指令执行if的分支,第二次指令执行else分支,活跃的线程数就少了一半。线程束分化会导致性能明显地下降。在前面的例子中可以看到,线程束中并行线程的数量减少了一半:只有16个线程同时活跃地执行,而其他16个被禁用了。条件分支越多,并行性削弱越严重。 图示如下,代码为mathKernel1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 __global__ void mathKernel1 (float *c) { int tid = blockIdx.x* blockDim.x + threadIdx.x; float a = 0.0 ; float b = 0.0 ; if (tid % 2 == 0 ) { a = 100.0f ; } else { b = 200.0f ; } c[tid] = a + b; }
如果使用线程束方法(而不是线程方法)来交叉存取数据,可以避免线程束分化,并且设备的利用率可达到100%。条件(tid/warpSize)%2==0使分支粒度是线程束大小的倍数;偶数编号的线程执行if子句,奇数编号的线程执行else子句 。mathKernel2函数与mathKernel1的输出数据排列不一样,但是效率高。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __global__ void mathKernel2 (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; if ((tid / warpSize) % 2 == 0 ) { ia = 100.0f ; } else { ib = 200.0f ; } c[tid] = ia + ib; }
源代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 #include "../common/common.h" #include <cuda_runtime.h> #include <stdio.h> __global__ void mathKernel1 (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; if (tid % 2 == 0 ) { ia = 100.0f ; } else { ib = 200.0f ; } c[tid] = ia + ib; } __global__ void mathKernel2 (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; if ((tid / warpSize) % 2 == 0 ) { ia = 100.0f ; } else { ib = 200.0f ; } c[tid] = ia + ib; } __global__ void mathKernel3 (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; bool ipred = (tid % 2 == 0 ); if (ipred) { ia = 100.0f ; } if (!ipred) { ib = 200.0f ; } c[tid] = ia + ib; } __global__ void mathKernel4 (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; int itid = tid >> 5 ; if (itid & 0x01 == 0 ) { ia = 100.0f ; } else { ib = 200.0f ; } c[tid] = ia + ib; } __global__ void warmingup (float *c) { int tid = blockIdx.x * blockDim.x + threadIdx.x; float ia, ib; ia = ib = 0.0f ; if ((tid / warpSize) % 2 == 0 ) { ia = 100.0f ; } else { ib = 200.0f ; } c[tid] = ia + ib; } int main (int argc, char **argv) { int dev = 0 ; cudaDeviceProp deviceProp; CHECK(cudaGetDeviceProperties(&deviceProp, dev)); printf ("%s using Device %d: %s\n" , argv[0 ], dev, deviceProp.name); int size = 64 ; int blocksize = 64 ; if (argc > 1 ) blocksize = atoi(argv[1 ]); if (argc > 2 ) size = atoi(argv[2 ]); printf ("Data size %d " , size); dim3 block (blocksize, 1 ) ; dim3 grid ((size + block.x - 1 ) / block.x, 1 ) ; printf ("Execution Configure (block %d grid %d)\n" , block.x, grid.x); float *d_C; size_t nBytes = size * sizeof (float ); CHECK(cudaMalloc((float **)&d_C, nBytes)); size_t iStart, iElaps; CHECK(cudaDeviceSynchronize()); iStart = seconds(); warmingup<<<grid, block>>>(d_C); CHECK(cudaDeviceSynchronize()); iElaps = seconds() - iStart; printf ("warmup <<< %4d %4d >>> elapsed %d sec \n" , grid.x, block.x, iElaps ); CHECK(cudaGetLastError()); iStart = seconds(); mathKernel1<<<grid, block>>>(d_C); CHECK(cudaDeviceSynchronize()); iElaps = seconds() - iStart; printf ("mathKernel1 <<< %4d %4d >>> elapsed %d sec \n" , grid.x, block.x, iElaps ); CHECK(cudaGetLastError()); iStart = seconds(); mathKernel2<<<grid, block>>>(d_C); CHECK(cudaDeviceSynchronize()); iElaps = seconds() - iStart; printf ("mathKernel2 <<< %4d %4d >>> elapsed %d sec \n" , grid.x, block.x, iElaps ); CHECK(cudaGetLastError()); iStart = seconds(); mathKernel3<<<grid, block>>>(d_C); CHECK(cudaDeviceSynchronize()); iElaps = seconds() - iStart; printf ("mathKernel3 <<< %4d %4d >>> elapsed %d sec \n" , grid.x, block.x, iElaps); CHECK(cudaGetLastError()); iStart = seconds(); mathKernel4<<<grid, block>>>(d_C); CHECK(cudaDeviceSynchronize()); iElaps = seconds() - iStart; printf ("mathKernel4 <<< %4d %4d >>> elapsed %d sec \n" , grid.x, block.x, iElaps); CHECK(cudaGetLastError()); CHECK(cudaFree(d_C)); CHECK(cudaDeviceReset()); return EXIT_SUCCESS; }
编译执行如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 zmurder@zmurder:~/WorkSpace/zyd/note/cuda/CUDAC编程权威指南/CUDAC编程权威指南练习code/chapter03$ sudo nvprof -m branch_efficiency ./simpleDivergence ==2809481== NVPROF is profiling process 2809481, command: ./simpleDivergence ./simpleDivergence using Device 0: Quadro P2000 Data size 64 Execution Configure (block 64 grid 1) warmup <<< 1 64 >>> elapsed 0 sec mathKernel1 <<< 1 64 >>> elapsed 0 sec mathKernel2 <<< 1 64 >>> elapsed 0 sec mathKernel3 <<< 1 64 >>> elapsed 0 sec mathKernel4 <<< 1 64 >>> elapsed 0 sec ==2809481== Profiling application: ./simpleDivergence ==2809481== Profiling result: ==2809481== Metric result: Invocations Metric Name Metric Description Min Max Avg Device "Quadro P2000 (0)" Kernel: mathKernel1(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: mathKernel2(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: mathKernel3(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: mathKernel4(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: warmingup(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00%
上面看到执行的线程都是100%,原因是编译器优化了。。。它将短的、有条件的代码段的断定指令取代了分支指令。
使用下面的命令禁止编译器优化并重新编译
1 nvcc -g -G -arch sm_61 simpleDivergence.cu -o simpleDivergence2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 zmurder@zmurder:~/WorkSpace/zyd/note/cuda/CUDAC编程权威指南/CUDAC编程权威指南练习code/chapter03$ sudo nvprof -m branch_efficiency ./simpleDivergence2 ==2810233== NVPROF is profiling process 2810233, command: ./simpleDivergence2 ./simpleDivergence2 using Device 0: Quadro P2000 Data size 64 Execution Configure (block 64 grid 1) warmup <<< 1 64 >>> elapsed 0 sec mathKernel1 <<< 1 64 >>> elapsed 0 sec mathKernel2 <<< 1 64 >>> elapsed 0 sec mathKernel3 <<< 1 64 >>> elapsed 0 sec mathKernel4 <<< 1 64 >>> elapsed 0 sec ==2810233== Profiling application: ./simpleDivergence2 ==2810233== Profiling result: ==2810233== Metric result: Invocations Metric Name Metric Description Min Max Avg Device "Quadro P2000 (0)" Kernel: mathKernel1(float*) 1 branch_efficiency Branch Efficiency 83.33% 83.33% 83.33% Kernel: mathKernel2(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: mathKernel3(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: mathKernel4(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00% Kernel: warmingup(float*) 1 branch_efficiency Branch Efficiency 100.00% 100.00% 100.00%
上面的结果依然大于50%,说明编译器还是优化了一部分。只有在条件语句的指令数小于某个阈值时,编译器才用断定指令替换分支指令。因此,一段很长的代码路径肯定会导致线程束分化。
另外,可以用nvprof获得分支和分化分支的事件计数器,如下所示:
1 sudo nvprof --events branch,divergent_branch ./simpleDivergence2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 zmurder@zmurder:~/WorkSpace/zyd/note/cuda/CUDAC编程权威指南/CUDAC编程权威指南练习code/chapter03$ sudo nvprof --events branch,divergent_branch ./simpleDivergence2 ==2810791== NVPROF is profiling process 2810791, command: ./simpleDivergence2 ./simpleDivergence2 using Device 0: Quadro P2000 Data size 64 Execution Configure (block 64 grid 1) warmup <<< 1 64 >>> elapsed 0 sec mathKernel1 <<< 1 64 >>> elapsed 0 sec mathKernel2 <<< 1 64 >>> elapsed 0 sec mathKernel3 <<< 1 64 >>> elapsed 0 sec mathKernel4 <<< 1 64 >>> elapsed 0 sec ==2810791== Profiling application: ./simpleDivergence2 ==2810791== Profiling result: ==2810791== Event result: Invocations Event Name Min Max Avg Total Device "Quadro P2000 (0)" Kernel: mathKernel1(float*) 1 branch 12 12 12 12 1 divergent_branch 2 2 2 2 Kernel: mathKernel2(float*) 1 branch 11 11 11 11 1 divergent_branch 0 0 0 0 Kernel: mathKernel3(float*) 1 branch 16 16 16 16 1 divergent_branch 0 0 0 0 Kernel: mathKernel4(float*) 1 branch 6 6 6 6 1 divergent_branch 0 0 0 0 Kernel: warmingup(float*) 1 branch 11 11 11 11 1 divergent_branch 0 0 0 0
3.2.3 资源分配 对于一个给定的内核,同时存在于同一个SM中的线程块和线程束的数量取决于在SM中可用的且内核所需的寄存器和共享内存 的数量。 图3-13显示了若每个线程消耗的寄存器越多,则可以放在一个SM中的线程束就越少。如果可以减少内核消耗寄存器的数量,那么就可以同时处理更多的线程束。如图3-14所示,若一个线程块消耗的共享内存越多,则在一个SM中可以被同时处理的线程块就会变少。如果每个线程块使用的共享内存数量变少,那么可以同时处理更多的线程块。
关于寄存器资源的分配:
关于共享内存的分配:
当计算资源(如寄存器和共享内存)已分配给线程块时,线程块被称为活跃的块。它所包含的线程束被称为活跃的线程束。活跃的线程束可以进一步被分为以下3种类型:
一个SM上的线程束调度器在每个周期都选择活跃的线程束,然后把它们调度到执行单元。活跃执行的线程束被称为选定的线程束。如果一个活跃的线程束准备执行但尚未执行,它是一个符合条件的线程束。如果一个线程束没有做好执行的准备,它是一个阻塞的线程束。如果同时满足以下两个条件则线程束符合执行条件。
32个CUDA核心可用于执行
当前指令中所有的参数都已就绪
在CUDA编程中需要特别关注计算资源分配:计算资源限制了活跃的线程束的数量。因此必须了解由硬件产生的限制和内核用到的资源。为了最大程度地利用GPU,需要最大化活跃的线程束数量。
3.2.4 延迟隐藏 在指令发出和完成之间的时钟周期被定义为指令延迟 。当每个时钟周期中所有的线程调度器都有一个符合条件的线程束时,可以达到计算资源的完全利用。这就可以保证,通过在其他常驻线程束中发布其他指令,可以隐藏每个指令的延迟。
指令可以被分为两种基本类型:
算术指令:算术指令延迟是一个算术操作从开始到它产生输出之间的时间。为10~20个周期
内存指令:内存指令延迟是指发送出的加载或存储操作和数据到达目的地之间的时间。全局内存访问为400~800个周期
估算隐藏延迟所需要的活跃线程束的数量。利特尔法则(Little’s Law)可以提供一个合理的近似值。它起源于队列理论中的一个定理,它也可以应用于GPU中: 所需线程束数量=延迟×吞吐量 图3-16形象地说明了利特尔法则。假设在内核里一条指令的平均延迟是5个周期。为了保持在每个周期内执行6个线程束的吞吐量,则至少需要30个未完成的线程束。
计算所需并行的一个简单的公式是,用每个SM核心的数量乘以在该SM上一条算术指令的延迟。例如,Fermi有32个单精度浮点流水线线路,一个算术指令的延迟是20个周期,所以,每个SM至少需要有32×20=640个线程使设备处于忙碌状态。然而,这只是一个下边界。
3.2.5 占用率 当一个线程束阻塞时,SM切换执行其他符合条件的线程束。理想情况下,我们想要有足够的线程束占用设备的核心。占用率是每个SM中活跃的线程束占最大线程束数量的比值
CUDA工具包包含了一个电子表格(在 CUDA Toolkit 中)CUDA_Occupancy_Calculator.xls
,它被称为CUDA占用率计算器,有助于选择网格和块的维数以使一个内核的占用率最大化。在确定GPU的计算能力后,物理限制部分的数据是自动填充的 。图3-17展示了占用率计算器的一个截图。
在确定GPU的计算能力后,物理限制部分的数据是自动填充的。接下来,需要输入以下内核资源信息(也就是上面表格橙色的部分) :
每个块的线程(执行配置)
每个线程的寄存器(资源使用情况)
每个块的共享内存(资源使用情况)
每个块的线程是我们程序控制的
后两个可以通过nvcc
查到。nvcc添加下面的参数
1 2 3 --ptxas-options=-v 或者使用 --resource-usage
结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 zmurder@zmurder:~/WorkSpace/zyd/note/cuda/CUDAC编程权威指南/CUDAC编程权威指南练习code/chapter03$ nvcc -g -G --ptxas-options=-v -arch sm_61 simpleDivergence.cu -o simpleDivergence2 ptxas info : 0 bytes gmem ptxas info : Function properties for cudaMalloc 8 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Compiling entry function '_Z11mathKernel1Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel1Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 11 registers, 328 bytes cmem[0] ptxas info : Compiling entry function '_Z11mathKernel2Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel2Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 13 registers, 328 bytes cmem[0] ptxas info : Compiling entry function '_Z11mathKernel3Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel3Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 11 registers, 328 bytes cmem[0] ptxas info : Compiling entry function '_Z11mathKernel4Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel4Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 10 registers, 328 bytes cmem[0] ptxas info : Compiling entry function '_Z9warmingupPf' for 'sm_61' ptxas info : Function properties for _Z9warmingupPf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 13 registers, 328 bytes cmem[0] ptxas info : Function properties for _ZN4dim3C1Ejjj 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Function properties for _ZN4dim3C2Ejjj 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 zmurder@zmurder:~/WorkSpace/zyd/note/cuda/CUDAC编程权威指南/CUDAC编程权威指南练习code/chapter03$ nvcc --resource-usage simpleDivergence.cu -arch sm_61 ptxas info : 0 bytes gmem ptxas info : Compiling entry function '_Z9warmingupPf' for 'sm_61' ptxas info : Function properties for _Z9warmingupPf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 10 registers, 328 bytes cmem[0], 4 bytes cmem[2] ptxas info : Compiling entry function '_Z11mathKernel4Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel4Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 4 registers, 328 bytes cmem[0] ptxas info : Compiling entry function '_Z11mathKernel3Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel3Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 4 registers, 328 bytes cmem[0], 4 bytes cmem[2] ptxas info : Compiling entry function '_Z11mathKernel2Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel2Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 10 registers, 328 bytes cmem[0], 4 bytes cmem[2] ptxas info : Compiling entry function '_Z11mathKernel1Pf' for 'sm_61' ptxas info : Function properties for _Z11mathKernel1Pf 0 bytes stack frame, 0 bytes spill stores, 0 bytes spill loads ptxas info : Used 4 registers, 328 bytes cmem[0], 4 bytes cmem[2]
上面的分析:
gmem:全局内存 global memory
每个kernel的资源如下
Stack frame:此函数使用的每线程堆栈使用情况
spill loads/stores:溢出存储和加载表示在堆栈内存上执行的存储和加载,这些存储和加载用于存储不能分配给物理寄存器的变量。也就是将寄存器交换到local memory而进行的读写操作(注意你的上文多少字节多少字节是per thread的数据。请注意换算),溢出了会慢,因为local memory速度慢于寄存器。
常量内存(cmem)、共享内存(smem)和寄存器数(register)。具体的内存区别可以查看4-1 CUDA内存模型概述
按照kernel1的数据填写后(红色方框是需要填写的部分,其他都是固定的和生成的,右侧的图标三角形会移动)
网格和线程块大小的准则
保持每个块中线程数量是线程束大小(32)的倍数
避免块太小:每个块至少要有128或256个线程
根据内核资源的需求调整块大小
块的数量要远远多于SM的数量,从而在设备中可以显示有足够的并行
通过实验得到最佳执行配置和资源使用情况
3.2.6 同步 参考官网7.6. Synchronization Functions
在CUDA中,同步可以在两个级别执行:
3.2.7 可扩展性 对于任何并行应用程序而言,可扩展性是一个理想的特性 。可扩展性意味着为并行应用程序提供了额外的硬件资源,相对于增加的资源,并行应用程序会产生加速。例如,若一个CUDA程序在两个SM中是可扩展的,则与在一个SM中运行相比,在两个SM中运行会使运行时间减半。