首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 其他教程 > 互联网 >

精确丈量Direct3D API调用(一)

2013-10-08 
精确测量Direct3D API调用(一)主要有以下议题: 精确测量Direct3D非常困难如何精确测量Direct3D渲染序列测

精确测量Direct3D API调用(一)

主要有以下议题:

 精确测量Direct3D非常困难如何精确测量Direct3D渲染序列测量Direct3D状态变化小结附录

一旦你有一个实用的Mircosoft Direct3D应用,并且你想提高它的性能,你通常会使用一个现成的测量工具或者一些定制的测量技术来度量一个或更多的API调用所花费的时间。


如果你成功做到了这一点,但是从不同的渲染序列得到了不同的计时结果或实验结果并不支持你的假设,接下来的信息可能会帮你理解其中的原因。

下面提供的信息是基于你已经拥有了如下的知识和经验:

C/C++编程Direct3D API编程.测量API耗时显卡和驱动测量经验中无法解释的结果

精确测量Direct3D非常困难


分析器可以提供每个API调用的耗时。通过分析器能够寻找到热点并通过消除它来提高性能。这里有多种分析器和分析技术。

一个采样分析器大部分时间是空闲的,它按照固定的间隔来采样(或者记录)正在被执行的函数。它返回每个调用所花费的时间的比例。通常来说,采样分析器对于应用来说是非侵入性的并且具有非常小的额外开销。一个测量分析器度量每一次调用返回所花费的实际时间。它需要在应用程序中加入起始和结束符。测量分析器相比于采样分析器具有更大的侵入性。也有可能通过高精度计时器来使用定制的测量技术。这个产生的结果非常像测量分析器。

分析器或者分析技术的类型只是精确测量所面临挑战的原因之一。

分析器能够给你提供信息来帮助你预估性能。举个例子,假设一个API调用平均一个时钟周期执行一千次。关于性能你可以断言:

一个2GHz CPU(其中50%的时间用于渲染)的限制是每秒钟调用API一百万次。为了达到30fps,你最多每帧调用这个API33000次。每帧仅能渲染3.3K个对象(假定每个对象的渲染序列花费为10)

换句话说,如果你得到了每个API调用的时间,那么你可以回答一些预估的问题:比如可以交互渲染的图元的数量。但是测量分析器返回的原始数量不能够精确地回答这个预估问题。这是因为图形管线设计很复杂,比如需要不同组件来完成这项工作、组件间的多个处理器(CPU和GPU)协同控制工作流以及为了使管线更加有效率而设计的runtime和驱动的优化策略。

每个API调用经历多个组件,每个调用在从应用程序到显卡的路上需要被多个组件处理。举个例子,考虑下面的渲染序列:包含2个调用来绘制一个三角形

Direct3D控制场景、处理用户交互、决定如何渲染,应用程序调用Direct3D。所有的这些工作定义在渲染序列中,然后使用D3D API 调用发送到runtime。渲染序列是软件无关的(即API调用是软件无关的但是应用程序知道显卡支持的功能)


runtime转换这些调用为硬件无关格式。runtime处理应用程序和驱动之间的通信,所以一个应用程序能够运行在一种兼容的硬件(需要的特性来决定)。当度量一个函数调用,一个测量分析器度量这个函数花费的时间即这个函数返回所需的时间。测量分析器的一个限制就是它可能没有包括驱动发送工作到显卡以及显卡处理这份工作所需的时间。换句话说,现成的测量分析器不能正确地分析出每个函数调用关联的所有工作。


软件驱动利用显卡相关的硬件知识将设备无关的命令转化为一系列显卡的命令。驱动也会对将要发送到显卡的命令序列进行优化,以便显卡有效地渲染。这些优化会导致测量问题,因为所做的工作并非我们所见(你可能需要了解优化工作以便解释它们)。驱动一般会在显卡处理完所有命令前就返回控制权。


