D3D11和D3D12多线程渲染框架的比较(二)

1.     多线程的一些基础知识和问题

1.1.    并发和并行

如果你对多线程编程理解比较深刻的话,那么首先第一个要搞明白的概念就是“并发”和“并行”的区别,并发很多时候指的是在一个时间段内,共同执行的任务数。比如1秒钟之内轮流执行了50个左右的线程;再比如说一个网站的并发访问峰值是1秒钟10000次,这些任务中的某几个可能在某一时刻内是同时执行的也可能是在某个时间段内是先后执行的,但总的在一个较大的时间颗粒度比如刚才说的1秒钟之内看来是同时执行的(对于现代计算机来说1秒钟已经是一个非常长的时间颗粒度了。颤抖吧,孱弱的人类!)。最终并发则被更多的描述的像一个统计值。

而并行则是指任务之间真正的同时执行,比如多核CPU上每个核执行不同的线程,或者CPU执行物理变换,而GPU同时忙着进行画面渲染等等,这些任务在任何一个时刻都是真正的同时执行的。正如之前例子中所说现代计算机中CPU和GPU也是可以同时执行任务的。甚至在今天我们在说某某设备的计算性能时,更多的是说它的并行计算性能,比如刚刚发布的TITAN V计算卡的计算能力是15TFlops,则是指它的并行计算能力,还有值得我们骄傲的“曙光一号”超算其计算性能也是指并行计算能力。因此可以说现在是一个并行计算的时代,其重要标志就是严重依赖并行计算能力的人工智能大行其道!

1.2.    并行计算极简史

历史上在没有D3D11多线程渲染的年代里,游戏开发人员也大多都没有什么多线程编程经验,在那时CPU通常只有一个核,甚至也还没有GPU的概念,人们所能做的就是编写一个“死循环”,在里面读取用户输入、接着进行物理碰撞检测、从而进行游戏逻辑计算、更新场景、渲染(很多时候是CPU+GPU渲染),然后进入下一次循环,如此往复。那时我们期盼的就是CPU足够Strong,能够飞快的执行每一项任务,从而使我们的游戏可以更加复杂,或者说画面更加的逼真。

在那个年代里,有些游戏开发人员甚至对并发是非常深恶痛绝的。因为在Windows引入多线程结构的早期年代,其实就是利用多个线程不断的在一个CPU内核上来回切换的并发执行方式,让人们产生多线程执行多任务的“错觉”的(因为毕竟人类的反应速度是很慢的),其中线程的切换时间颗粒度大概是20毫秒,而切换线程上下文的时间也是不容忽视的(往往是系统内核执行时间,更深入的知识希望你读过《Windows核心编程》,对你理解D3D12多线程渲染也是大有帮助的),所以总体上虽然我们获得了一定的多任务的体验,但实质上是牺牲了一些系统性能的(系统内核时间几乎不会执行用户真正需要的任务,纯粹是管理开销,想象一个公司中全部都是领导和各种复杂的办事流程,你就明白我在说什么了)。尤其当系统中线程数量非常可观的时候,Windows就表现的非常卡顿,有时甚至像死机一样。如果你有过这样的感受的话,你一定是记忆犹新的。而游戏开发人员则是利用前面所述的死循环来让自己的游戏线程想尽一切办法独占CPU的,所以他们很不喜欢这种并发带来的所谓多任务体验,他们更追求的是游戏的高效流畅运行。

在今天,多核CPU已经是基本配置的情况下,并发的多任务(多线程)则有可能利用多个不同的CPU内核并行的执行,从而真正的让人们感受到多任务带来的优越性。如果我没有记错的话,第一代可以并行执行多任务(多线程)的CPU(超线程)大约出现在2002年左右,距今已经十多年过去了。

那么到了今天这个并行计算大行其道的时代,作为游戏开发人员的你就不能再对多线程编程或者并行计算置若罔闻了。我们所要做的就是掌握它,征服它,从而更好的在游戏开发中应用它。这也就是学习D3D12多线程渲染所能够带来的最大好处!

对多线程渲染本身来说,它使的我们有能力去渲染更复杂的场景,甚至可以在实时渲染中加入很多光线追踪的渲染效果(并行!),从而使最终画面几乎可以达到电影级的质量(这也是当代游戏画质越来越好的原因之一),这些编程能力的解放想想都令人神往,同时这也是多线程渲染带来的巨大优势!

1.3.    多线程内存管理问题

接着你要明白的概念就是多线程内存管理中常说的“脏读”问题了,举个例子来说,就像你往黑板上写字,还没有写完,其他一些同学就记录了这还没写完的信息作为全部信息,或者你只写了一部分,而另一半黑板还没有擦除,还剩余着上次写的内容,这时其他同学把黑板上所有的信息都抄下来作为了完整的信息。无论哪种情况,我们都可以看出来,其他同学读到的信息都不是真正完整有效的信息,这也是多线程无序访问内存时必然遇到的问题,称之为“脏读”。

