CUDA编程(六)

进一步并行

在之前我们使用Thread完毕了简单的并行加速,尽管我们的程序运行速度有了50甚至上百倍的提升,可是依据内存带宽来评估的话我们的程序还远远不够。在上一篇博客中给大家介绍了一个訪存方面非常重要的优化。我们通过使用连续的内存存取模式。取得了令人惬意的优化效果,终于内存带宽也达到了GB/s的级别。

之前也已经提到过了,CUDA不仅提供了Thread。还提供了Grid和Block以及Share Memory这些非常重要的机制,我的显卡的Thread极限是1024,可是通过block和Grid。线程的数量还能成倍增长,甚至用几万个线程。所以本篇博客我们将再次回到线程和并行的角度,进一步的并行加速我们的程序。

Thread AND Block AND Grid

第一篇博客的时候就给大家说明过thread-block-grid 结构了。这里我们再复习一下。

在 CUDA 架构下。显示芯片运行时的最小单位是thread。数个 thread 能够组成一个block。一个 block 中的 thread 能存取同一块共享的内存。并且能够高速进行同步的动作。

每一个 block 所能包括的 thread 数目是有限的。只是,运行同样程序的 block。能够组成grid。不同 block 中的 thread 无法存取同一个共享的内存。因此无法直接互通或进行同步。

因此,不同 block 中的 thread 能合作的程度是比較低的。只是,利用这个模式,能够让程序不用操心显示芯片实际上能同一时候运行的 thread 数目限制。

比如。一个具有非常少量运行单元的显示芯片,可能会把各个 block 中的 thread 顺序运行。而非同一时候运行。不同的 grid 则能够运行不同的程序(即 kernel)。

每一个 thread 都有自己的一份 register 和 local memory 的空间。

同一个 block 中的每一个thread 则有共享的一份 share memory。此外,全部的 thread(包括不同 block 的 thread)都共享一份 global memory、constant memory、和 texture memory。不同的 grid 则有各自的 global memory、constant memory 和 texture memory。

大家可能注意到不同block之间是无法进行同步工作的,只是,在我们的程序中。事实上不太须要进行 thread 的同步动作,因此我们能够使用多个 block 来进一步添加thread 的数目。

通过多个block使用很多其它的线程

以下我们就開始继续改动我们的程序:

先贴一下之前的完整代码:

#include <stdio.h>
#include <stdlib.h>
#include <time.h> //CUDA RunTime API
#include <cuda_runtime.h> //1M
#define DATA_SIZE 1048576 #define THREAD_NUM 1024 int data[DATA_SIZE]; //产生大量0-9之间的随机数
void GenerateNumbers(int *number, int size)
{
for (int i = 0; i < size; i++) {
number[i] = rand() % 10;
}
} //打印设备信息
void printDeviceProp(const cudaDeviceProp &prop)
{
printf("Device Name : %s.\n", prop.name);
printf("totalGlobalMem : %d.\n", prop.totalGlobalMem);
printf("sharedMemPerBlock : %d.\n", prop.sharedMemPerBlock);
printf("regsPerBlock : %d.\n", prop.regsPerBlock);
printf("warpSize : %d.\n", prop.warpSize);
printf("memPitch : %d.\n", prop.memPitch);
printf("maxThreadsPerBlock : %d.\n", prop.maxThreadsPerBlock);
printf("maxThreadsDim[0 - 2] : %d %d %d.\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("maxGridSize[0 - 2] : %d %d %d.\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
printf("totalConstMem : %d.\n", prop.totalConstMem);
printf("major.minor : %d.%d.\n", prop.major, prop.minor);
printf("clockRate : %d.\n", prop.clockRate);
printf("textureAlignment : %d.\n", prop.textureAlignment);
printf("deviceOverlap : %d.\n", prop.deviceOverlap);
printf("multiProcessorCount : %d.\n", prop.multiProcessorCount);
} //CUDA 初始化
bool InitCUDA()
{
int count; //取得支持Cuda的装置的数目
cudaGetDeviceCount(&count); if (count == 0) {
fprintf(stderr, "There is no device.\n");
return false;
} int i; for (i = 0; i < count; i++) { cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, i);
//打印设备信息
printDeviceProp(prop); if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
if (prop.major >= 1) {
break;
}
}
} if (i == count) {
fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
return false;
} cudaSetDevice(i); return true;
} // __global__ 函数 (GPU上运行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{ //表示眼下的 thread 是第几个 thread(由 0 開始计算)
const int tid = threadIdx.x; int sum = 0; int i; //记录运算開始的时间
clock_t start; //仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行记录
if (tid == 0) start = clock(); for (i = tid; i < DATA_SIZE; i += THREAD_NUM) { sum += num[i] * num[i] * num[i]; } result[tid] = sum; //计算时间的动作,仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行
if (tid == 0) *time = clock() - start; } int main()
{ //CUDA 初始化
if (!InitCUDA()) {
return 0;
} //生成随机数
GenerateNumbers(data, DATA_SIZE); /*把数据拷贝到显卡内存中*/
int* gpudata, *result; clock_t* time; //cudaMalloc 取得一块显卡内存 ( 当中result用来存储计算结果,time用来存储运行时间 )
cudaMalloc((void**)&gpudata, sizeof(int)* DATA_SIZE);
cudaMalloc((void**)&result, sizeof(int)*THREAD_NUM);
cudaMalloc((void**)&time, sizeof(clock_t)); //cudaMemcpy 将产生的随机数拷贝到显卡内存中
//cudaMemcpyHostToDevice - 从内存拷贝到显卡内存
//cudaMemcpyDeviceToHost - 从显卡内存拷贝到内存
cudaMemcpy(gpudata, data, sizeof(int)* DATA_SIZE, cudaMemcpyHostToDevice); // 在CUDA 中运行函数 语法:函数名称<<<block 数目, thread 数目, shared memory 大小>>>(參数...);
sumOfSquares << < 1, THREAD_NUM, 0 >> >(gpudata, result, time); /*把结果从显示芯片复制回主内存*/ int sum[THREAD_NUM]; clock_t time_use; //cudaMemcpy 将结果从显存中复制回内存
cudaMemcpy(&sum, result, sizeof(int)* THREAD_NUM, cudaMemcpyDeviceToHost);
cudaMemcpy(&time_use, time, sizeof(clock_t), cudaMemcpyDeviceToHost); //Free
cudaFree(gpudata);
cudaFree(result);
cudaFree(time); int final_sum = 0; for (int i = 0; i < THREAD_NUM; i++) { final_sum += sum[i]; } printf("GPUsum: %d gputime: %d\n", final_sum, time_use); final_sum = 0; for (int i = 0; i < DATA_SIZE; i++) { final_sum += data[i] * data[i] * data[i]; } printf("CPUsum: %d \n", final_sum); return 0;
}

我们要去添加多个block来继续添加我们的线程数量:

首先define一个block的数目

#define THREAD_NUM 256
#define BLOCK_NUM 32

我们准备建立 32 个 blocks。每一个 blocks 有 256个 threads,也就是说总共同拥有 32*256= 8192个threads,这里有一个问题。我们为什么不用极限的1024个线程呢?那样就是32*1024 = 32768 个线程,难道不是更好吗?事实上并非这种,从线程运行的原理来看,线程数量达到一定大小后,我们再一味的添加线程也不会取得性能提升了,反而有可能会让性能下降,感兴趣的同学能够改一下数量试一下。另外我们的加和部分是在CPU上进行的,越多的线程意味着越多的结果,而这也意味着CPU上的运算压力会越来越大。

接着,我们须要改动kernel 部份,添加bid = blockIdx.x:


// __global__ 函数 (GPU上运行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{ //表示眼下的 thread 是第几个 thread(由 0 開始计算)
const int tid = threadIdx.x; //表示眼下的 thread 属于第几个 block(由 0 開始计算)
const int bid = blockIdx.x; int sum = 0; int i; //记录运算開始的时间
clock_t start; //仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行记录。每一个 block 都会记录開始时间及结束时间
if (tid == 0) time[bid]= clock(); //thread须要同一时候通过tid和bid来确定,同一时候不要忘记保证内存连续性
for (i = bid * THREAD_NUM + tid; i < DATA_SIZE; i += BLOCK_NUM * THREAD_NUM) { sum += num[i] * num[i] * num[i]; } //Result的数量随之添加
result[bid * THREAD_NUM + tid] = sum; //计算时间的动作。仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行,每一个 block 都会记录開始时间及结束时间
if (tid == 0) time[bid + BLOCK_NUM] = clock(); }

关于改动凝视已经写得非常清楚了。

blockIdx.x 和 threadIdx.x 一样是 CUDA 内建的变量。它表示的是眼下的 block 编号。

另外我们把计算时间的方式改成每一个 block 都会记录開始时间及结束时间。

