优化Unity渲染器
来源:互联网 发布:耽美网络剧美拍 编辑:程序博客网 时间:2024/04/30 15:27
引用:l SetPassWithShader有些时候为避免持久指针解引用(PPtr deref)所做的优化。现在看来它总是执行持久指针解引用(PPtr deref),然后调用SetPass(它会再执行一次解引用(deref))。l 材质显示列表经常被>1的逐像素光源重建。每个材质需要缓存不止一个列表(周期性的Kaspar的5bce615bca6b)l GetTextureDecodeValues被调用了多次(设置像素光属性),最后做了一些无用的对“1”的伽马线性转换。l 未赋值的全局纹理属性(_Cube,没有设置任何物体)导致的蝴蝶效应,经常使材质显示列表被重建。应该弄清为什么在任何全局属性丢失的时候,我们都没有记录。l GpuProgramParameters::MakeReady实际做了什么?它为什么要排序?l 为什么在属性表中使用了STL容器map?n 仅使用健壮的简单的数据层。n 相关的奇怪问题u 为什么从PropertySheet中分离出TexEnv(但是两者还被关联在一起)u SetRectTextureID——为什么,是什么u 纹理的纹理尺寸/高动态光照渲染(TexelSize/HDR )解码了内置设备状态的属性部分。u NotifyMipBiasChanged 听起来像是大量复杂操作,且目的不明。l IsPassSuitable 被渲染循环重复调用。可能是为每个渲染循环建立了一个直接pass指针的表?l 一次性应用所有纹理,而不是通过SetTexture一次一个的设置。l 在内存中重排属性表,使其和实际常量缓存布局一致。对不同的关键字变量可能需要不同的布局。l TextureID转换为长整型(在mac/linux 64位机上占64bit!),针对PS4平台提出的3cbd28d4d6cdn 看起来这是一个仅针对PS4的优化,它直接在TextureID中存储了一个指针。考虑如果可能的话再在各处执行该操作,或者只在PS4上将它转为长整型(intptr_t 更好)l 为什么ChannelAssigns总是被传递?这似乎没什么用。l ChannelAssigns和VertexComponent尤其诡异(太多可能的VertexComponent实体)。l 渲染循环排序成本太高。使用基于哈希表的排序(渲染循环排序)并启用它!
经过对几个项目的性能分析,主要暴露了两个问题:
1)渲染代码实际可以使用更多的多线程,而不仅仅是我们现在使用的“主线程和渲染线程”。这有一张unity5性能分析器中时间轴的截图:
在这种特殊情况下,CPU的瓶颈是渲染线程,时间主要耗费在glDrawElements(这是运行在MacBookPro上,场景来源于ButterflyEffect demo,GPU已经简化,draw call达到6000左右)。主线程结束,等待渲染线程完成后再继续。这和硬件、平台、图形API等有关。瓶颈也可以是其他地方,例如,同一个项目运行在更快的电脑的DX11下,主线程和渲染线程花费的时间是相等的。
左边代表剔除的颜色条的看起来很好,我们希望最终所有的渲染代码也可以是这样的。下面是剔除部分的放大图:
2)没有“优化一个函数就可以使所有事情快一倍”的地方:( 这是一个漫长的过程:重排数据、移除冗余决策、移除这里的小操作,直到我们可以达到如同“每个线程有从前两倍快”的状态。如果可以的话。
渲染线程的性能分析数据并不太让人感兴趣。主要的时间开销(下面高亮标注的内容)来自于OpenGLobal运行时/驱动。顺便说一下,可能是因为我们做了什么愚蠢的事,导致驱动做了太多工作(我不太清楚,可能是不同顶点布局间做了多余的切换等。),但是其他没有太多是我们的原因。剩下的大部分时间花在了 动态批处理。
分析那些在主线程中耗时的操作,我们得出:
现在出现了一些问题(为什么有这么多哈希表查找?为什么排序这么慢?等等,参见上面的列表),但是关键是,没有单独哪个地方进行了优化之后就能得到奇迹般的性能提升。
观测点1:材质“显示列表(display lists)”被频繁重建
在我们的代码中,一个材质可以为渲染线程预先记录被称为“显示列表(display list)”的东西。它可以被视为一个小的命令缓冲区,其中存储了一批命令(“设置此光栅状态对象,设置此着色器,设置这些纹理”)。最重要的是:他们存储了所有解析过的参数(最终的纹理值,着色器统一值等)。当“应用”一个显示列表时,我们只是把他传递给渲染线程,不需要查找材质属性值或其他事情。
这一切都很好,除了当材质中的一些东西改变时,会使记录的显示列表无效。在Unity中,每个着色器内部往往是 许多着色器的变种,当切换到不同的着色器变种时,我们需要应用不同的显示列表。如果场景以这种方式运行,会导致相同材质进行不同列表间的切换,那么,就出现问题了。
这些情况确实在我的几个基准项目发生了;长话短说就是“多个逐像素光源在前向渲染中引起了这个问题”。结果是,我们已经在一些分支上修复了这个问题,只需要补全它——所以我找到了它,在当前的代码库中编译,它能够正常工作。现在材质可以预先记录不止一个“显示列表”,这个问题不会再出现了。
在Windos的电脑上(酷睿i7 5820K),一个场景在主线中花费的时间从9.52ms降低到7.25ms,这是相当出色的。
提前透露:在我这将近两个星期的工作中,这一修改,给受影响的场景带来的好处是最大的。而且,这甚至不是“我写”的代码;我只是从某个被忽略的分支上找出了它。所以,哈!一个简单的修改带来了30%的性能提升!不过下面所写的内容将不会再有这种修改。
观测点 2:过多的哈希表查找
接下来,从上述的观测列表中,研究一下“为什么有这么多哈希表查找”的问题(如果还没有a song about it,那么应该有!)
在渲染代码中,很多年前我添加了这样一段
[mw_shl_code=csharp,true]Material::SetPassWithShader(Shader*shader, ...) [/mw_shl_code]
因为调用代码已经知道应该设置哪个着色器材质状态。材质也知道它的着色器,但是它存储了一些被我们称为PPtr(“持久指针(persistent pointer)”)的东西,它本质上是一个句柄。直接传递这种指针可以避免进行句柄->指针查找(目前这是一个哈希表查找,因为很多复杂的原因,很难做到基于数组的处理系统,我之前已经讲过了)。
结果是,经过很多变化之后,不知为何,Material::SetPassWithShader以进行两次句柄->指针查找而告终,即使它已经以参数形式获取到了实际着色器指针!修复它:
引用:[着色器库]清理并优化材质pass的应用SetPassWithShader被添加到需要避免持久指针解引用(PPtr deref)的地方。结果,现在它总是进行*两次*m_Shader 持久指针解引用。这不是个可怕的优化。所以清理掉它们;使Material.SetShaderPass直接持有它需要的pass,避免解引用并进入子着色器(subshader)。只缓存直接基于pass的指针;这允许移除特殊情况下针对阴影caster pass的重复代码。VikingVillageStatic工作台项目,酷睿i75820K,主线程:Camer.Render 7.25ms -> 6.59ms。修改17个文件,135处增加和213处删除:
好了,所以这个问题也解决了,可衡量的而且很简单的性能优化。这也使得代码库更小了,这是一件很好的事。
各处的细微调整
从上述Mac机的渲染线程性能分析中可以看出,我们自己的代码在 BindDefaultVertexArray 操作上花费了2.3%的时间,这个开销听来些过大了。原来,它遍历了所有可能的顶点组件类型并检查了一些东西;让代码仅遍历被着色器使用的顶点组件。这样稍快了些。
一个项目调用很多次 GetTextureDecodeValues ,为纹理计算一些颜色空间、HDR(高动态光照渲染)和光照贴图的解压常量。它根据一个可选的“强度倍增”(“intensity multiplier”)参数,做了一批复杂的sRGB数学运算,但该参数在所有的调用点都被设置为1.0,只有一处除外。认识到这一点,就可以消除代码中大量的pow()调用了。添加到“关注”列表:为什么我们起初非常平常频繁的调用这个函数?
一些代码,在渲染循环中计算了绘制调用(draw call)批处理的边界应该位于何处(即,在哪里切换一个新的着色器等。),比较了一些用独立的布尔值表示的状态。将它们打包为一组bit域,然后以整数进行比较。没有获得明显的性能提升,但是代码最终变少了,所以也算一个胜利:)
注意,弄清哪些被物体使用的顶点缓存和顶点布局,查询了远在内存中的网格数据。根据使用类型(渲染数据、碰撞数据、动画数据等)记录数据。
而且使用@msinilo’s 出色的 CruncherSharp来减少数据填充间隙(并按照这种方式做了一些调整:)),听说有一个针对Linux(pahole)的小工具。在Mac机上有struct_layout,但它处理Unity的可执行脚本以及Python脚本时,经常会因为递归溢出而失败。
浏览代码时注意到,我们跟踪每个纹理的mipmap偏置的方式,客气点说,很令人费解。它被设置每个纹理,然后纹理跟踪所有的材质属性表在哪里被使用;在任何mip偏差改变的时候通知它们,并从属性表中取出偏差,和每个纹理一起被应用,每次一个纹理被设置到图形设备。天呐,修复它。因为这个修改了我们图形API的抽象接口,这意味着要修改共计11个渲染后端;每个都只是一些细微的修改,但却让人觉得恐慌(我甚至不能局部地构建其中的一半)。不用怕,我有build farm来检查编译错误,有测试套件来检查递归!
引用:[图形加速设备]使纹理的mip偏置的处理更健全如我所言,这很奇怪,因为我想要处理OpenGLpre-1.4(2002!?),其中偏置是纹理层的一部分,而不是纹理的。或者可能时其他的什么。现在它只是纹理过滤器/重复(wrap)/各向异性(aniso)设置的一部分,只在它改变时被应用。修改:偏置应用到每个gfxdevice.SetTexture调用,Texture::NotifyMipBiasChanged,TexEnv::TextureMipBiasChanged,TexEnvData::mipBias.80个文件修改,228出新增,263处删除:
没有显著的性能变化,但是摆脱了所有的复杂性感觉也不错。添加到“关注”列表:我们跟踪每个纹理时,有一些相似数据;一些关于非2的整数次幂纹理的UV缩放的东西。现在我怀疑这没什么存在的必要,继续观察,可能的话将其删除。
还有一些其他类似的局部调整,每个都很简单,使一些特殊的地方变得更好一点,但是没有任何显著的性能提升。进行上百个这种调整可能带来一些明显的效果,但是更可能的是,我们需要更严谨的重新分析这些问题,以得到好的结果。
材质属性表的数据布局
令我烦恼的一件事是如何存储材质。每次我把代码库给一个新的工程师看,我会翻翻白眼说“嗨,我们在这里存储了材质纹理、矩阵、颜色等,在单独的STL映射(map)中。简直惊悚。”。
这种想法很流行:C++的STL容器在高性能代码中没有立足之地,没有好游戏曾经使用过它们(不是真的),如果你使用它你就太愚蠢了,会被人耻笑(我不知道…可能?)。所以,嗨,我用更好的数据布局替代这些映射(map)如何?肯定会让一切都快上百万倍,对吧?
在Unity中,着色器的参数可以来自两个地方:一种来自于每个材质的数据,另一种来自于“全局”着色器参数。前者中典型的是“漫反射纹理(diffuse texture)”,而后者例如“雾的颜色(fog color)”或“相机投影(camera projection)”(每个实例参数都是MaterialPropertyBlock等的形式,这有点复杂,但是现在我们先忽略那些)。
之前我们的数据布局大概是这样(PropertyName基本上都是整型):
map<PropertyName, Vector4f> m_Vectors;
map<PropertyName, Matrix4x4f> m_Matrices;
map<PropertyName, TextureProperty> m_Textures;
map<PropertyName, ComputeBufferID>
m_ComputeBuffers;
set<PropertyName> m_IsGammaSpaceTag; // which properties come as sRGB[/mw_shl_code]
What I replaced it with (simplified, onlyshowing data members; dynamic_array is very much like std::vector, butmore EASTL style):
我把它替换成(简单,只展示数据成员;dynamic_array很像std::vector,但是更有EASTL 风格):
struct NameAndType { PropertyName name; PropertyType type; };
// Data layout:
// - Array of name+type information for lookups (m_Names). Do
// not put anything else; only have info needed for lookups!
// - Location of property data in the value buffer (m_Offsets).
// Uses 4 byte entries for smaller data; don't use size_t!
// - Byte buffer with actual property values (m_ValueBuffer).
// - Additional data per-property in m_GammaProps and
// m_TextureAuxProps bit sets.
//
// All the arrays need to be kept in sync (sizes the same; all
// indexed by the same property index).
dynamic_array<NameAndType> m_Names;
dynamic_array<int> m_Offsets;
dynamic_array<UInt8> m_ValueBuffer;
// A bit set for each property that should do gamma->linear
// conversion when in linear color space
dynamic_bitset m_GammaProps;
// A bit set for each property that is aux for a texture
// (e.g. *_ST for texture scale/tiling)
dynamic_bitset m_TextureAuxProps;[/mw_shl_code]
向属性表添加新的属性时,它只是追加到所有的数组。属性名/类型信息和属性在数据缓存中的位置保持独立,以便查找属性时,我甚至不获取对查找本身不需要的数据。
之前,最大的外部变化就是,可以查找一个属性值并存储它的直接指针(用于预先记录材质显示列表,能够在重用显示列表前,“插入”全局着色器属性的值)。现在只要改变数组大小,指针就会失效;所以,所有可能存储了指针的代码,都需要替换为存储数据在属性表中的偏移量。这最后导致了相当多的代码修改。
查找属性的复杂度从O(logN)ss(映射查找)变成了O(N)(从名称数组中线性扫描)。如果你正在学习计算机科学中一般理论,那这种情况听起来并不太好。但是,我查看了各种项目,发现通常数量表总共只包含5到30个属性(大部分是10个左右);线性扫描内存中所有的彼此相邻的待查询数据,比起STL的映射(map)查找就不那么差了。因为STL中映射(map)的节点的位置可能彼此间隔很远(如果是这样,访问每个节点可能降低CPU缓存的命中)。从几个不同项目的分析数据来看,“查找属性”这部分功能,在电脑、笔记本和iPhone上都稍快了一些。
这个修改带来魔法般的性能提升了吗?没有。它带来的是平均帧时间的小改善,内存消耗略有降低,尤其有一大堆不同的材质时。“只是用封装的数组替换STL映射”有导致魔法般的性能提升吗?也没有,至少向别人展示代码时不必再翻白眼了。所以,就是这些了。
在进行代码审查时,有一种意见是,我应该尝试分离属性数据,以便相同类型的数据可以被分为一组。一个属性表可以知道哪些起始索引和结束索引是针对哪个类型的,那么查找指定属性就只需要扫描那个类型的名字数组(这个数组可能只包含每个属性的名字,而不是名字+类型)。向表中添加新属性可能开销更大,但是查找它们的开销会更小。
这些从侧面说明:现代CPU在那些你视为“坏代码”的地方速度很快,而且可能也有很大的缓存。我没有太关注移动CPU的硬件,只是意识到iPhone6的CPU有4兆字节的L3缓存。4兆字节啊,在一个手机上。这顶我第一台电脑的多少个RAM了!
目前的结果
这就是两周左右的工作(我估计实际只有75%的时间——其他花费在修复无关的bug,代码审查等);目前的状态是,所有的平台都构建并测试通过;下载请求已经就绪。40次提交,135个文件,将近2000行的代码修改。
引用:背景:…-根据v1的公共反馈添加待办事项。节约的时间大部分来自于更好的图形加速设备缓存显示列表,尤其是总在不同关键字间切换材质(例如 VikingsVillage在性能好的PC上,时间变化为12ms->8.5ms)。其他情况下速度稍有提升,但不是特别大(详情参考以上总结页中的谷歌文档链接。)似乎带部分使帧率更加稳定了,可能时因为在运行时内存分配减少了。*在材质中缓存不止一个显示列表。*更好的属性表数据布局(6个std::maps –>3个dynamic_array和两个bitset)。这意味着不能再存储值的指针了;将所有代码改为存储偏移。为属性表添加更多的单元测试!*使纹理的mip偏置处理更健全,现在它只是过滤器/重复(wrap)/各向异性(aniso)的参数,而不再在循环中被跟踪,应用到每个SetTexture调用。*SetPassWithShader是一个为了在某些地方避免持久指针解引用(PPtrderef)的优化,但是现在它总是进行两次解引用!清除掉这些。*在前向循环中光源属性前,移除世界矩阵单位化的设置。看起来没用。*针对线程的显示列表补丁数据的细微优化。*OpenGL:对BindDefaultVertexArray的细微优化(GLES也同样做了优化)。*为倍增值(multiplier)为1的情况增加特定GetTextureDecodeValues。*根据平台floks,在android/Tizen上,网格缓存永远不会丢失。*除了PS4上,其他平台的TextureID变回32bit。*在某些地方使用更紧凑的结构/类成员结构。*从着色器库(shaderlab)中移除无用的内容(如MatrixVal),并在很多地方添加注释。*杂项:在发布的播放器上支持 –datafolder 命令行参数,在mac/linux上也是。
性能方面,一个基准项目改善了很多(最受“显示列表被重建”问题的影响),一帧的总时间,在电脑上从11.8ms降到8.5mc,在笔记本上从29.2ms降到26.9毫秒。其他项目也有改善,但效果远不及这个(大部分在电脑上从7.8ms降到7.3ms;另一个项目在iPhone上从15.2ms降到14.1ms,等等)
大部分性能提升实际来自于两个地方(显示列表重建;避免无用的哈希表查找)。我不知道该如何看待剩下的修改——总体上感觉它们都算是好的,因为现在我更好地理解了代码库,并添加了很多注释来解释是什么&为什么。我现在还有了一个更长的列表“这些地方很奇怪或者应该被改善“。
花费将近两周的时间,对我现在的结果来说是否值得?很难说。有时我真感觉一周什么都没做,所以现在的结果总比这个更好:)
总的来说,我还是不太清楚“优化工作“是否是我擅长的领域。我觉得我至少对这几件事比较擅长:1)调试难题——对问题我可以提出合理的假设和方法快速地分而治之;2)了解一些修改或一个系统的影响——其他系统会受到什么影响,有问题的交互应该或可能是什么;3)对代码库中其他人做的事有很好的总体意识——我经常可以发现几个人正在开发重叠的东西,并告诉他们“嘿,你们两个应该协调工作”。
这些是对优化有用的技能吗?我不知道。我当然不能在脑中同时处理指令延迟、执行端口和TLB缺失。但是如果我实践的话,也许可以让情况更好?谁知道呢。
不太确定下一步该怎么走;但至少有几个可能的方向:
- 继续进行增量改进,希望大量改进的净效果会很好。单个改进让人有些失望,因为性能确实难以衡量。
- 开始着眼大局,并寻找如何避免当前大量的已完成工作,即更严谨地“重塑“代码的结构。
- 一些清理工作完成后,切换到帮助他人处理“多线程的内容“。
- 优化很难!让我们多玩玩 《摇滚史密斯》 (Rocksmith)直到情况改善吧。
我想我会和其他人讨论讨论,进行上面的一项或几项,直到下次!
扫描下方二维码关注游戏蛮牛官方微信~每日都有精选干货与你分享呦~
原文链接:http://aras-p.info/blog/2015/04/04/optimizing-unity-renderer-2-cleanups/
原文作者:Aras Pranckevičius
引用:l SetPassWithShader有些时候为避免持久指针解引用(PPtr deref)所做的优化。现在看来它总是执行持久指针解引用(PPtr deref),然后调用SetPass(它会再执行一次解引用(deref))。l 材质显示列表经常被>1的逐像素光源重建。每个材质需要缓存不止一个列表(周期性的Kaspar的5bce615bca6b)l GetTextureDecodeValues被调用了多次(设置像素光属性),最后做了一些无用的对“1”的伽马线性转换。l 未赋值的全局纹理属性(_Cube,没有设置任何物体)导致的蝴蝶效应,经常使材质显示列表被重建。应该弄清为什么在任何全局属性丢失的时候,我们都没有记录。l GpuProgramParameters::MakeReady实际做了什么?它为什么要排序?l 为什么在属性表中使用了STL容器map?n 仅使用健壮的简单的数据层。n 相关的奇怪问题u 为什么从PropertySheet中分离出TexEnv(但是两者还被关联在一起)u SetRectTextureID——为什么,是什么u 纹理的纹理尺寸/高动态光照渲染(TexelSize/HDR )解码了内置设备状态的属性部分。u NotifyMipBiasChanged 听起来像是大量复杂操作,且目的不明。l IsPassSuitable 被渲染循环重复调用。可能是为每个渲染循环建立了一个直接pass指针的表?l 一次性应用所有纹理,而不是通过SetTexture一次一个的设置。l 在内存中重排属性表,使其和实际常量缓存布局一致。对不同的关键字变量可能需要不同的布局。l TextureID转换为长整型(在mac/linux 64位机上占64bit!),针对PS4平台提出的3cbd28d4d6cdn 看起来这是一个仅针对PS4的优化,它直接在TextureID中存储了一个指针。考虑如果可能的话再在各处执行该操作,或者只在PS4上将它转为长整型(intptr_t 更好)l 为什么ChannelAssigns总是被传递?这似乎没什么用。l ChannelAssigns和VertexComponent尤其诡异(太多可能的VertexComponent实体)。l 渲染循环排序成本太高。使用基于哈希表的排序(渲染循环排序)并启用它!
经过对几个项目的性能分析,主要暴露了两个问题:
1)渲染代码实际可以使用更多的多线程,而不仅仅是我们现在使用的“主线程和渲染线程”。这有一张unity5性能分析器中时间轴的截图:
在这种特殊情况下,CPU的瓶颈是渲染线程,时间主要耗费在glDrawElements(这是运行在MacBookPro上,场景来源于ButterflyEffect demo,GPU已经简化,draw call达到6000左右)。主线程结束,等待渲染线程完成后再继续。这和硬件、平台、图形API等有关。瓶颈也可以是其他地方,例如,同一个项目运行在更快的电脑的DX11下,主线程和渲染线程花费的时间是相等的。
左边代表剔除的颜色条的看起来很好,我们希望最终所有的渲染代码也可以是这样的。下面是剔除部分的放大图:
2)没有“优化一个函数就可以使所有事情快一倍”的地方:( 这是一个漫长的过程:重排数据、移除冗余决策、移除这里的小操作,直到我们可以达到如同“每个线程有从前两倍快”的状态。如果可以的话。
渲染线程的性能分析数据并不太让人感兴趣。主要的时间开销(下面高亮标注的内容)来自于OpenGLobal运行时/驱动。顺便说一下,可能是因为我们做了什么愚蠢的事,导致驱动做了太多工作(我不太清楚,可能是不同顶点布局间做了多余的切换等。),但是其他没有太多是我们的原因。剩下的大部分时间花在了 动态批处理。
分析那些在主线程中耗时的操作,我们得出:
现在出现了一些问题(为什么有这么多哈希表查找?为什么排序这么慢?等等,参见上面的列表),但是关键是,没有单独哪个地方进行了优化之后就能得到奇迹般的性能提升。
观测点1:材质“显示列表(display lists)”被频繁重建
在我们的代码中,一个材质可以为渲染线程预先记录被称为“显示列表(display list)”的东西。它可以被视为一个小的命令缓冲区,其中存储了一批命令(“设置此光栅状态对象,设置此着色器,设置这些纹理”)。最重要的是:他们存储了所有解析过的参数(最终的纹理值,着色器统一值等)。当“应用”一个显示列表时,我们只是把他传递给渲染线程,不需要查找材质属性值或其他事情。
这一切都很好,除了当材质中的一些东西改变时,会使记录的显示列表无效。在Unity中,每个着色器内部往往是 许多着色器的变种,当切换到不同的着色器变种时,我们需要应用不同的显示列表。如果场景以这种方式运行,会导致相同材质进行不同列表间的切换,那么,就出现问题了。
这些情况确实在我的几个基准项目发生了;长话短说就是“多个逐像素光源在前向渲染中引起了这个问题”。结果是,我们已经在一些分支上修复了这个问题,只需要补全它——所以我找到了它,在当前的代码库中编译,它能够正常工作。现在材质可以预先记录不止一个“显示列表”,这个问题不会再出现了。
在Windos的电脑上(酷睿i7 5820K),一个场景在主线中花费的时间从9.52ms降低到7.25ms,这是相当出色的。
提前透露:在我这将近两个星期的工作中,这一修改,给受影响的场景带来的好处是最大的。而且,这甚至不是“我写”的代码;我只是从某个被忽略的分支上找出了它。所以,哈!一个简单的修改带来了30%的性能提升!不过下面所写的内容将不会再有这种修改。
观测点 2:过多的哈希表查找
接下来,从上述的观测列表中,研究一下“为什么有这么多哈希表查找”的问题(如果还没有a song about it,那么应该有!)
在渲染代码中,很多年前我添加了这样一段
[mw_shl_code=csharp,true]Material::SetPassWithShader(Shader*shader, ...) [/mw_shl_code]
因为调用代码已经知道应该设置哪个着色器材质状态。材质也知道它的着色器,但是它存储了一些被我们称为PPtr(“持久指针(persistent pointer)”)的东西,它本质上是一个句柄。直接传递这种指针可以避免进行句柄->指针查找(目前这是一个哈希表查找,因为很多复杂的原因,很难做到基于数组的处理系统,我之前已经讲过了)。
结果是,经过很多变化之后,不知为何,Material::SetPassWithShader以进行两次句柄->指针查找而告终,即使它已经以参数形式获取到了实际着色器指针!修复它:
引用:[着色器库]清理并优化材质pass的应用SetPassWithShader被添加到需要避免持久指针解引用(PPtr deref)的地方。结果,现在它总是进行*两次*m_Shader 持久指针解引用。这不是个可怕的优化。所以清理掉它们;使Material.SetShaderPass直接持有它需要的pass,避免解引用并进入子着色器(subshader)。只缓存直接基于pass的指针;这允许移除特殊情况下针对阴影caster pass的重复代码。VikingVillageStatic工作台项目,酷睿i75820K,主线程:Camer.Render 7.25ms -> 6.59ms。修改17个文件,135处增加和213处删除:
好了,所以这个问题也解决了,可衡量的而且很简单的性能优化。这也使得代码库更小了,这是一件很好的事。
各处的细微调整
从上述Mac机的渲染线程性能分析中可以看出,我们自己的代码在 BindDefaultVertexArray 操作上花费了2.3%的时间,这个开销听来些过大了。原来,它遍历了所有可能的顶点组件类型并检查了一些东西;让代码仅遍历被着色器使用的顶点组件。这样稍快了些。
一个项目调用很多次 GetTextureDecodeValues ,为纹理计算一些颜色空间、HDR(高动态光照渲染)和光照贴图的解压常量。它根据一个可选的“强度倍增”(“intensity multiplier”)参数,做了一批复杂的sRGB数学运算,但该参数在所有的调用点都被设置为1.0,只有一处除外。认识到这一点,就可以消除代码中大量的pow()调用了。添加到“关注”列表:为什么我们起初非常平常频繁的调用这个函数?
一些代码,在渲染循环中计算了绘制调用(draw call)批处理的边界应该位于何处(即,在哪里切换一个新的着色器等。),比较了一些用独立的布尔值表示的状态。将它们打包为一组bit域,然后以整数进行比较。没有获得明显的性能提升,但是代码最终变少了,所以也算一个胜利:)
注意,弄清哪些被物体使用的顶点缓存和顶点布局,查询了远在内存中的网格数据。根据使用类型(渲染数据、碰撞数据、动画数据等)记录数据。
而且使用@msinilo’s 出色的 CruncherSharp来减少数据填充间隙(并按照这种方式做了一些调整:)),听说有一个针对Linux(pahole)的小工具。在Mac机上有struct_layout,但它处理Unity的可执行脚本以及Python脚本时,经常会因为递归溢出而失败。
浏览代码时注意到,我们跟踪每个纹理的mipmap偏置的方式,客气点说,很令人费解。它被设置每个纹理,然后纹理跟踪所有的材质属性表在哪里被使用;在任何mip偏差改变的时候通知它们,并从属性表中取出偏差,和每个纹理一起被应用,每次一个纹理被设置到图形设备。天呐,修复它。因为这个修改了我们图形API的抽象接口,这意味着要修改共计11个渲染后端;每个都只是一些细微的修改,但却让人觉得恐慌(我甚至不能局部地构建其中的一半)。不用怕,我有build farm来检查编译错误,有测试套件来检查递归!
引用:[图形加速设备]使纹理的mip偏置的处理更健全如我所言,这很奇怪,因为我想要处理OpenGLpre-1.4(2002!?),其中偏置是纹理层的一部分,而不是纹理的。或者可能时其他的什么。现在它只是纹理过滤器/重复(wrap)/各向异性(aniso)设置的一部分,只在它改变时被应用。修改:偏置应用到每个gfxdevice.SetTexture调用,Texture::NotifyMipBiasChanged,TexEnv::TextureMipBiasChanged,TexEnvData::mipBias.80个文件修改,228出新增,263处删除:
没有显著的性能变化,但是摆脱了所有的复杂性感觉也不错。添加到“关注”列表:我们跟踪每个纹理时,有一些相似数据;一些关于非2的整数次幂纹理的UV缩放的东西。现在我怀疑这没什么存在的必要,继续观察,可能的话将其删除。
还有一些其他类似的局部调整,每个都很简单,使一些特殊的地方变得更好一点,但是没有任何显著的性能提升。进行上百个这种调整可能带来一些明显的效果,但是更可能的是,我们需要更严谨的重新分析这些问题,以得到好的结果。
材质属性表的数据布局
令我烦恼的一件事是如何存储材质。每次我把代码库给一个新的工程师看,我会翻翻白眼说“嗨,我们在这里存储了材质纹理、矩阵、颜色等,在单独的STL映射(map)中。简直惊悚。”。
这种想法很流行:C++的STL容器在高性能代码中没有立足之地,没有好游戏曾经使用过它们(不是真的),如果你使用它你就太愚蠢了,会被人耻笑(我不知道…可能?)。所以,嗨,我用更好的数据布局替代这些映射(map)如何?肯定会让一切都快上百万倍,对吧?
在Unity中,着色器的参数可以来自两个地方:一种来自于每个材质的数据,另一种来自于“全局”着色器参数。前者中典型的是“漫反射纹理(diffuse texture)”,而后者例如“雾的颜色(fog color)”或“相机投影(camera projection)”(每个实例参数都是MaterialPropertyBlock等的形式,这有点复杂,但是现在我们先忽略那些)。
之前我们的数据布局大概是这样(PropertyName基本上都是整型):
map<PropertyName, Vector4f> m_Vectors;
map<PropertyName, Matrix4x4f> m_Matrices;
map<PropertyName, TextureProperty> m_Textures;
map<PropertyName, ComputeBufferID>
m_ComputeBuffers;
set<PropertyName> m_IsGammaSpaceTag; // which properties come as sRGB[/mw_shl_code]
What I replaced it with (simplified, onlyshowing data members; dynamic_array is very much like std::vector, butmore EASTL style):
我把它替换成(简单,只展示数据成员;dynamic_array很像std::vector,但是更有EASTL 风格):
struct NameAndType { PropertyName name; PropertyType type; };
// Data layout:
// - Array of name+type information for lookups (m_Names). Do
// not put anything else; only have info needed for lookups!
// - Location of property data in the value buffer (m_Offsets).
// Uses 4 byte entries for smaller data; don't use size_t!
// - Byte buffer with actual property values (m_ValueBuffer).
// - Additional data per-property in m_GammaProps and
// m_TextureAuxProps bit sets.
//
// All the arrays need to be kept in sync (sizes the same; all
// indexed by the same property index).
dynamic_array<NameAndType> m_Names;
dynamic_array<int> m_Offsets;
dynamic_array<UInt8> m_ValueBuffer;
// A bit set for each property that should do gamma->linear
// conversion when in linear color space
dynamic_bitset m_GammaProps;
// A bit set for each property that is aux for a texture
// (e.g. *_ST for texture scale/tiling)
dynamic_bitset m_TextureAuxProps;[/mw_shl_code]
向属性表添加新的属性时,它只是追加到所有的数组。属性名/类型信息和属性在数据缓存中的位置保持独立,以便查找属性时,我甚至不获取对查找本身不需要的数据。
之前,最大的外部变化就是,可以查找一个属性值并存储它的直接指针(用于预先记录材质显示列表,能够在重用显示列表前,“插入”全局着色器属性的值)。现在只要改变数组大小,指针就会失效;所以,所有可能存储了指针的代码,都需要替换为存储数据在属性表中的偏移量。这最后导致了相当多的代码修改。
查找属性的复杂度从O(logN)ss(映射查找)变成了O(N)(从名称数组中线性扫描)。如果你正在学习计算机科学中一般理论,那这种情况听起来并不太好。但是,我查看了各种项目,发现通常数量表总共只包含5到30个属性(大部分是10个左右);线性扫描内存中所有的彼此相邻的待查询数据,比起STL的映射(map)查找就不那么差了。因为STL中映射(map)的节点的位置可能彼此间隔很远(如果是这样,访问每个节点可能降低CPU缓存的命中)。从几个不同项目的分析数据来看,“查找属性”这部分功能,在电脑、笔记本和iPhone上都稍快了一些。
这个修改带来魔法般的性能提升了吗?没有。它带来的是平均帧时间的小改善,内存消耗略有降低,尤其有一大堆不同的材质时。“只是用封装的数组替换STL映射”有导致魔法般的性能提升吗?也没有,至少向别人展示代码时不必再翻白眼了。所以,就是这些了。
在进行代码审查时,有一种意见是,我应该尝试分离属性数据,以便相同类型的数据可以被分为一组。一个属性表可以知道哪些起始索引和结束索引是针对哪个类型的,那么查找指定属性就只需要扫描那个类型的名字数组(这个数组可能只包含每个属性的名字,而不是名字+类型)。向表中添加新属性可能开销更大,但是查找它们的开销会更小。
这些从侧面说明:现代CPU在那些你视为“坏代码”的地方速度很快,而且可能也有很大的缓存。我没有太关注移动CPU的硬件,只是意识到iPhone6的CPU有4兆字节的L3缓存。4兆字节啊,在一个手机上。这顶我第一台电脑的多少个RAM了!
目前的结果
这就是两周左右的工作(我估计实际只有75%的时间——其他花费在修复无关的bug,代码审查等);目前的状态是,所有的平台都构建并测试通过;下载请求已经就绪。40次提交,135个文件,将近2000行的代码修改。
引用:背景:…-根据v1的公共反馈添加待办事项。节约的时间大部分来自于更好的图形加速设备缓存显示列表,尤其是总在不同关键字间切换材质(例如 VikingsVillage在性能好的PC上,时间变化为12ms->8.5ms)。其他情况下速度稍有提升,但不是特别大(详情参考以上总结页中的谷歌文档链接。)似乎带部分使帧率更加稳定了,可能时因为在运行时内存分配减少了。*在材质中缓存不止一个显示列表。*更好的属性表数据布局(6个std::maps –>3个dynamic_array和两个bitset)。这意味着不能再存储值的指针了;将所有代码改为存储偏移。为属性表添加更多的单元测试!*使纹理的mip偏置处理更健全,现在它只是过滤器/重复(wrap)/各向异性(aniso)的参数,而不再在循环中被跟踪,应用到每个SetTexture调用。*SetPassWithShader是一个为了在某些地方避免持久指针解引用(PPtrderef)的优化,但是现在它总是进行两次解引用!清除掉这些。*在前向循环中光源属性前,移除世界矩阵单位化的设置。看起来没用。*针对线程的显示列表补丁数据的细微优化。*OpenGL:对BindDefaultVertexArray的细微优化(GLES也同样做了优化)。*为倍增值(multiplier)为1的情况增加特定GetTextureDecodeValues。*根据平台floks,在android/Tizen上,网格缓存永远不会丢失。*除了PS4上,其他平台的TextureID变回32bit。*在某些地方使用更紧凑的结构/类成员结构。*从着色器库(shaderlab)中移除无用的内容(如MatrixVal),并在很多地方添加注释。*杂项:在发布的播放器上支持 –datafolder 命令行参数,在mac/linux上也是。
性能方面,一个基准项目改善了很多(最受“显示列表被重建”问题的影响),一帧的总时间,在电脑上从11.8ms降到8.5mc,在笔记本上从29.2ms降到26.9毫秒。其他项目也有改善,但效果远不及这个(大部分在电脑上从7.8ms降到7.3ms;另一个项目在iPhone上从15.2ms降到14.1ms,等等)
大部分性能提升实际来自于两个地方(显示列表重建;避免无用的哈希表查找)。我不知道该如何看待剩下的修改——总体上感觉它们都算是好的,因为现在我更好地理解了代码库,并添加了很多注释来解释是什么&为什么。我现在还有了一个更长的列表“这些地方很奇怪或者应该被改善“。
花费将近两周的时间,对我现在的结果来说是否值得?很难说。有时我真感觉一周什么都没做,所以现在的结果总比这个更好:)
总的来说,我还是不太清楚“优化工作“是否是我擅长的领域。我觉得我至少对这几件事比较擅长:1)调试难题——对问题我可以提出合理的假设和方法快速地分而治之;2)了解一些修改或一个系统的影响——其他系统会受到什么影响,有问题的交互应该或可能是什么;3)对代码库中其他人做的事有很好的总体意识——我经常可以发现几个人正在开发重叠的东西,并告诉他们“嘿,你们两个应该协调工作”。
这些是对优化有用的技能吗?我不知道。我当然不能在脑中同时处理指令延迟、执行端口和TLB缺失。但是如果我实践的话,也许可以让情况更好?谁知道呢。
不太确定下一步该怎么走;但至少有几个可能的方向:
- 继续进行增量改进,希望大量改进的净效果会很好。单个改进让人有些失望,因为性能确实难以衡量。
- 开始着眼大局,并寻找如何避免当前大量的已完成工作,即更严谨地“重塑“代码的结构。
- 一些清理工作完成后,切换到帮助他人处理“多线程的内容“。
- 优化很难!让我们多玩玩 《摇滚史密斯》 (Rocksmith)直到情况改善吧。
我想我会和其他人讨论讨论,进行上面的一项或几项,直到下次!
扫描下方二维码关注游戏蛮牛官方微信~每日都有精选干货与你分享呦~
原文链接:http://aras-p.info/blog/2015/04/04/optimizing-unity-renderer-2-cleanups/
原文作者:Aras Pranckevičius
0 0
- 优化Unity渲染器
- 优化Unity渲染器
- [Unity] Unity渲染优化
- [Unity 优化]渲染优化
- Unity渲染优化
- unity渲染优化
- 【unity优化】渲染批处理
- unity shader:渲染优化
- unity多线程渲染优化想法。
- 【Unity】图形渲染优化、渲染管线优化、图形性能优化
- unity游戏性能优化之渲染优化
- Unity 线渲染器
- Unity图形渲染优化(二)
- Unity渲染优化中文翻译(一)
- Unity渲染优化中文翻译(一)
- Unity渲染优化中文翻译(一)
- Unity渲染优化中文翻译(一)
- Unity渲染优化中文翻译(一)
- 静态模型的Lightmap(光照贴图)与Vertex-Lighting(顶点光照)比较
- 各种移动GPU压缩纹理的使用方法
- Unity 精灵图集Shader渲染错乱
- NYOJ-5-Binary String Matching
- 优化Unity渲染器
- 优化Unity渲染器
- Unity5.3官方VR教程重磅登场-系列8 VR开发的更多资料
- 图形学理论知识 BRDF 双向反射分布函数 Bidirectional Reflectance Distribution Function
- Ward BRDF实现心得
- Unity3D Shader 入门
- 【Unity技巧】开发技巧(技巧篇)
- 深入理解Unity5中的StandardShader屏幕像素化特效的实现
- Unity研究院之多余的MeshCollider和Animation组件
- Unity5.3官方VR教程重磅登场-系列1