显卡综合顶点缓存、索引缓存、纹理、渲染状态信息、图形命令来处理绝大多数的渲染。当显卡完成渲染时,渲染序列创建的工作就完成了。

每个Direct3D API调用必须经过每个组件(runtime, 驱动,显卡)处理来渲染任何东西。


多个处理器同时控制组件


这些组件间的关系更加复杂,因为应用程序,runtime,驱动来一个处理器控制,而显卡是被另一个独立的处理器控制。下面的框图现实了两种不同类型的处理器:一个中央处理器单元(CPU)和一个图形处理器单元(GPU)。

精确丈量Direct3D API调用(一)

PC系统拥有至少一个CPU和一个GPU,但是也可以超过一个。CPU位于主板上,GPU在主板上或者显卡上都有。CPU的速度是由主板上的时钟芯片来决定,GPU的速度则是由另外一个独立的时钟芯片来决定。CPU时钟控制应用程序、runtime和驱动工作的速度。应用程序通过runtime和驱动发送工作到GPU。


CPU和GPU通常独立地以不同速度运行。GPU可能在得到工作时就立即对工作进行反馈 (假定GPU已经完成处理之前的工作)。如上面的曲线图所示,GPU的工作和CPU的工作是并行处理的。分析器度量的是CPU的性能,而不是GPU。这使得度量变得非常有挑战,因为分析器得出的测量结果包含了CPU时间但是可能没有包含GPU时间。


GPU是为了图形工作特别设计的处理器来降低CPU处理的负载。在现代的显卡上,GPU替代CPU完成了流水线中的大部分的变换和光照工作。这大大地降低了CPU的工作负载,可以留下更多的CPU周期作其他的处理。为了调整图形应用以便得到最高的性能,你需要度量CPU和GPU的性能,平衡者两种类型的处理器的工作。


这份文档没有包含度量GPU性能以及平衡CPU和GPU工作相关的主题。如果你想更好地理解某个GPU(或者特定的视频卡),访问供应商的网站来查询GPU性能的更多信息。相反,这篇文档通过降低GPU的工作到可以忽略的程度来聚焦runtime和驱动所做的工作。这是因为据经验应用程序通常是因为CPU限制而导致的性能问题。


Runtime和驱动优化能够掩盖API测量


Runtime内置了性能优化,它可能会淹没单独某个调用的度量。这里有一个例子说明这个问题。考虑下面的渲染序列

Local Variable

Number of Ticks

start

1792998845094

stop

1792998845102

freq

3579545

你可以将这些值用如下的方法转换为执行API调用的周期数

# ticks = (stop - start) = 1792998845102 - 1792998845094 = 8 ticks
# cycles = CPU speed * number of ticks / QPF
# 4568   = 2 GHz      * 8              / 3,579,545

换句话说,在2 GHz的机器上它需要花费4568个时钟周期来处理SetTexture和DrawPrimitive。你可以将这些值转换成它执行所有调用所需要花费的实际实际。

# ticks = (stop - start) = 1792998845102 - 1792998845094 = 8 ticks
# cycles = CPU speed * number of ticks / QPF
# 4568   = 2 GHz      * 8              / 3,579,545
使用QueryPerformanceCounter需要你在渲染序列的开始和结束处增加度量并且使用QueryPerformanceFrequency将差值(滴答数)转换为CPU时钟周期数或实际时间。确定测量技术是开发一个定制的测量应用的好的开始。但是在深入测量之前,你需要知道如何和视频卡打交道。

关注于CPU度量


如前面所提到的,CPU和GPU并行处理API调用产生的工作。一个实际的应用需要度量两种类型的处理器来找出你的应用程序是CPU限制还是GPU限制。因为GPU性能是供应商指定的,因此这篇文章的结果不可能涵盖各种可能的显卡。


