帧率、吞吐量、延迟

观点: 帧率只能反映吞吐量,而不能反映延迟,帧率能否作为判断应用程序流畅度的依据有待商榷,个人认为应当根据延迟判断应用程序的流畅度

同时,本文解释了次世代API——Direct3D12、Vulkan和Metal——的意义之一


理由:

我们设 CPU端请求渲染第N帧的时刻 为 第N帧的请求时刻 //一般可以大致认为是应用程序调用Render函数的那一刻

我们设 GPU端完成渲染第N帧的时刻 为 第N帧的完成时刻

我们假定应用程序各帧的请求时刻和完成时刻如下://请求时刻和完成时刻的单位并不影响我们的讨论,你也可以假定单位为毫秒

1 2 3
请求时刻 100 130 160
完成时刻 130 160 190


正如2008年的《Real Time Rendering Third Edition》的15.5.1节——Multiprocessor Pipelining——中所述:

早在1994年,人们就提出了可以用Pipeling并行模式对应用程序进行加速。 [ John Rohlf, James Helman. "IRIS Performer: A High Performance Multiprocessing Toolkit for Real-Time 3D Graphics." ACM 1994 ]

我们先以一种形象的方式介绍Pipeline并行模式:

比如,在期末考试结束后,老师们对同学们的试卷进行批改,试卷共100题,第一位老师负责批第1-25题,完成后,他将试卷传给第二位老师,随后第二位老师负责批第26-50题,完成后他将试卷传给第三位老师,以此类推...以上这四位老师即构成了Pipeline并行模式。

如果假定批改第1-25题、26-50题、51-75题和76-100题所需的时间都相同,那么,假定只有一位老师批改100份试卷时的所需时间为4个小时,以上四位老师构成了Pipeline并行模式所需的时间将缩短到1/4,即只需要1个小时

虽然,对于100份试卷而言,批改时间将缩短到1/4,但是,Pipeline并行模式有一个致命的缺陷:对于同一份试卷而言,由于每位老师需要等待前一位老师完成批改,批改时间并不会缩短(考虑到老师之间传试卷会占用时间,批改时间可能反而会增加)

我们将单位时间内完成Task的数量(即批改试卷的数量)称为吞吐量(Throughout),完成同一个Task(即批改同一份试卷)所需的时间称为延迟(Latency),Pipeline并行模式只能提升吞吐量,而无法降低延迟

比如,我们将应用程序分为APP、CULL、DRAW三个阶段,使用Pipeline并行模式进行加速,应用程序各帧的请求时刻和完成时刻大致会变为如下:

1 2 3 4 5 6 7
请求时刻 100 110 120 130 140 150 160
完成时刻 130 140 150 160 170 180 190

显然,应用程序的帧率会提升到3倍(在130-190之间,帧数由2帧变为6帧),但是延迟并不会发生变化(仍为30)

显然,在一些对延迟要求较高的应用中(比如VR应用),Pipeline并行模式并不能有效地对应用程序进行加速(甚至可能适得其反)


事实上,帧率能否作为判断应用程序流畅度的依据也有待商榷,比如,我们有以下应用程序:

1 2 3 4 5 6 7
请求时刻 100 110 120 130 140 150 160
完成时刻 1130 1140 1150 1160 1170 1180 1190

相对于最初的情形,应用程序的帧率提升到3倍(之前,在130-190之间,帧数为2帧;现在,在1130-1190之间,帧数为6帧),但是延迟大致增加到30倍(由30变为1000),如果这是一款第一人称射击游戏,那么相信用户一定获得了相当糟糕的体验


在之前的讨论中,我们将渲染同一帧看作一个Task,并且将Task分为APP、CULL和DRAW三个阶段。

根据Pipeline并行模式的性质可知,Pipeline并行模式不能降低完成同一个Task所需的时间,即渲染同一帧的时间。

为了降低渲染同一帧的时间,[ John 1994 (即上文中的论文) ] 提出将Task进一步细分,即对于渲染同一帧的CULL和DRAW阶段,我们将一个Object(物体)看作一个Task,由于在渲染同一帧中,Object的数量不止一个,因此可以做到降低渲染同一帧的时间

但是,我们在CULL阶段结束后,往往需要对CULL阶段得到的所有Object进行一些整体性的操作(比如从近到远排序,根据材质分类等),随后才将这些Object传入到DRAW阶段

然而,在[ John 1994 ]的方法中,CULL阶段得到的Object会被立刻传入到DRAW阶段,我们无法对CULL阶段得到的所有Object进行整体性的操作,因此这种做法基本上被弃用


与Pipeline并行模式相对的另一种做法,是使用Map并行模式对应用程序进行加速,这也是我打算在PatriotEngine中所使用的做法

我们同样以批试卷的例子对Map并行模式进行介绍

试卷共100题,不同的是,试卷分为4页,第一页为1-25题,第二页为26-50题,第三页为51-75题,第四页为76-100题,学生在每页上都写上了自己的姓名和学号;同样是四位老师,不同的是,对于同一份试卷,第一个老师批第一页,第二位老师批第二页,第三位老师批第三页,第四位老师批第四页;对于同一份试卷而言,批改时间将缩短到1/4,即延迟缩短到1/4

使用Map并行模式对应用程序进行加速可以大致概括如下

比如有90个Object构成的数组,我们用3个线程基于Map并行模式进行加速

在APP阶段,分别调用[0-29][30-59][60-99]的Object的LogicUpdate方法

在CULL阶段,我们可以参考NVIDIA的论文,并行构造BVH并进行Frustum Culing

在DRAW阶段,次世代API——Direct3D12、Vulkan和Metal——允许我们分别对[0-29][30-59][60-99]的Object生成DrawCall命令


这也是次世代API的意义之一,老式的API并不允许我们用Map并行模式对DRAW阶段进行加速(比如OpenGL只允许单个MakeContextCurrent的线程调用API)


应用程序各帧的请求时刻和完成时刻大致会变为如下:

1 2 3 4 5 6 7
请求时刻 100 110 120 130 140 150 160
完成时刻 110 120 130 140 150 160 170

与Pipeline并行模式对比可以发现,应用程序的帧率都会提升到3倍,但是Map并行模式会使应用程序的延迟降低到1/3(由30变为10),这是Pipeline并行模式所不具备的