v -0.500000 -0.500000 0.500000
v 0.500000 -0.500000 0.500000
...
vt 0.001992 0.001992
vt 0.998008 0.001992
vt 0.001992 0.998008
...
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
...
s 1
f 1/1/1 2/2/2 3/3/3
f 3/3/3 2/2/2 4/4/4
s 2
f 3/13/5 4/14/6 5/15/7
f 5/15/7 4/14/6 6/16/8
...
其中,以 v 开头的一行,表示一个顶点的位置坐标;以 vt 开头的一行,表示一个顶点的纹理坐标;以 vn 开头的一行,表示一个顶点的法线方向。这里顶点数据是按 SoA 布局存放的。以 f 开头的一行表示一个三角形图元三个顶点的索引,每个顶点的坐标、纹理、法线索引用 / 隔开。索引是可有可无的,但对于三角形网格模型来说,有的顶点会被相邻三角形公用,如果没有索引,这些公用的顶点将重复出现在不同三角形中,增加了存储和带宽开销,因此大多数情况下都会使用索引。
Top Level Acceleration Structure(TLAS):存储所有模型实例。一个 BLAS 中的模型可以创建多个实例,每个实例需要有一个独一无二的 Inst-ID。同一模型的不同实例可以有不同的“材质”,在光线追踪管线中,材质由 any hit shader、closet hit shader、intersection shader 和输入 Shader 的参数共同决定。另外实例中还需要有一个 3*4 的仿射变换矩阵,用于局部-世界坐标变换,可以每帧更新。
CISC 和 RISC 的区别这里就不介绍。其中,Intel 和 AMD CPU 使用的 x86 指令集架构是一类典型的 CISC ;在移动设备 CPU 上常用的 ARM 指令集架构属于 RISC。不管是 x86 还是 ARM,指令都是按流水线的方式执行,以提高 CPU 的使用率,在下面的章节中详细介绍。
其中, I是 Cache Line 索引, A是内存块索引, N是 Cache Line 数量。那么,0、8、16、32 这四个内存块会映射到 0 号 Cache Line 中,如上图左一。因此一个 Cache Line 不仅包含数据本身,还需要有一个 Tag 标记该 Cache Line 对应哪一个内存块。
直接映射实现简单,但当有新的内存块需要换入缓存时,相应 Cache Line 中的数据就要被换出,置换缺乏灵活性,因为缓存的 Cache Line 可能是未来会使用或者经常使用的,会降低缓存命中率。因此在直接映射的基础上,提出 N 路组相连模式。
N 路组相连(N-Way Set Associativity):多个 Cache Line 组成一个组(Set),如果一个 Set 中有两个 Cache Line,就称为 2 路组相连;如果有四个 Cache Line,就称为 4 路组相连。多个内存块映射到同一个缓存组中任意一个 Cache Line 中。内存块与 Cache Line 是多对多映射,但内存块与 Cache Set 是多对一映射。内存块与 Cache Set 的映射关系可以用取模得到。
以 2 路组相连为例,0、8、16、32 这四个内存块会映射到 0 号 Cache Set 中,并任选一个 Cache Line 存放,如上图左 2。那么当一个内存块映射到一个 Cache Set 时,可以选择最近最久最少使用的 Cache Line 换出,可以有效的提高缓存命中率。缺点是,N 路组相连模式增加了电路设计的复杂性,而且当 CPU 读写缓存时,需要遍历一个 Cache Set 才能判断缓存是否命中。
直接映射也可以看成 1 路组相连
全相连(Fully Associativity):全相连可以是 N 路组相连的极端情况,只有一个 Cache Set,一个 Cache Set 包含所有的 Cache Line,也即一个内存块可以映射到任意一个 Cache Line 中,如上图右 1。那么 CPU 读写缓存时,需要遍历整个缓存才能确定缓存是否命中,适合缓存比较小的计算机使用。
写回(Write Back):如果数据在缓存中,将数据写入缓存,并将该 Cache Line 标记为脏(dirty),当该 Cache Line 被替换时,才把数据写入内存。如果数据不在缓存中,还是将数据写入缓存,先检查目标 Cache Line 是否为脏,如果是,则该 Cache Line 存有别的内存地址需要写入的数据,把该数据写回到内存,然后替换 Cache Line;如果 Cache Line 不为脏,则直接替换并写入
写直达实现简单,但不管数据是否在缓存中,都需要写入内存,影响效率;写回实现复杂,在有必要时才将数据写入内存,效率更高,因此大多数 CPU 都采用回写策略。
缓存一致性(Cache Coherency):在多核 CPU 下,内存供多个核心共享。同时,每个核心独占 L1/L2 缓存,因此同一数据在不同核心的 Cache 中都有一个副本。为了保证缓存一致性,某一 CPU 核心会在写入操作时,通知其他 CPU 核心更新缓存。另外引入锁,写入操作串行化。随着处理器中核数量的增多,保证缓存一致性的开销越来越大,这也是 CPU 核心不能无限扩展的重要原因。
在 NVIDIA GPU 中,共享内存会被分成多个 Bank,可供多个流处理器同时访问,Bank 数量通常和流处理器数量相同。如果每个流处理器与其访问的 Bank 一一对应,那么不需要同步。如果多个流处理器访问同一个 Bank 会导致 Bank Conflicts。下图中从左到右,1、3、4 没有 Bank Conflicts;2、5、6 有 Bank Conflicts。如果发生 Bank Conflict,需要给共享内存数据设置屏障,访问将会串行化。当所有线程的访问结束后,整个线程组的指令才会继续执行。发生 Bank Conflicts 之后,必然会影响性能,应该尽可能避免。
CPU 和 GPU 在内存访问时都要求数据是内存对齐(Memory Alignment)的。以 CPU 为例,一次内存(或缓存)访问,可以并行读写 2 的整数次幂个字节的数据,称为存取粒度。比如 32 位的 CPU 一次可以读写 4 Bytes 数据、64 位 CPU 可一次读写 8 Bytes 数据、16 位 CPU 一次可读写 2 Bytes 数据。这 2 的整数次幂个字节的数据为一组,就将内存划分成一个一个的格子。在 32 位机上,一个 int 数据占 4 个字节,如果刚好放到一个内存“格子”中,那么 CPU 只需要一次内存访问就可以读出这个 int 数据,这就是内存对齐。如果内存不对齐,一个 int 跨越了两个“格子”,那么 CPU 就需要两次内存访问,还要遮掩掉无效的位,并把有效的位拼接起来才能得到一个 int 数据,如下图:
因此连续存放的数据结构,其成员需要考虑内存对齐,并且其首地址也要内存对齐,比如在 32 位机上的如下的 C++ 代码:
结构体可以看成是可以放置不同类型数据的数组,那么为了使内存对齐,成员变量之间就会出现“空隙”,而且不仅成员变量之间需要对齐,结构体对象的首地址也需要对齐。内存对齐是编译器自动完成的,但我们理解了对齐规则后,合理的安排结构体中成员的布局,可以减少成员与成员之间的内存空隙。32 位 CPU 是按照 4 字节对齐,那么上面 Struct A 和 Struct B 的内存布局如下:
这一时期,GPU 硬件的性能大战也进入最终阶段。3D/fx 被 NVIDIA 收购;ATI 和 AMD 合并;Imagination Technologies 放弃桌面端显卡市场,转向移动端 GPU 的研发。SGI 宣布破产,OpenGL 架构评审委员会投票决定将 OpenGL API 标准的控制权交给 Khronos Group
2.4 2006 年-2013 年 GPU 通用计算
2006 年,主机游戏进入 PlayStation3 和 XBOX 360 时代。同一年,Direct3D 10 发布,一并提出的还有 Shader Model 4.0。SM4.0 新增几何着色器(Geometry Shader)。同时 SM 4.0 还提出统一着色架构(Unified Shader Architecture)的概念。最早宣布支持统一着色架构的硬件是 ATI 推出的,使用在 XBOX 360 上的 GPU。所谓的统一着色器架构是相对于分离式着色架构而言的,分离式着色架构是指顶点着色器和像素着色器运行在不同的 ALU 上,这些处理器使用不同的指令集,因为顶点和像素处理的数量的差别,可能出现像素着色处理器繁忙但顶点着色处理器闲置的情况。统一着色器架构是将顶点处理器和像素处理器合二为一,称为流处理器(Streaming processors,SP),它们使用同一套指令集,拥有能运行顶点着色器代码、几何着色器代码和像素着色器代码的所有指令,GPU 负责调度这些流处理器的均衡,避免处理器闲置,提高计算效率。
2006 年,NVIDIA 以科学家 Tesla 命名的显卡架构支持统一着色架构;AMD 和 ATI 合作的第一代显卡架构 TeraScale 也支持统一着色架构。GPU 支持统一着色架构意味着 GPU 计算核心的指令集相比过去更加复杂,那么也能完成更加通用的计算任务,GPGPU(General Purpose GPU Programming)的概念出现。利用 GPU 多核处理架构和大数据吞吐能力,可以加速一些数据密集型任务。NVIDIA 提出统一计算设备架构(Compute Unified Device Architecture,CUDA),并为软件开发者提供 CUDA SDK ,能在非图形程序中为控制 GPU 执行特定算法;而 AMD/ATI、Apple 等与 Khronos Group 合作,制定开源的 OpenCL(Open Computing Language)标准,是一种跨平台的 GPGPU 接口。
其优势是渲染管线没有中断,有利于提高GPU的最大吞吐量,最大化的利用GPU性能。同时从vertex到raster的处理都是在GPU内部的on-chip buffer上进行的,这意味着只需要很少的带宽(bandwidth),就可以存取(storing and retrieving)处理过程中的图元数据。
移动芯片都是SoC(System on Chip),CPU和GPU等元件都在一个芯片上,芯片面积(die size)寸土寸金。自然不可能像桌面端一样给显卡配备GDDR显存,通过独立的北桥(PCI-e)进行通信。在移动端CPU和GPU使用同一个物理内存也更加灵活一些,操作系统可以决定分配给GPU的显存大小。当然副作用就是CPU和GPU很多时候会抢占带宽,这会进一步限制GPU能使用的带宽。
Mali GPU的ALU是不区分FP32和FP16的。像Midgard一样,GPU会做指令的分解和融合。其ALU每个时钟周期可以处理1个INT32、2个INT16或4个INT8。像PowerVR,FP32和FP16 ALU是独立的,这也是在空间和功率效率之间进行权衡的选择。独立ALU会占用更多的芯片面积,但是会减少功耗。从结果上看,PowerVR的设计更加合理,后面会介绍的Mali-G78重写的FMA(Fused-Multiply-Add)也修改为FP32和FP16独立ALU的设计了。
现在衡量GPU性能的一个重要指标是Floating-point Operations的能力。结合GPU核心的时钟频率就可以得到FLOPS(Floating-point Operations Per Second),也就是我们在跑分软件里面看到的GFLOPS。需要注意的是,FMA(Fused-Multiply-Add,a x b + c)或者叫MAD,也就是乘加,一次执行记做两个FLOP。
试说PowerVR家的TBDR。文中摘引是Alpha Test VS Alpha Blend这里的讨论,算是比较官方的回答。
Alpha tested primitives will do the following:ISP HSR: Depth and and stencil tests (no writes)Shading: Colours are calculated for fragments that pass the testsVisibility feedback to ISP: After the shader has executed, the GPU knows which fragments were discarded and which where kept. Visibility information is fed back to the ISP so depth and stencil writes can be performed for the fragments that passed the alpha testWhen discard is used, pixel visibility isn’t known until the corresponding fragment shader executes. Because of this, depth and stencil writes must be deferred until pixel visibility is known. This reduces performance as the pixel visibility information has to be fed back to the ISP unit after shader execution to determine which depth/stencil positions need to be written to. The cost of this can vary, but in the worse case the entire fragment processing pipeline will stall until the deferred depth/stencil writes complete.