相反,这篇文章仅关注于测量CPU所做的工作,通过定制测量技术来测量runtime和驱动所做的工作。GPU的工作将会被降低到一个不显著的程度,以便CPU的结果更加明显。这种方法的一个好处就是在附录中用这种技术产生的结果会和你的测量结果能够一致。为了降低视频卡的工作量到一个不显著的水平,简单的方法是减少渲染工作到最小的程度。这个可以通过仅仅渲染一个三角形达到,同时进一步限制每个三角形仅仅包含一个像素。


在这篇文章度量CPU工作采用的是CPU时钟周期数而不是实际时间作为测量单元。对于具有不同CPU速度的机器,CPU时钟比实际时间更通用(对于CPU限制的应用)。如果需要的话,它可以轻易地转换为实际时间。

这篇文档没有包含平衡CPU和GPU之间的工作负载的话题。记住,这篇文章的目的不是如何测量一个应用程序的全面的性能,而是告诉你精确测量runtime和驱动处理API调用需要花费的时间。有了这些精确测量,你可以估算在特定性能场合下CPU能够完成的任务。


CPU的工作能够分解为3块:应用程序工作、runtime工作以及驱动工作。之所以忽略应用程序工作是因为它在程序员控制下。从应用程序的角度,runtime和驱动是黑盒,因为应用程序对于它们没有办法控制。关键是去理解runtime和驱动中可能应用的优化技术。如果你不了解这些优化,基于分析测量器你很容易得出GPU工作量的错误结果。特别的,和指令缓冲区相关的两个内容很容易使测量困惑,这两个主题是:

和指令缓冲区相关的runtime优化。指令缓冲区是一种runtime优化策略来降低模式转换带来的影响。  忽略指令缓冲区的时间消耗。模式转换消耗的时间对于测量分析器具有很大的影响。

控制指令缓冲区


当一个应用程序调用了一个API,runtime将这个API转变为硬件无关的格式(我们称之为指令),并且将它存储在指令缓冲区。指令缓冲区添加到如下的框图中了

精确丈量Direct3D API调用(一)

每次应用程序产生一个API调用,runtime重复这个过程并添加一个指令到指令缓冲区中。在某个时间点,runtime清空这个缓存(发送这些指令到驱动)。在Windows XP中,清空指令缓冲区会导致模式转换,因为操作系统从runtime(runtime在用户态)到驱动(驱动在内核态),如下图所示。

用户态-非特权处理器状态,执行应用程序代码。用户态应用程序不能访问系统数据除非通过系统服务。内核态-特权处理器状态,基于Windows运行的执行代码。运行于内核态的驱动或者线程能够访问所有的系统内存,直接访问硬以及CPU指令来实施硬件IO。

精确丈量Direct3D API调用(一)

每次CPU从用户态转换到内核态(反之亦然),相比于单独的API调用它需要大量的时钟周期。如果每个API调用runtime都将其发送到驱动,那么每个API调用都会遭受模式转换造成的代价。


相反,设计指令缓冲区这种runtime优化就是为了降低模式转换的有效代价。指令缓冲区队列化很多驱动指令而做一次模式转换。当runtime增加一个指令到指令缓冲区,控制权交还给应用程序。分析器没有方法知道驱动指令还没有发送给驱动。因此,现成的测量分析器返回的结果是错误的因为它仅仅测量了runtime的工作而没有测量相关的驱动的工作。


没有模式转换的测量结果


使用例2中的渲染序列,这里是一些典型的计时测量结果来说明模式转换的工作量。假定SetTexture和DrawPrimitive并没有导致模式转换,一个现成的测量分析器可能返回类似如何的结果:

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900

每个数字代表的是runtime将这个调用加入到指令缓冲区中需要花费的时间。因为没有模式转换,驱动还没有工作。这个度量结果是精确的,但是它没有测量渲染序列最终会使CPU执行的所有工作。


有模式转换的测量结果


Number of cycles for SetTexture           : 98 
Number of cycles for DrawPrimitive        : 946,900

SetTexture的测量时间是相同的。然而,DrawPrimitive花费时间的戏剧性的增长原因在于模式转换,下面是发生的事情:

1. 假定指令缓冲区在我们想渲染序列开始前仅有一个空间。

终上所述这些结果,你可以看到:

DrawPrimitive = kernel-transition + driver work    + user-transition + runtime work
DrawPrimitive = 5000              + 935,000 + 2750 + 5000            + 900
DrawPrimitive = 947,950  

类似于没有模式转换的DrawPrimitive的测量(900时钟周期),有模式转换的DrawPrimitive的测量(947950时钟周期)是精确的但是对于CPU工作的预算是没有任何帮助的。这个结果中包含了正确的runtime工作、驱动工作、先于SetTexture的任何指令以及两个模式转换。然而,测量缺少了DrawPrimitive的驱动工作。


查询机制


微软Direct3D9 中的查询机制是为了runtime查询GPU的进度并从GPU返回特定的数据而设计的。在分析时,如果GPU的工作被最小化了,对性能的影响可以忽略。毕竟,当GPU看到这些驱动指令时驱动工作已经完成。另外,查询机制能够控制对分析器非常重要的两个指令缓冲区特性:什么时候指令缓冲区清空以及在缓冲器中的工作量。


这里是使用查询机制的相同的渲染序列

Local Variable

Number of Ticks

start

1792998845060

stop

1792998845090

freq

3579545

再次转换滴答数为时钟周期(在一个2GHz的机器上)
# ticks  = (stop - start) = 1792998845090 - 1792998845060 = 30 ticks
# cycles = CPU speed * number of ticks / QPF
# 16,450 = 2 GHz      * 30             / 3,579,545
这里是分解之后每个调用的时钟数:
Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900
Number of cycles for Issue                : 200
Number of cycles for GetData              : 16,450
查询机制允许我们控制被测量的runtime和驱动的工作。为了理解这些数字,这里是每个API调用发生的事情及其估计。
    带D3DGETDATA_FLUSH的第一次GetData调用会清空指令缓冲区。当GPU完成处理指令缓冲区中的工作时,GetData返回S_OK,循环结束了因为GPU已经空闲渲染序列开始时转换SetTexture为硬件无关格式并将其加入到指令缓冲区。假定这花费100时钟周期。转换DrawPrimitive,并加入指令缓冲区。假定这花费900时钟周期。Issue增加一个查询标志到指令缓冲区。假定这花费200时钟周期。GetData清空指令缓冲区导致内核态模式的状态,假定这花费5000时钟周期。驱动接着处理这4个调用相关的工作。假定驱动处理SetTexture大约2964个时钟周期,DrawPrimitive大约3600个时钟周期。Issue大约200个时钟周期。所以所有4个指令总的驱动时间是6450个时钟周期。注意驱动也需要花费一点时间来看当前GPU是什么状态。因为GPU的工作量很小,GPU应该已经完成了。GetData将返回S_OK基于GPU已经完成的近似。当驱动完成了它的工作,用户态转换返回控制权给runtime,指令缓冲区现在为空。假定这花费5000时钟周期。
GetData包含:
Data = kernel-transition + driver work + user-transition
GetData = 5000              + 6450        + 5000           
GetData = 16,450  
 
driver work = SetTexture + DrawPrimitive + Issue = 
driver work = 2964       + 3600          + 200   = 6450 cycles 

 

采用QueryPerformanceCounter,查询机制测量了CPU的所有工作。这是通过一个查询标记和一个查询状态比对完成的。起始和结束查询标记加入到指令缓冲区中来控制缓冲区的工作量。通过等待正确的返回码返回,测量的开始正好位于一个干净的渲染序列开始之前,测量的结束正好位于驱动已经完成了指令缓冲区内容相关的工作。这样有效的获取了runtime和driver所做的工作。