因此我们result和time变量的长度须要进行更改:

//cudaMalloc 取得一块显卡内存 ( 当中result用来存储计算结果,time用来存储运行时间 )
cudaMalloc((void**) &result, sizeof(int) * THREAD_NUM * BLOCK_NUM);
cudaMalloc((void**) &time, sizeof(clock_t) * BLOCK_NUM * 2);

然后在调用核函数的时候,把控制block数量的的參数改成我们的block数:


sumOfSquares << < BLOCK_NUM, THREAD_NUM, 0 >> >(gpudata, result, time);

注意从显存复制回内存的部分也须要改动(因为result和time长度的改变):


/*把结果从显示芯片复制回主内存*/ int sum[THREAD_NUM*BLOCK_NUM]; clock_t time_use[BLOCK_NUM * 2]; //cudaMemcpy 将结果从显存中复制回内存
cudaMemcpy(&sum, result, sizeof(int)* THREAD_NUM*BLOCK_NUM, cudaMemcpyDeviceToHost);
cudaMemcpy(&time_use, time, sizeof(clock_t)* BLOCK_NUM * 2, cudaMemcpyDeviceToHost); //Free
cudaFree(gpudata);
cudaFree(result);
cudaFree(time); int final_sum = 0; for (int i = 0; i < THREAD_NUM*BLOCK_NUM; i++) { final_sum += sum[i]; }

此外,因为涉及到block,我们须要採取不同的计时方式,即把每一个 block 最早的開始时间,和最晚的结束时间相减,取得总运行时间。

    //採取新的计时策略 把每一个 block 最早的開始时间,和最晚的结束时间相减,取得总运行时间
clock_t min_start, max_end; min_start = time_use[0]; max_end = time_use[BLOCK_NUM]; for (int i = 1; i < BLOCK_NUM; i++) {
if (min_start > time_use[i])
min_start = time_use[i];
if (max_end < time_use[i + BLOCK_NUM])
max_end = time_use[i + BLOCK_NUM];
} printf("GPUsum: %d gputime: %d\n", final_sum, max_end - min_start);

完整程序:

#include <stdio.h>
#include <stdlib.h>
#include <time.h> //CUDA RunTime API
#include <cuda_runtime.h> //1M
#define DATA_SIZE 1048576 #define THREAD_NUM 256 #define BLOCK_NUM 32 int data[DATA_SIZE]; //产生大量0-9之间的随机数
void GenerateNumbers(int *number, int size)
{
for (int i = 0; i < size; i++) {
number[i] = rand() % 10;
}
} //打印设备信息
void printDeviceProp(const cudaDeviceProp &prop)
{
printf("Device Name : %s.\n", prop.name);
printf("totalGlobalMem : %d.\n", prop.totalGlobalMem);
printf("sharedMemPerBlock : %d.\n", prop.sharedMemPerBlock);
printf("regsPerBlock : %d.\n", prop.regsPerBlock);
printf("warpSize : %d.\n", prop.warpSize);
printf("memPitch : %d.\n", prop.memPitch);
printf("maxThreadsPerBlock : %d.\n", prop.maxThreadsPerBlock);
printf("maxThreadsDim[0 - 2] : %d %d %d.\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("maxGridSize[0 - 2] : %d %d %d.\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
printf("totalConstMem : %d.\n", prop.totalConstMem);
printf("major.minor : %d.%d.\n", prop.major, prop.minor);
printf("clockRate : %d.\n", prop.clockRate);
printf("textureAlignment : %d.\n", prop.textureAlignment);
printf("deviceOverlap : %d.\n", prop.deviceOverlap);
printf("multiProcessorCount : %d.\n", prop.multiProcessorCount);
} //CUDA 初始化
bool InitCUDA()
{
int count; //取得支持Cuda的装置的数目
cudaGetDeviceCount(&count); if (count == 0) {
fprintf(stderr, "There is no device.\n");
return false;
} int i; for (i = 0; i < count; i++) { cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, i);
//打印设备信息
printDeviceProp(prop); if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
if (prop.major >= 1) {
break;
}
}
} if (i == count) {
fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
return false;
} cudaSetDevice(i); return true;
} // __global__ 函数 (GPU上运行) 计算立方和
// __global__ 函数 (GPU上运行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{ //表示眼下的 thread 是第几个 thread(由 0 開始计算)
const int tid = threadIdx.x; //表示眼下的 thread 属于第几个 block(由 0 開始计算)
const int bid = blockIdx.x; int sum = 0; int i; //记录运算開始的时间
clock_t start; //仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行记录,每一个 block 都会记录開始时间及结束时间
if (tid == 0) time[bid] = clock(); //thread须要同一时候通过tid和bid来确定,同一时候不要忘记保证内存连续性
for (i = bid * THREAD_NUM + tid; i < DATA_SIZE; i += BLOCK_NUM * THREAD_NUM) { sum += num[i] * num[i] * num[i]; } //Result的数量随之添加
result[bid * THREAD_NUM + tid] = sum; //计算时间的动作,仅仅在 thread 0(即 threadIdx.x = 0 的时候)进行。每一个 block 都会记录開始时间及结束时间
if (tid == 0) time[bid + BLOCK_NUM] = clock(); } int main()
{ //CUDA 初始化
if (!InitCUDA()) {
return 0;
} //生成随机数
GenerateNumbers(data, DATA_SIZE); /*把数据拷贝到显卡内存中*/
int* gpudata, *result; clock_t* time; //cudaMalloc 取得一块显卡内存 ( 当中result用来存储计算结果。time用来存储运行时间 )
cudaMalloc((void**)&gpudata, sizeof(int)* DATA_SIZE);
cudaMalloc((void**)&result, sizeof(int)*THREAD_NUM* BLOCK_NUM);
cudaMalloc((void**)&time, sizeof(clock_t)* BLOCK_NUM * 2); //cudaMemcpy 将产生的随机数拷贝到显卡内存中
//cudaMemcpyHostToDevice - 从内存拷贝到显卡内存
//cudaMemcpyDeviceToHost - 从显卡内存拷贝到内存
cudaMemcpy(gpudata, data, sizeof(int)* DATA_SIZE, cudaMemcpyHostToDevice); // 在CUDA 中运行函数 语法:函数名称<<<block 数目, thread 数目, shared memory 大小>>>(參数...);
sumOfSquares << < BLOCK_NUM, THREAD_NUM, 0 >> >(gpudata, result, time); /*把结果从显示芯片复制回主内存*/ int sum[THREAD_NUM*BLOCK_NUM]; clock_t time_use[BLOCK_NUM * 2]; //cudaMemcpy 将结果从显存中复制回内存
cudaMemcpy(&sum, result, sizeof(int)* THREAD_NUM*BLOCK_NUM, cudaMemcpyDeviceToHost);
cudaMemcpy(&time_use, time, sizeof(clock_t)* BLOCK_NUM * 2, cudaMemcpyDeviceToHost); //Free
cudaFree(gpudata);
cudaFree(result);
cudaFree(time); int final_sum = 0; for (int i = 0; i < THREAD_NUM*BLOCK_NUM; i++) { final_sum += sum[i]; } //採取新的计时策略 把每一个 block 最早的開始时间,和最晚的结束时间相减。取得总运行时间
clock_t min_start, max_end; min_start = time_use[0]; max_end = time_use[BLOCK_NUM]; for (int i = 1; i < BLOCK_NUM; i++) {
if (min_start > time_use[i])
min_start = time_use[i];
if (max_end < time_use[i + BLOCK_NUM])
max_end = time_use[i + BLOCK_NUM];
} printf("GPUsum: %d gputime: %d\n", final_sum, max_end - min_start); final_sum = 0; for (int i = 0; i < DATA_SIZE; i++) { final_sum += data[i] * data[i] * data[i]; } printf("CPUsum: %d \n", final_sum); return 0;
}

运行结果:

为了对照我们把block改成1再运行一次:

我们看到32block 256 thread 连续存取的情况下运行用了133133个时钟周期

而在 1block 256 thread 连续存取的情况下运行用了3488971个时钟周期

3488971/133133= 26.21倍

能够看到我们的速度整整提升了26倍,这个版本号的程序。运行的时间降低非常多。

我们还是从内存带宽的角度来进行一下评估:

首先计算一下使用的时间:

133133/ (797000 * 1000) = 1.67e-4S

然后计算使用的带宽:

数据量仍然没有变 DATA_SIZE 1048576,也就是1024*1024 也就是 1M

1M 个 32 bits 数字的数据量是 4MB。

因此。这个程序实际上使用的内存带宽约为:

4MB / 1.67e-4S = 23945.9788MB/s = 23.38GB/s

这对于我这块640,频率仅有797000。已经是一个非常不错的效果了。只是,这个程序尽管在GPU上节省了时间。可是在 CPU 上运行的部份,须要的时间加长了(因为 CPU 如今须要加总 8192 个数字)。

