五,额外章节,使用技巧和并行处理来提高性能
最后更新于:2022-04-01 10:02:46
译者前言:
本文译自[MSDN](http://blogs.msdn.com/b/davrous/archive/2013/06/13/tutorial-series-learning-how-to-write-a-3d-soft-engine-from-scratch-in-c-typescript-or-javascript.aspx),原作者为[David Rousset](https://social.msdn.microsoft.com/profile/david%20rousset/),文章中如果有我的额外说明,我会加上【译者注:】。
正文开始:
在之前的教程中,我们学习了如何通过C#、TypeScript、JavaScript使用光栅化和深度缓冲来填充我们的三角形。由于我们的3D软件渲染引擎使用的是CPU运算,因此它将耗费大量的CPU处理时间。不过倒是有一个好消息,那就是CPU大多是多核心的。那么,我们可以想象一下使用并行处理来提高引擎性能。不过我们只能在C#中这么做,至于为什么Html5不可以,我将会稍后做出解释。我们在此篇章中可以学到一些简单的技巧,以此达到优化渲染性能的目的。而且实际上,我们也得到了从5 FPS提升到50 FPS的结果,这可是10倍的性能提升!
本章教程是以下系列的一部分:
[1 – 编写相机、网格和设备对象的核心逻辑](http://blog.csdn.net/teajs/article/details/49989681)
[2 – 绘制线段和三角形来获得线框渲染效果](http://blog.csdn.net/teajs/article/details/49998675)
[3 – 加载通过Blender扩展导出JSON格式的网格](http://blog.csdn.net/teajs/article/details/50001659)
[4 –填充光栅化的三角形并使用深度缓冲](http://blog.csdn.net/teajs/article/details/50010073)
4b – 额外章节:使用技巧和并行处理来提高性能(本文)
[5 – 使用平面着色和高氏着色处理光 ](http://blogs.msdn.com/b/davrous/archive/2013/07/03/tutorial-part-5-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-flat-amp-gouraud-shading.aspx)
[6 – 应用纹理、背面剔除以及一些WebGL相关](http://blogs.msdn.com/b/davrous/archive/2013/07/18/tutorial-part-6-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-texture-mapping-back-face-culling-amp-webgl.aspx)
**计算FPS**
首先第一步我们需要知道FPS,这是性能体现的一个重要指标。我们不论在C#、TypeScript或JavaScript中都可以实现FPS的统计。
首先我们需要知道两帧之间的执行间隔时间。
我们要进行循环渲染,在Html5中使用requestAnimationFrame,在XAML中使用Composition.Rendering,它们会自动管理循环时间。
一般而言,最佳间隔时间在 1000(每秒毫秒数) / 60(显示器最大刷新率) 毫秒内,也就是 ≈ 16.666667 毫秒。
这里有一个David和我一起做的[基准测试专题](http://blogs.msdn.com/b/eternalcoding/archive/2013/05/21/benchmarking-a-html5-game-html5-potatoes-gaming-bench.aspx)。
综上所诉,在C#中,添加一个新的XAML TextBlock元素,将其命名为"fps",用于显示当前FPS值:
~~~
DateTime previousDate;
void CompositionTarget_Rendering(object sender, object e)
{
// Fps
var now = DateTime.Now;
var currentFps = 1000.0 / (now - previousDate).TotalMilliseconds;
previousDate = now;
fps.Text = string.Format("{0:0.00} fps", currentFps);
// Rendering loop
device.Clear(0, 0, 0, 255);
foreach (var mesh in meshes)
{
mesh.Rotation = new Vector3(mesh.Rotation.X, mesh.Rotation.Y + 0.01f, mesh.Rotation.Z);
device.Render(mera, mesh);
}
device.Present();
}
~~~
我在我的联想 Carbon X1 Touch(分辨率为1600x900)电脑上运行前面的C#版示例,每秒5 FPS。我的联想电脑CPU为Intel Core I7-3667U(4个逻辑处理器)以及一个HD4000核心显卡。
**优化和并行处理策略**
WinRT应用程序使用.Net Framework4.5版本,其中默认(TPL)包含了任务并行库(Task Parallel Library)。如果你留心写算法的方式,并且你的算法可以并行处理,那么使之并行化处理将变得非常容易。如果你不了解这个概念,可以先看看[并行处理的.Net Framework4: 入门](http://blogs.msdn.com/b/csharpfaq/archive/2010/06/01/parallel-programming-in-net-framework-4-getting-started.aspx)
**避免接触UI控件**
多线程/多任务的第一条规则是没有一点关于UI控件代码存在。只有UI线程可以接触/操控图形控件。但在我们的例子中,有一段代码访问到bmp.PixelWidth和bmp.PixelHeight。bmp是WriteableBitmap类型,它被认为是一个UI元素,而不是线程安全的。这就是为什么,我们需要先修改这些代码才能使他们“并行”。在之前的教程中,我们就已经开始这么做了,那就是将bmp中的PixelWidth和PixelHeight的引用全部更改为renderWidth和renderHeight就可以了。
顺便说一句,这个规则不仅是并行处理所必须的,也是一般的性能优化。因此,通过简单的缓存机制把WriteableBitmap转移到我们的变量中便可以达到从5 FPS提升到45 FPS!
在Html5中这个规则也是非常重要的。你应该避免直接访问DOM元素的属性,因为DOM操作非常慢。所以,它并不以16毫秒每帧的运行速度在运行。我们最好始终缓存接下来要使用到的值。不过我们已经在之前的教程中这么做了。
**自给自足**
第二个原则是,代码将在多个可用核心中自给自足。你的代码不能等待很长时间,否则将抵消并行处理的优势。不过幸运的是,在我们的例子中,一直遵循着这个规则。
你可能已经看到,我们有几个地方用Parallel.For替换了经典的循环以此来并行处理。
第一个例子是DrawTriangle方法中。我们将用并行处理用几条线画三角形。在此,你可以很容易的将2个正常循环替换为Parallel.For循环:
~~~
if (dP1P2 > dP1P3)
{
Parallel.For((int)p1.Y, (int)p3.Y + 1, y =>
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p3, p1, p2, color);
}
else
{
ProcessScanLine(y, p1, p3, p2, p3, color);
}
});
}
else
{
Parallel.For((int)p1.Y, (int)p3.Y + 1, y =>
{
if (y < p2.Y)
{
ProcessScanLine(y, p1, p2, p1, p3, color);
}
else
{
ProcessScanLine(y, p2, p3, p1, p3, color);
}
});
}
~~~
但是,在我的情况下,输出的结果有一点点出乎意料。我的性能被降低了,竟然从45 FPS跌到了40 FPS!那么,是什么原因导致了这个问题呢?
那么,出现这样的情况,显然不是使用并行处理就够了。我们需要花更多的时间用于上下文切换,也就是从一个核心移动到另一个核心而不是真正并行处理。你可以使用Visual Studio 2012的嵌入式分析工具:[Visual Studio 2012并行可视化](http://msdn.microsoft.com/en-us/library/dd537632.aspx)来查看。
下面是第一种并行方式的核心利用图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0e98a02e39.jpg)
各种颜色都与工作线程有关,这真的没有效率可言。下面再看看**非并行版本**的核心利用图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0e98a216a8.jpg)
我们只有一个线程在工作(绿色的),所显示的是由操作系统派出的一些核心。即使我们不明确使用并行,CPU也会自动使用多核心处理。显然我们的第一种并行方案上下文切换太频繁了。
**互斥锁和适当循环的并行处理**
嗯,我猜你和我得出了同样的结论。并行化的drawTriangle循环方案似乎并没有成为一个很好的选择。我们需要另外的东西来让线程切换更加高效。我们要并行处理多个三角形,而不是绘制一个三角形。总之,每一个核心都要绘制一个完整的三角形。
这种方法的问题将会在PutPixel方法中出现。现在我们要并行处理几个面,我们可能会陷入2个内核/线程试图并行访问同一个像素的情况。这是,我们只需要保护要访问的像素就可以了。事实上,如果我们用更多时间去保护正在工作中的数据,并行化将会非常有用。
解决的办法是使用一个锁:
~~~
private object[] lockBuffer;
public Device(WriteableBitmap bmp)
{
this.bmp = bmp;
renderWidth = bmp.PixelWidth;
renderHeight = bmp.PixelHeight;
// 后台缓冲区大小值是要绘制的像素
// 屏幕(width*height) * 4 (R,G,B & Alpha值)
backBuffer = new byte[renderWidth * renderHeight * 4];
depthBuffer = new float[renderWidth * renderHeight];
lockBuffer = new object[renderWidth * renderHeight];
for (var i = 0; i < lockBuffer.Length; i++)
{
lockBuffer[i] = new object();
}
}
// 调用此方法把一个像素绘制到指定的X, Y坐标上
public void PutPixel(int x, int y, float z, Color4 color)
{
// 我们的后台缓冲区是一维数组
// 这里我们简单计算,将X和Y对应到此一维数组中
var index = (x + y * renderWidth);
var index4 = index * 4;
// 使用锁来保护缓冲区不被并行处理扰乱
lock (lockBuffer[index])
{
if (depthBuffer[index] < z)
{
return; // 深度测试不通过
}
depthBuffer[index] = z;
backBuffer[index4] = (byte)(color.Blue * 255);
backBuffer[index4 + 1] = (byte)(color.Green * 255);
backBuffer[index4 + 2] = (byte)(color.Red * 255);
backBuffer[index4 + 3] = (byte)(color.Alpha * 255);
}
}
~~~
使用第二种方法,从45 FPS提升到了53 FPS。你可能会认为这点性能提升并不能让人影响深刻。但是在接下来的教程中,drawTriangle方法将更加复杂,比如处理阴影和照明之类的,并行处理几乎可以得到两倍的性能提升。
我们还可以看看这个并行处理方法分析的核心利用图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-03-22_56f0e98a3d654.jpg)
比较以前的核心图,你就会明白为什么它更加高效了。
你可以在这里下载含有这种优化的C#版本解决方案:
- C#:[SoftEngineCSharpPart4Bonus.zip](http://david.blob.core.windows.net/softengine3d/SoftEngineCSharpPart4Bonus.zip)
那么,在Html5/JavaScript中为什么不能应用这种方法呢?
Html5在JavaScript中卫开发人员提供了一个新的API用于处理类似的情况。它叫Web Workers,使用它可以处理使用多核心的情况。
David Catuhe和我已经在这三篇话题中讨论过这个东西:
- [介绍Html5的Web Workers:JavaScript的多线程方法](http://blogs.msdn.com/b/davrous/archive/2011/07/15/introduction-to-the-html5-web-workers-the-javascript-multithreading-approach.aspx):如果你还不知道Web Workers,那么你应该先阅读这篇文章
- [使用Web Workers,用于提高图像处理性能](http://blogs.msdn.com/b/eternalcoding/archive/2012/09/20/using-web-workers-to-improve-performance-of-image-manipulation.aspx):一篇很有意思的文章,我们使用Web Workers来获得像素操作的性能提升
- [教程系列:在Windows 8中使用WinJS和WinRT构建一个有趣的Html5摄像头应用程序](http://blogs.msdn.com/b/davrous/archive/2012/10/02/tutorial-series-using-winjs-amp-winrt-to-build-a-fun-html5-camera-application-for-windows-8-4-4.aspx):在这个教程中使用Web Workers来进行摄像头拍摄的图像效果处理
我们使用message与workers进行沟通。这意味着大多数时间被用于从UI线程给workers线程传输拷贝数据。我们只有很少部分的类型可以使用。事实上,随着[转换对象](http://www.w3.org/html/wg/drafts/html/master/infrastructure.html#transferable-objects),当转移到workers时原始对象将从呼叫者上下文(UI线程)中清除。
但是在我们加速Html5的3D软件渲染引擎时,这并不是主要的问题。在发送数据的时候将有*memcpy()*进行操作,这是一个非常快速的过程。但真正的问题是,当workers完成了处理后,需要将结果发送回主线程而且这个主线程还需要将数据重新填充回像素数组中。这个操作将很简单的抵消掉所有Web Workers带来的性能增益。
所以最后,我还没有找到一个有效的并行处理方法来实现Html5中3D软件渲染引擎的性能提升。不过,可能我有一些东西还不知道。如果您可以解决当前Web Workers的局限,获得到一个显著的性能提升的话,我非常乐意接受建议! :)
在接下来的教程中,我们将讨论[平面着色和高氏着色](http://blogs.msdn.com/b/davrous/archive/2013/07/03/tutorial-part-5-learning-how-to-write-a-3d-software-engine-in-c-ts-or-js-flat-amp-gouraud-shading.aspx)。我们的实现将开始真正耀眼! :)