现在你已经知道了指令缓冲区以及它在分析器中的效果,你应该知道有少量的其他条件能够导致runtime清空指令缓冲区。你应该小心你的渲染序列。其中的一些对应于API调用,其他对应于runtime中的资源的变化。下面条件中的任何一个将导致模式转换:

 当顶点缓冲区、索引缓冲区或者纹理缓冲区中的任何一个Lock方法调用时当一个设备、顶点缓冲区、索引缓冲区或者纹理创建时 当一个设备、顶点缓冲区、索引缓冲区或者纹理被最后一个release销毁时 当ValidateDevice调用时当Present调用时当指令缓冲区满当带有D3DGETDATA_FLUSH的GetData调用

小心查看你的渲染序列中的这些条件。每次增加一个模式转换,10000时钟周期的驱动工作就会增加到你的测量结果中。另外,指令缓冲区不是静态的大小。runtime可能根据应用程序产生的工作量来动态决定缓冲区的大小。这是依赖于渲染序列的另一个优化。

因此,在测量分析中小心控制模式转换。查询机制提供了一种鲁棒的方法来清空指令缓冲区。于是,你既可以控制模式的转换的次数也可以控制缓冲区包含的工作量。然而,这种技术也可以通过减少模式转换的时间使其相对于测量结果不显著来提升。


使渲染序列相对于模式转换更大


在前面的例子中,内核态模式转换和用户态模式花费大约10000时钟周期,而这个和runtime和驱动的工作无关。因为模式转换是内置于操作系统中,它不能减少到0.为了使模式转换不显著,需要调节渲染序列使得runtime的工作比模式转换大一个数量级。你可以尝试减少来消除转换,但是在一个更大的渲染序列上花费均摊更可信。


减少模式转换直到它变得不显著的策略是给渲染序列增加一个循环。例如,让我们看看增加一个循环使渲染序列循环1500次。

Local Variable

Number of Tics

start

1792998845000

stop

1792998847084

freq

3579545

现在使用QueryPerformanceCounter测量2840滴答。将滴答转换为时钟周期和我们的展示结果相同。

# ticks  = (stop - start) = 1792998847084 - 1792998845000 = 2840 ticks
# cycles    = machine speed * number of ticks / QPF
# 6,900,000 = 2 GHz          * 2840           / 3,579,545
换句话说,在一个2GHz的机器上它花费6900000时钟周期来处理渲染序列中的1500次调用。在6900000时钟周期中模式转换的时间大约10000,因此这个测量的结果几乎是全部是和SetTexture和DrawPrimitive相关的工作。
 
注意到,代码例子需要两个纹理一组。为了避免优化:如果每次调用设置同样的纹理它可能会删掉SetTexture。可以简单使用两个纹理一组。在那种方式,每次循环中的遍历,纹理指针发生了变化,SetTexture相关的全部工作都会施行。确保两个Texture有同样的大小和格式,所以没有其它的状态会发生变化。
到目前为止,这篇文章已经展示了如何测量一个渲染序列。每个渲染序列相当的简单,包含一个简单的DrawPrimitive调用和一个SetTexture调用。这样子是为了聚焦于指令缓冲区以及使用查询机制来控制它。这里是测量任意一个渲染序列简单小结。
使用类似于QueryPerformanceCounter的高精度定时器来测量每个API调用花费的时间。使用QueryPerformanceFrequency和CPU 时钟频率来转换为每个API调用的时钟周期数。 最小化渲染三角列表中的GPU工作量,每个三角形仅仅包括一个像素。 通过查询事件标记来控制加入到指令缓冲区总的工作量。这种查询机检测到GPU完成了它的工作。因为GPU工作非常小,这等价于测量驱动工作完成的时间。
所有的这些技术都是用来测量状态变化。假定你已经阅读并且理解了如何控制指令缓冲区,并成功地完成DrawPrimitive的基本测量,你可以在你的渲染序列增加状态变化。当增加状态到渲染序列中时可以有一些其他的测量挑战。如果你打算增加状态变化到你的渲染序列,确保继续阅读下一章节。

热点排行