为了避免这个问题,下一步我们能够让每一个 block 把自己的每一个 thread 的计算结果进行加总。

关于很多其它线程的小实验,越多线程越好?

之前中间提过我们为什么不用很多其它的线程,比方一个block 1024个。或者很多其它的block。

这是因为从线程运行的原理来看。线程数量达到一定大小后,我们再一味的添加线程也不会取得性能提升了。反而有可能会让性能下降。

我们能够试验一下:

1024Thread *128block = 101372 个 Thread 够多了吧。我们看下运行结果:

我们看到终于用了153292个时钟周期,劲爆的10万个线程真的变慢了。

为什么会这样呢?以下我们从GPU的原理上来解说这个问题。

从GPU结构理解线程:

之前关于为什么线程不能这么多的问题,说的还是不是非常清楚。事实上从硬件角度分析,支持CUDA的NVIDIA 显卡,都是由多个multiprocessors 组成。每一个 multiprocessor 里包括了8个stream processors,其组成是四个四个一组,也就是两组4D的处理器。

每一个 multiprocessor 还具有 非常多个(比方8192个)寄存器,一定的(比方16KB) share memory,以及 texture cache 和 constant cache

在 CUDA 中,大部份主要的运算动作。都能够由 stream processor 进行。每一个 stream processor 都包括一个 FMA(fused-multiply-add)单元,能够进行一个乘法和一个加法。

比較复杂的运算则会须要比較长的时间。

在运行 CUDA 程序的时候。每一个 stream processor 就是相应一个 thread。每一个 multiprocessor 则相应一个 block。可是我们一个block往往有非常大量的线程,之前我们用到了256个和1024个。远超一个 multiprocessor 全部的8个 stream processor 。

实际上。尽管一个 multiprocessor 仅仅有八个 stream processor,可是因为 stream processor 进行各种运算都有 latency,更不用提内存存取的 latency,因此 CUDA 在运行程序的时候,是以warp 为单位。

比方一个 warp 里面有 32 个 threads。分成两组 16 threads 的 half-warp。

因为 stream processor 的运算至少有 4 cycles 的 latency,因此对一个 4D 的stream processors 来说。一次至少运行 16 个 threads(即 half-warp)才干有效隐藏各种运算的 latency。

也因此。线程数达到隐藏各种latency的程度后。之后数量的提升就没有太大的作用了。

另一个重要的原因是,因为 multiprocessor 中并没有太多别的内存,因此每一个 thread 的状态都是直接保存在multiprocessor 的寄存器中。所以,假设一个 multiprocessor 同一时候有愈多的 thread 要运行,就会须要愈多的寄存器空间。比如,假设一个 block 里面有 256 个 threads。每一个 thread 用到20 个寄存器,那么总共就须要 256x20 = 5,120 个寄存器才干保存每一个 thread 的状态。

而一般每一个 multiprocessor 仅仅有 8,192 个寄存器。因此。假设每一个 thread 使用到16 个寄存器,那就表示一个 multiprocessor 的寄存器同一时候最多仅仅能维持 512 个 thread 的运行。假设同一时候进行的 thread 数目超过这个数字。那么就会须要把一部份的数据储存在显卡内存中,就会降低运行的效率了。

总结:

这篇博客主要使用block进行了进一步增大了线程数,进行进一步的并行,终于的结果还是比較令人惬意的,至少对于我这块显卡来说已经非常不错了,因为我的显卡主频比較低,假设用一块1.5Ghz的显卡。用的时间就会是我的一半,而这时候内存带宽也就基本达到45GB/s左右了。

同一时候也回答了非常多人都会有的疑问,即为什么我们不搞几万个线程。

可是我们也看到了新的问题,我们在CPU端的加和压力变得非常大。那么我们能不能从GPU上直接完毕这个工作呢?我们知道每一个block内部的Thread之间是能够同步和通讯的,下一步我们将让每一个block把每一个thread的计算结果进行加和。

希望我的博客能帮助到大家~

參考资料:《深入浅出谈CUDA》

