热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何找到并加速缓慢的代码,提高性能

本文讲述了如何找到并加速缓慢的代码,提高系统性能。作者通过优化内部循环、利用多处理器运行内部循环以及减少内部循环运行次数等方式来提高代码的执行效率。以一个游戏开发中的案例为例,作者介绍了如何通过调查和排查问题,找到导致帧率下降的设计缺陷,并进行修复。文章总结了优化代码的关键点,并提供了一些实用的方法和技巧。

https://img8.php1.cn/3cdc5/24726/807/d78c401fae030c7c.jpeg

前言

当性能是一项功能时,缓慢就是一个漏洞。找到缓慢的源头就像是追踪一个漏洞,但是当你找到缓慢代码时,通常有三种方式去加速代码:

· 让内部循环运行更快。这包括了缓存优化、减少分支和优化SIMD。

·用更多的处理器运行内部循环。在多处理器内核以及/或是多台机器上实行并行。

·减少内部循环运行次数。这包括了从早期测试到算法革新的一系列手段来提高计算复杂性(big-O)。

  故事是关于内部循环已经高度优化,并且也同步运行了,但是一些列严重的设计缺陷使得系统运行远慢于预期。


背景

2014年初,我在箭头游戏工作室工作,当时我们打算重新启动经典的街头游戏Gauntlet。事件发生在我们打算首次公布该游戏的前一周。我们想要让玩家可以任意选择游戏关卡,最后一周里大家都忙于修复漏洞、改善游戏设置,以及提升视觉效果。看起来一切顺利,但是最后一周出现了一些情况:帧率从预期的60FPS下降到了20-25FPS的龟速。这引起了极大的恐慌。哪里出错了?我们还有时间修复吗?

https://img8.php1.cn/3cdc5/24726/807/667631309ab8184e.jpeg

调查

我们马上把怀疑方向转向了lighting接口。近期刚增加了大量的全方位阴影投射灯,而且价格昂贵。

Gauntlet的开发利用了第三方BitSquid引擎(原名叫StingRay)。我们用阴影贴图来使用一个延期的遮光管道。这篇博客就不详细阐述阴影贴图的工作原理了,不过这里是一个简短的概述,可以看出在BitSquid中如何完成全方面的阴影映射:

每个全向光模拟为六个聚光灯走向的正负x、y、z轴。每个这种“虚拟聚焦”引擎会把附近的几何图形呈现在阴影贴图上,这是一个屏幕外的缓冲区,包含了从灯光到最近几何图形的距离。这些阴影贴图之后会被用来确定场景中的各个像素是否需要被阴影投射灯挡住。

关掉阴影上的所有投射灯之后确实让帧率重回60FPSl ,但是这也破坏了这个游戏的整体感觉。在最后的两天理,我决定跟踪找到问题的来源。

我开始尝试改动阴影贴图设置。尤其是,我强力否定了他们的提议,从1024² 到16²。这不会对帧率速度造成任何影响。这一点提醒了我,有一些地方出了大错。在加强游戏内部探查器之后我发现了罪魁祸首:阴影投射的剔除。


问题

在绘制阴影贴图的时候,我们不能只是简单地把所有的几何图形发送给渲染器,因为这可能会导致阴影贴图渲染速度非常低。相反的,渲染器会首先剔除掉几何图形。就是这种剔除过程耗时太长。事实上,这会耗费长达25毫秒!我们为一个稳定的60FPS做的预算是16毫秒。这16毫秒里需要做所有事:游戏逻辑、物理模拟、阴影渲染、场景渲染、场景照明和后期处理等。16毫秒来做这一切,而现在光做一件事就需要25毫秒。该死的!

这是在周三发现的。周五我们就要让最终版本开始运行。在周四早上我做出了一个大胆的承诺:今天结束的时候,帧率会加倍。然后我就去上班了。

值得庆幸的是,我们拥有BitSquid的源代码许可证,我习惯于优化引擎并修复漏洞。看了这些代码之后发现BitSquid单纯地把所有的几何图形在整体水平上进行剔除,在每一次的阴影投射聚光灯和每六次的全方位灯光时进行。此外,这种剔除是通过昂贵的OBB(面向定界框)和锥测试的斗争中完成的。这就意味着在水平上有N个几何图形和L个全方位灯,就会有N*L*6个OBB-锥测试。就是这些测试花费了25毫秒。很显然,BitSquid已经有人意识到这部分代码可能变成一个瓶颈,因为这样的OBB-锥测试是SIMD优化的,并且在数个工作路线中并行处理。这也意味着我不可以让这些代码运行的更快,或是用更多的处理器去运行它。所以我只剩下了唯一一条路:让代码少运行几次。


解决方案

我只剩下一天时间来提高性能了,所以我采取的方法都对引擎(我只是大概了解的)产生了最小的改变。