当然内存访问冲突的问题不止这么简单,无论是“并发”还是“并行”最终都有可能遇到各种内存无序访问的问题,而这些问题对于需要高效执行的程序尤其是并行程序来说都是致命的错误。或者说多线程带来的首要问题就是内存并发(并行)访问问题(内存管理问题)。在C++中还有可能是一个线程已经释放了某块内存,而另一个线程又去试图访问这块内存,或者CPU向显存提交了某块缓冲,在GPU正式渲染之前,这块显存又被意外释放了,从而造成GPU访问违规的问题。现实中多线程或并行内存访问带来的问题,远比这里描述的要复杂的多,传说中曾有程序员为修正多线程程序中的内存访问冲突问题而最终猝死。当然我也曾经遇到过这样的噩梦。

为了解决多线程访问内存冲突的问题,现代操作系统中首先提供了同步堆(Heap,当然你喜欢酷炫的话可以叫它原子堆)来管理内存的分配,同时堆操作方法提供了线程同步访问的原子操作(也称为线程安全的函数),也就是当一个线程调用一个堆方法时,其它线程只能等待它执行结束,才能继续下一个访问(通常是指分配和释放内存操作)。你可以想象这是只有一个马桶的厕所,不能两个以上的人同时访问,大家只能排队一个个访问。这样就最终保障了内存访问的一致性。同样的D3D12中也借助堆这个概念来解决CPU与GPU之间交互访问内存(主要是显存访问)的问题。

当然为了解决多线程访问内存冲突的问题,系统中还准备了很多线程同步对象,比如知名的Event对象、Mutex对象等等。甚至在现代的C++语言标准库中也引入了类似的等价物,来帮助开发人员更好的驾驭多线程编程或者并行编程。

最后还有一个非常可怕的消息,就是过去那些为了单线程渲染而优化设计的无锁的(非同步)的内存池,对象缓冲池,BSP树结构,四叉树等结构,在多线程渲染架构下有可能都会失效了,开发人员可能面临着编写这些对象或结构代码的高效同步版本的问题,以应对多线程访问带来的潜在冲突。给你个小的提示,如果想要高效同步,那么利用Lock-Free算法是个很不错的主意!

2.     D3D12中的内存管理

2.1.    显存堆

在D3D12中为了更好的支持多线程渲染,显存管理也主要以堆的形式来管理。当然为了渲染,需要管理的内存就不仅仅是系统内存这么简单了,其中还包括GPU上的显存(对于共享内存的核显来说被当做显存的那段内存是无法再当做内存来用的)。对于系统内存来说,Windows系统已经有了完整函数族来管理了,没必要在D3D12中再另起炉灶了。因此D3D12中的堆就主要是显存堆了,记住这一点特别重要不然你会被D3D12那冒着弄弄的C语言味道的代码给熏晕的。(此处描述的堆是内存管理的堆,而不是简单的数据结构中的堆栈,不要搞混。)

以Windows堆来类比D3D12的堆的话,D3D12的堆在种类上要丰富的多,这是因为对于GPU渲染来说,不同资源(顶点、索引、常量、纹理、渲染目标等)的显存之间的访问方式和处理方式是大相径庭的,所以很有必要区别对待,所以显存堆的种类就比CPU堆的丰富。

D3D12中堆主要被分为三大类:一类是资源堆,用于存放各种缓冲、纹理、常量等的堆,使用ID3D12Device::CreateHeap创建;另一类是描述符堆,用于存放各类资源视图(RTV、DSV、CBV、UAV等),使用ID3D12Device::CreateDescriptorHeap方法创建;最后一类就是查询堆,主要用于性能分析查询,使用ID3D12Device::CreateQueryHeap创建。

说到这里很多人可能就疑问重重了,说好的是跟大家讲D3D12的多线程渲染的怎么讲了这么多堆概念呢?请大家稍安勿躁,听我慢慢说来,因为D3D12多线程并不是D3D11的升级那么简单,正如我说的,它实际是一个“显卡操作系统”,所以要了解它的核心理念,就需要多一些概念做铺垫。

比之D3D11来说,D3D12中是将显存管理的概念单独以堆的形式独立了出来。其实这主要是为了方便在极其复杂的多趟渲染过程中的不同线程之间,或者是不同的命令队列之间在并行执行的时候,尤其是在CPU线程与GPU线程之间并行(异步)执行任务时,高效安全的管理显存。在D3D11中,这些工作都是隐藏在一个个接口方法之内的,因为显存管理上的固化,其实反倒限制了多线程渲染的自由灵活性,同时还带来了显存重复利用上的限制,因为你根本无法控制那些在显存中的资源的生命周期,有时不得不重复的向显存中提交各种资源。因此D3D12中干脆加入了单独的显存堆管理接口及方法,让显存管理从幕后走到台前,从而为开发人员带来了极大的灵活性。

2.2.    显存的虚拟化管理(tile)

当然在D3D12中还加入了强大的针对显存的虚拟内存管理的概念,被称之为tile和tiled resources。如果你熟悉Windows的内存管理的话,应该立刻就能明白,在Windows中其实也有虚拟内存管理,主要用于大规模内存管理(以MB或GB为单位),而只需要少许的真实物理内存来支持(若干个物理内存页,每页4KB大小)。而D3D12中的tile概念跟这个是异曲同工,由此可以看出tile也是主要解决大规模内存管理之用,当然它对于多线程渲染的助益有限(它的主要目的是管理大规模内存的,因为现在的很多精细纹理动辄可以上几百兆),此文中我就不再过多讲述了,回头另写文章详解之。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片