CUDA编程(六)进一步并行的更多相关文章

  1. CUDA编程模型——组织并行线程2 (1D grid 1D block)

    在”组织并行编程1“中,通过组织并行线程为”2D grid 2D block“对矩阵求和,在本文中通过组织为 1D grid 1D block进行矩阵求和.一维网格和一维线程块的结构如下图: 其中,n ...

  2. CUDA编程模型——组织并行线程3 (2D grid 1D block)

    当使用一个包含一维块的二维网格时,每个线程都只关注一个数据元素并且网格的第二个维数等于ny,如下图所示: 这可以看作是含有二维块的二维网格的特殊情况,其中块儿的第二个维数是1.因此,从块儿和线程索引到 ...

  3. 【并行计算-CUDA开发】GPU并行编程方法

    转载自:http://blog.sina.com.cn/s/blog_a43b3cf2010157ph.html 编写利用GPU加速的并行程序有多种方法,归纳起来有三种: 1.      利用现有的G ...

  4. CUDA编程之快速入门

    CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架构.做图像视觉领域的同学多多少少都会接触到CUDA,毕竟要做性能速度优化,CUDA是个很重要 ...

  5. CUDA编程之快速入门【转】

    https://www.cnblogs.com/skyfsm/p/9673960.html CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架 ...

  6. 不同版本CUDA编程的问题

    1 无法装上CUDA的toolkit 卸载所有的NVIDIA相关的app,包括NVIDIA的显卡驱动,然后重装. 2之前的文件打不开,one or more projects in the solut ...

  7. cuda编程基础

    转自: http://blog.csdn.net/augusdi/article/details/12529247 CUDA编程模型 CUDA编程模型将CPU作为主机,GPU作为协处理器(co-pro ...

  8. CUDA学习笔记(一)——CUDA编程模型

    转自:http://blog.sina.com.cn/s/blog_48b9e1f90100fm56.html CUDA的代码分成两部分,一部分在host(CPU)上运行,是普通的C代码:另一部分在d ...

  9. CUDA编程

    目录: 1.什么是CUDA 2.为什么要用到CUDA 3.CUDA环境搭建 4.第一个CUDA程序 5. CUDA编程 5.1. 基本概念 5.2. 线程层次结构 5.3. 存储器层次结构 5.4. ...

随机推荐

  1. Javascript 内核Bug

    Javascript 内核Bug: js 执行(9.9+19.8)加法运算 等于 29.700000000000003) <html> <head> <title> ...

  2. java删除数组中的第n个数

    package test; import java.util.Scanner; public class Deletearr { public static void deletearr(){ Sca ...

  3. Google Play 购买(IAB)测试流程

    Google Play 购买(IAB)测试流程 0. 前言 虽然Google 官方也有说明,但是说话很含糊(英文原文也很含糊),很多时候不清楚它到底表达什么.而且帮助文档和开发文档是分开的,可能常常出 ...

  4. div内长串数字或字母不断行处理

    比如: <div>1111tryrt645645rt4554111112324353453454364</div> <div>qwewretrytuytuiyiuo ...

  5. R学习笔记 第五篇:字符串操作

    文本数据存储在字符向量中,字符向量的每个元素都是字符串,而非单独的字符.在R中,可以使用双引号,或单引号表示字符,函数nchar用于获得字符串中的字符数量: > s='read' > nc ...

  6. JavaScript学习笔记(二)——字符串

    在学习廖雪峰前辈的JavaScript教程中,遇到了一些需要注意的点,因此作为学习笔记列出来,提醒自己注意! 如果大家有需要,欢迎访问前辈的博客https://www.liaoxuefeng.com/ ...

  7. Python执行show slave status输出的两个格式

    1.元组的方式 输出格式如下: ('Waiting for master to send event', '10.75.19.79', 'mysqlsync', 5580L, 60L, 'mysql- ...

  8. Java面试之框架篇(八)

    71,谈谈你对Struts的理解. 1. struts是一个按MVC模式设计的Web层框架,其实它就是一个Servlet,这个Servlet名为ActionServlet,或是ActionServle ...

  9. SSE图像算法优化系列十二:多尺度的图像细节提升。

    无意中浏览一篇文章,中间提到了基于多尺度的图像的细节提升算法,尝试了一下,还是有一定的效果的,结合最近一直研究的SSE优化,把算法的步骤和优化过程分享给大家. 论文的全名是DARK IMAGE ENH ...

  10. 如何用webgl(three.js)搭建一个3D库房-第一课

    今天我们来讨论一下如何使用当前流行的WebGL技术搭建一个库房并且实现实时有效交互 第一步.搭建一个3D库房首先你得知道库房长啥样,我们先来瞅瞅库房长啥样(这是我在网上找的一个库房图片,百度了“库房” ...