用 C 表示回文串中心 Centre,用 R 表示臂长右半部分下标边界,即 R=C+P[i]。C 和 R 对应当前循环中 R 最靠右的回文串(手最长的,能管这个数组到最远的,之所以可以保证复杂度是因为保证 R 不退缩)。
如图,在求 P[i] 时,用 i_mirror 表示要求的第 i 个字符关于 C 对应的下标。我们要求 P[i],用中心扩展法就是向两边伸长。但是可以利用已经求出来的回文串中心 C 的对称性,因为 P[i_mirror]=3,那干脆让 P[i]=3。有以下 3 中情况会导致这个直接赋值不正确:
1. 超出了 R
当求 P[i] 时,P[i_mirror]=7,但是却不应该等于 7,因为从 i 往后面数 7 个已经等于 22 了,然而 R 才到 20,很显然还没有探索到那里。但是可以保证我们一定可以扩展到 R(因为对称),所以 P[i] 至少可以为 5。剩下的只能中心扩展了。
voidmerge(int a, int b){ int x = find(a); int y = find(b); if (x == y) { return; } // 比高度 if (rank[x] < rank[y]) { parent[x] = y; } else { parent[y] = x; if (rank[x] == rank[y]) { rank[x]++; } } }
直接内存访问(Direct Memory Access)是一种执行 I/O 的工作方式。在这种方式中,DMA 控制器从 CPU 完全接管对总线的控制。这意味着数据交换不经过 CPU,而直接在内存和 I/O 设备之间进行 。DMA 方式一般用于高速传送成组数据。DMA 控制器将向内存发出地址和控制信号,修改地址,对传送的字的个数计数,并且以中断方式向 CPU 报告传送操作的结束。
DMA 方式的主要优点是速度快。由于 CPU 根本不参加传送操作,因此就省去了 CPU 取指令、取数、送数等操作。在数据传送过程中,没有保存现场、恢复现场之类的工作。内存地址修改、传送字个数的计数等等,也不是由软件实现,而是用硬件线路直接实现的。所以 DMA 方式能满足高速 I/O 设备的要求,也有利于 CPU 效率的发挥。
用户态和内核态
在 Linux 系统中,有的程序权限很高,可以访问计算机的任何资源,但是有的程序权限就低,只能访问部分资源。这两个类型的程序,就可以映射为用户态和内核态。内核态是计算机的核心,可以访问计算机的任何资源,如网卡、硬盘。为了安全,CPU 不能让用户程序肆无忌惮的访问计算机的任何资源,这样如果用户程序不稳定可能会造成系统崩溃,因此才有的用户态。
堆同栈一样,都是为进程运行提供动态的内存分配,但其和栈的的一个很大区别在于堆上内存的生命周期和执行分配的函数的生命周期不同,堆上分配的内存只有在对应进程通过系统调用主动释放或进程结束后才会释放。堆的内存分配效率比栈要低得多。因为栈是由操作系统提供管理的,会在底层堆栈提供支持,分配专门的寄存器($esp)存放栈的地址,包括压栈出栈也都有专门的指令执行,所以执行效率很高。而堆则是由 C 函数库提供支持,它的机制相对复杂,例如分配一块内存,库函数会按照一定的算法在堆内存空间中搜索可用的足够大的内存空间,如果没有足够大的连续空间,则需要操作系统来重新整理堆内存,这样才有机会分到足够大小的空间,然后才返回。对于堆来说,频繁的 malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的内存碎片,程序的运行效率降低。而对于栈来说,分配的一定是连续的内存空间。
BSS段
堆段再往下便是 BSS 段这个静态内存区域,它是用来存储静态局部或静态全局变量的,其在编译期间便决定了虚拟内存的消耗,BSS 段存放的是未初始化的变量。另外根据 C 语言标准规定,未初始化的静态成员变量的初始值必须为 0,所以内核在加载二进制文件后执行程序前会将 BSS 段清 0。
DATA段
DATA 段也是个静态内存区域,也是用来存储静态局部或静态全局变量。但是放的是已经初始化的变量。
代码段
DATA 段再往下便是代码段,这段中存有程序的指令代码。TEXT 段是通过只读的方式加载到内存中的,它可以在多个进程中被安全共享。
classA { public: A() = default; A(int a, int b, int c) : pub(a), pro(b), priv(c) {} int pub; protected: int pro; private: int priv; };
classB : public A { // 公有继承 public: B(int a, int b, int c) { pub = a; pro = b; // pri = c; // test.cc:20:9: error: ‘pri’ was not declared in this scope // pri = c; // ^~~ } };
intmain(){ B b(1, 2, 3); cout << "b.pub:" << b.pub << endl; // cout << "b.pro:" << b.pro << endl; // test.cc:31:27: error: ‘int A::pro’ is protected within this context // cout << "b.pro:" << b.pro << endl; // ^~~ // cout << "b.priv:" << b.priv << endl; // test.cc:32:28: error: ‘int A::priv’ is private within this context // cout << "b.priv:" << b.priv << endl; // ^~~~ return0; }
classB : public A { public: voidfunc(int a, int b){ cout << "B::func(int a, int b)" << endl; } };
intmain(){ B b; b.func(10); return0; }
1 2 3 4 5 6 7 8 9
@└────> # g++ test.cc test.cc: In function ‘int main()’: test.cc:20:14: error: no matching function for call to ‘B::func(int)’ b.func(10); ^ test.cc:13:10: note: candidate: ‘void B::func(int, int)’ void func(int a, int b) { ^~~~ test.cc:13:10: note: candidate expects 2 arguments, 1 provided
CUDA 是 NVIDIA 推出的用于自家 GPU 的并行计算框架,也就是说 CUDA 只能在 NVIDIA 的 GPU 上运行,而且只有当要解决的计算问题是可以大量并行计算的时候才能发挥 CUDA 的作用。CUDA 的主要作用是连接 GPU 和 应用程序,方便用户通过 CUDA 的 API 调度 GPU 进行计算。
一个CUDA应用通常可以分解为两部分,
CPU 主机端代码
GPU 设备端代码
CUDA nvcc 编译器会自动分离你代码里面的不同部分,host 代码用 cpp 写成,使用本地的 g++ 编译器编译,设备端代码,也就是核函数,用 CUDA C 编写,通过 nvcc 编译,链接阶段,在内核程序调用或者明显的 GPU 设备操作时,添加运行时库。
intmain(int argc, char **argv){ printf("CPU: Hello world!\n"); hello_world<<<1,10>>>(); cudaDeviceReset(); //if no this line, it can not output hello world from gpu return0; }
这句话如果没有,则不能正常的运行,因为这句话包含了隐式同步,GPU 和 CPU 执行程序是异步的,核函数调用后成立刻会到主机线程继续,而不管 GPU 端核函数是否执行完毕,所以上面的程序就是GPU 刚开始执行,CPU已经退出程序了,所以我们要等 GPU 执行完了,再退出主机线程。
调用核函数
核函数就是在 CUDA 模型上诸多线程中运行的那段串行代码,这段代码在 GPU 上运行,用 NVCC 编译,产生的机器码是 GPU 的机器码,所以我们写 CUDA 程序就是写核函数,第一步我们要确保核函数能正确的运行产生正确的结果,第二优化 CUDA 程序的部分,无论是优化算法,还是调整内存结构,线程结构都是要调整核函数内的代码,来完成这些优化的。
我们一直把我们的 CPU 当做一个控制者,运行核函数,要从 CPU 发起。
左图:一个四核 CPU 一般有四个 ALU,ALU 是完成逻辑计算的核心,也是我们平时说四核八核的核,控制单元,缓存也在片上,DRAM 是内存,一般不在片上,CPU 通过总线访问内存。
右图:GPU,绿色小方块是 ALU,我们注意红色框内的部分 SM,这一组 ALU 共用一个 Control 单元和 Cache,这个部分相当于一个完整的多核 CPU,但是不同的是 ALU 多了,Control 部分变小。所以计算能力提升了,控制能力减弱了。所以对于控制密集的程序,一个 GPU 的 SM 是没办法和 CPU 比较的,但是对了逻辑简单,数据量大的任务,GPU 更高效。并且,一个 GPU 有好多个 SM。
SM:多个(几十或者上百,取决于设备) SP 加上其他的一些资源组成一个 SM,streaming multiprocessor。其他资源也就是存储资源,共享内存,寄储器等。各个 SM 之间只能通过全局内存间接通信,没有其它互联通道,所以这个集群只适合进行纯并行化计算。如果在计算过程中每个 SM 之间还需要通信,则整体运行效率很低。
Wrap:
SM 中的 SP 会分成成组的 Warp,每组 32 个。
Wrap Scheduler 会从在 SM 上的所有 Warp 中进行指令调度。从已经有指令可以被执行的 Warp 中挑选然后分配下去。这些 Warp 可能来自与驻留在 SM 上的任何线程块。