早期

BitSquid的所有几何图形都有一个已经预先计算好的OBB包围盒,但是OBB测试总是很慢。我决定在每一个几何图形和接口上增加一个很粗糙、但是运行更快的原始定界框:一个球体。测试彼此排斥的两个球体要便宜得多,这在很大程度上让我们免于昂贵的OBB测试,也节省了大量时间。然而,我还没有时间去修改引擎和所有的工具,来为每个几何图形增加一个预先计算好的最小包围球。相反的,我决定来计算从OBB出来的最小包围半径。这个长宽高分别为W、H、D的包围盒外包围球的最小半径是 √((W/2)² + (H/2)² + (D/2)²), 但是这个平方根有点大。所以我决定让这个包围球稍微大一点,计算后得出半径为√3/2 * max(W, H, D)( 其中√3/2可以重复使用)。

  我还计算出所有灯光的包围球,这对于全方位投影灯来说当然微不足道,但是对聚光灯来说这也稍显复杂,但是我想到了一个快速逼近的办法,不过还没有想出具体的细节。

https://img8.php1.cn/3cdc5/24726/807/a5f053f5f2087a2d.gif

预先计算一组潜在的阴影投射

把遥远的几何图形送去剔除完全是在浪费时间。大多数的游戏引擎都在某种形式的层次结构(如BSP)中存储游戏关卡,这使得引擎能大段大段地远距离剔除。BitSquid却没有这样的功能,我也没有时间在一天之内加上这样一个结构。所以我在帧的开头添加了额外的步骤,这样我就可以在所有相交的相机阴影投射灯周围加上边界框。这形成了一个包含一切的边界框,这就可以在我们的视野范围内投射一个接口。然后,我就可以预先剔除拥有这个边界框场景中的一切,从而选出一组潜在的阴影投射。这就意味着,在之后的剔除过程中,我们只需要测试该场景中的几何图形,而不是所有的。

https://img8.php1.cn/3cdc5/24726/807/b7d66df09e8b31e1.gif

把全向光看作一个整体

如前所述,BitSquid把每个全向光看做六个聚光灯,并且单独为每个聚光灯进行剔除。我增加了一个预先通过,在那我剔除潜在阴影投射中每一个有包围球的泛光灯,从而挑选出靠近泛光灯的几何图形。只有通过这项粗略测试之后,我才会把这些几何图形发送去进一步测试,也就是使用原始的OBB-锥测试代码对六个虚拟聚光灯进行的测试。


结果

在引擎上加上所有这些步骤之后,我成功把剔除过程从25毫秒减到了2毫秒左右,我们的帧率也顺利稳定在了60FPS。任务完成!第二天我们发布了预览版。


教训

· 拥有你现在正在使用的所有中间软件的源代码是十分必要的。它不仅会帮助你发现问题所在,你也可以通过它来修复问题。

· 如果你想要加速代码运行,首先退一步,想一想这个代码是否可以不运行。


推荐阅读
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 无损压缩算法专题——LZSS算法实现
    本文介绍了基于无损压缩算法专题的LZSS算法实现。通过Python和C两种语言的代码实现了对任意文件的压缩和解压功能。详细介绍了LZSS算法的原理和实现过程,以及代码中的注释。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 开发笔记:计网局域网:NAT 是如何工作的?
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了计网-局域网:NAT是如何工作的?相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文由编程笔记#小编整理,主要介绍了关于数论相关的知识,包括数论的算法和百度百科的链接。文章还介绍了欧几里得算法、辗转相除法、gcd、lcm和扩展欧几里得算法的使用方法。此外,文章还提到了数论在求解不定方程、模线性方程和乘法逆元方面的应用。摘要长度:184字。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 网卡工作原理及网络知识分享
    本文介绍了网卡的工作原理,包括CSMA/CD、ARP欺骗等网络知识。网卡是负责整台计算机的网络通信,没有它,计算机将成为信息孤岛。文章通过一个对话的形式,生动形象地讲述了网卡的工作原理,并介绍了集线器Hub时代的网络构成。对于想学习网络知识的读者来说,本文是一篇不错的参考资料。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 腾讯安全平台部招聘安全工程师和数据分析工程师
    腾讯安全平台部正在招聘安全工程师和数据分析工程师。安全工程师负责安全问题和安全事件的跟踪和分析,提供安全测试技术支持;数据分析工程师负责安全产品相关系统数据统计和分析挖掘,通过用户行为数据建模为业务决策提供参考。招聘要求包括熟悉渗透测试和常见安全工具原理,精通Web漏洞,熟练使用多门编程语言等。有相关工作经验和在安全站点发表作品的候选人优先考虑。 ... [详细]
author-avatar
mis安小米
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有