【译】利用xperf解决unity导入纹理缓慢的问题

发表于2016-03-10
评论0 3.9k浏览

  原文地址:http://blogs.unity3d.com/2015/01/10/curious-case-of-slow-texture-importing-and-xperf/

  原文作者未做版权声明,视为共享知识产权进入公共领域,自动获得授权

  我曾看到过一个缺陷报告:当前Unity5.0导入纹理过于缓慢。其中一个纹理的导入花费了其平常耗费的10倍时间。

  为什么纹理导入会如此慢?这份缺陷报告让我第一次使用了xperf(又名Windows性能工具包)。

一、回顾一下故事的过程
  我们得到一个案例报告,在当前Unity5.0测试版中一个TGA格式的纹理(2048*2048,未压缩,一个12M的文件)的导入花费了10秒,但是在Unity4.6版本中只需要花费大约1秒。
  第一个猜测:是不是有人不小心禁用了纹理的多线程压缩?
  第二个猜测:我们使用FreeImage库来导入纹理。也许谁提交了调试版本?
  首先,我们将使用profiler检测问题所在。




  我们发现所有的时间都很奇怪花在了WinAPI ReadFile函数上?!还是测试的TAG文件有一下特殊?让我们重新使用一个同样大小,未被压缩的PNG文件再次尝试。




  PNG的导入花费了108ms,而TGA花费是9800ms。(我已经禁用了DXT压缩,以保证只关注导入时间。)在Unity4.6中,同样的测试是116msPNG)和310msTGA)。文件大小大致相同。

二、使用xperf
  于是我咨询一位深谙Windows的同事:为什么读取一个文件,ReadFile函数会花费如此多的时间,但是另一个同样大小的文件却读取非常快?他给出答案,“你应该尝试使用xperf来看看
  配置Windows性能记录仪,勾选上“CPU usage”“Disk I/O activity”“File I/O activity”,然后点击“Start”



  在Unity中导入纹理,点击“Save”,然后“Open in WPA”按钮.




  侧栏给出了各项指标的统计图。一个诡异的现象是:无论是CPU还是存储的图都没显示出剧烈的活动?于是感觉问题变得棘手起来!。


三、CPU使用情况分析
  双击Computation的图,显示出每一个进程的CPU使用时间曲线。查看Unity.exe在图中高亮的那段时间内的CPU占用情况。

 

  下一件事,需要了解具体是什么在占用CPU。右击左侧的黄色分配器,然后选择需要关注的信息,工具会根据我们的选择,来组织右侧显示的细节信息。现在需要关注的是调用堆栈。所以,在右键菜单中选择“Stack”


  噢,现在对了。需要得到任何有用的堆栈信息,我们需要告诉xperf加载对应的信息。所以,需要去Trace->Configure Symbols Paths,增加一个Unity文件夹,然后Trace->Load Symbols。经过一段时间的等待,就可以得到调用堆栈信息了。


  进入另一个标签调用堆栈上,我们看到所有的时间果然是花费在ReadFile。可是我们已经通过profiler知道了这一点。



四、查看I/O使用
  还记得这个侧栏中的“Storage”图没有显示剧烈的活动?现在,我们来展开它从而得到更多的细节信息。




  突然发现,有点眉目了!当我们导入TGA文件的时候,“File I/O”的总图上显示出了剧烈的活动。现在只需要来搞清楚到底发生了什么。
  双击侧边栏的图来获取更详细的I/O信息:



  现在您也许看到了那里发生了什么。我们读取了一堆文件,实际上约有400,000之多。
  同CPU部分一样,工具是通过左侧的黄色的分配器的信息来组织列信息。把“Process”拖拽到左侧;下图表明了所有的这些读取的确都是由Unity触发的。


展开来看看谁是真正的罪魁祸首:


  我们正在阅读这些文件,一次读取3个字节。


五、缺陷是怎么出现?怎么解决?

  但是为什么我们三个字节三个字节的读取一个12MTGA文件?我们已经好久没更新过图片文件读取的库了,那么,这些缺陷是哪来的?
  在代码中找到我们读取进FreeImage的地方。看来我们正在建立自己的I/O函数,然后给FreeImage使用。



  历史版本检查:果然,一些星期前,这段代码发生了一些改变,从更为基础的你好,从这个文件路径加载图片变成了你好,使用这些I/O回调函数加载图片

  一般这些改动都是有意义的的。如果我们有自己的文件系统函数,然后合理的使用,就可以读取一些非普通文件。(例如存档或者压缩文件等。)在这个案例中,我们是为了支持在光照贴图上缓存LZ4压缩。(FreeImage不需要知道他们头部是否存在LZ4压缩,而能直接导入纹理文件。)
  一切都如此美好。但是这个改变却导致了一个完全意外的性能缺陷。
  当你不直接传递I/O例程到FreeImage,他会使用C的标准输入输出作为默认设置。



  C的标准例程默认做I/O缓冲……我们的例程没做。所以FreeImageTAG加载函数做了大量的读取操作,一次读取一个像素。


六、解决方案

  一个适当的解决方案是给FreeImage设置一个带有缓冲的I/O例程。
  需要确认的是,是否这就是真正的罪魁祸首,从而不会再出现“TGA文件导入太慢,我做了一个修补程序,直接读取整个文件到内存,从内存进行加载。



  一次读取整个图片到内存好吗?视情况而定,我猜测95%的情况下是好的,尤其64bitUnity编辑器。对于大多数未压缩的图像文件,最终数据大小会比文件大小大很多。也许唯一的例外是.PSD文件,它会携带很多层信息,但是我们只对复合图像节点感兴趣。所以,这也是为什么我说修补程序。一个妥善的解决方案是支持具有缓冲的I/O例程,又或者升级。

  这样导入TGAPNG会比以前快很多:TGA花费75msPNG花费87ms。(Unity 4.6TGA310msPNG116ms,当前测试版中TGA9800msPNG108ms。)


七、结论:
  当使用你自己的实现来取代内置的函数库时需要谨慎。(例如标准的输入输出,内存分配,日志,或者……其他函数库的例程。)他们可能会有不同的性能特性。
  在windows下xperf非常有用。如果感兴趣,可以查看Bruce Dawson博客。
  Mac上,Apple’s Instruments是一个类似工具。我应该会在以后的博客中使用到它。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引