唐纳德·克努斯(Donald Knuth)曾说过后来变得著名的话:“真正的问题是程序员(而不是他们需要的地方,而不是他们的需要)花费太多时间来关注效率。 过早的优化是编程中所有弊端(或至少其中大多数弊端)的根源。”

该材料的作者(我们今天将要翻译的翻译版)想谈谈他曾经如何陷入过早优化的陷阱,以及他如何从自己的痛苦经历中了解到过早优化是万恶之源。
游戏竞技场在线
几年前,我从事网络游戏GeoArena Online(然后我
出售了 ,新所有者将其发布在
geoarena.io上 )。 这是一种“最后的幸存者”风格的多人游戏。 在那里,玩家控制了飞船,与另一位玩家一对一作战。
游戏竞技场在线游戏竞技场在线动态世界充满了粒子和特效,需要大量的计算资源。 结果,某些旧计算机上的游戏在特别紧张的时刻“减速”。 我是一个对生产力问题无动于衷的人,很感兴趣地解决了这个问题。 我问自己:“如何加快GeoArena客户端JavaScript的速度。”
Fast.js库
在Internet上搜索了一下之后,我发现了
fast.js库。 这是“微优化的集合,旨在简化非常快速的JavaScript程序的开发”。 内置标准方法(如
Array.prototype.forEach())的更快实现的可用性加速了该库。
我发现这非常有趣。 GeoArena使用了许多数组,并对数组执行了许多操作,因此使用fast.js可以很好地帮助我加快游戏速度。
forEach()
性能研究的以下结果包含在fastme.js的
README中 。
Native .forEach() vs fast.forEach() (10 items) ✓ Array::forEach() x 8,557,082 ops/sec ±0.37% (97 runs sampled) ✓ fast.forEach() x 8,799,272 ops/sec ±0.41% (97 runs sampled) Result: fast.js is 2.83% faster than Array::forEach().
在某些外部库中实现的方法如何比其标准版本更快? 事实是,这里有一个窍门(这些窍门随处可见)。 该库仅适用于处理稀疏数组。
这是此类数组的几个简单示例:
为了理解为什么该库不能与稀疏数组一起正常工作,我研究了其源代码。 事实证明,fast.js中的
forEach()
实现基于for循环。
forEach()
方法的快速实现如下所示:
调用
fastForEach()
方法将
fastForEach()
三个值:
1 undefined 2
调用
sparseArray.forEach()
仅得出两个值的结论:
1 2
这种差异是由于以下事实:有关使用回调函数的JS规范表明,
不应在远程或未初始化的数组索引 (也称为“空洞”)
上调用此类函数。
fastForEach()
实现未检查数组是否有孔。 这导致速度的提高,但要以稀疏阵列的正确工作为代价。 这对我来说是完美的,因为GeoArena中没有使用稀疏数组。
此时,我应该对fast.js进行快速测试。 我应该安装该库,将
Array
对象的标准方法更改为fast.js的方法,然后测试游戏的性能。 但是,相反,我朝着完全不同的方向前进。
我的开发称为fast.js
生活在我里面的躁狂完美主义者想从优化游戏性能中榨取一切。 在我看来,fast.js库根本不是一个足够好的解决方案,因为它的使用暗含了对其方法的调用。 然后我想:“如果仅通过在代码中嵌入这些方法的新的,更快的实现来替换数组的标准方法,该怎么办? 那将节省我对库方法调用的需求。”
正是这个想法使我想到了一个巧妙的想法,那就是创建一个编译器,我大胆地将其称为
faster.js 。 我计划使用它代替fast.js。 例如,这是源代码片段:
fast.js编译器会将这段代码转换为以下代码-更快,但看起来更糟:
fast.js的创建是由fast.js所基于的相同思想所促成的。 即,由于拒绝支持稀疏数组,我们正在讨论性能的微观优化。
乍一看,fast.js在我看来是一个非常成功的开发。 这是来自fast.js性能研究的一些结果:
array-filter large ✓ native x 232,063 ops/sec ±0.36% (58 runs sampled) ✓ faster.js x 1,083,695 ops/sec ±0.58% (57 runs sampled) faster.js is 367.0% faster (3.386μs) than native array-map large ✓ native x 223,896 ops/sec ±1.10% (58 runs sampled) ✓ faster.js x 1,726,376 ops/sec ±1.13% (60 runs sampled) faster.js is 671.1% faster (3.887μs) than native array-reduce large ✓ native x 268,919 ops/sec ±0.41% (57 runs sampled) ✓ faster.js x 1,621,540 ops/sec ±0.80% (57 runs sampled) faster.js is 503.0% faster (3.102μs) than native array-reduceRight large ✓ native x 68,671 ops/sec ±0.92% (53 runs sampled) ✓ faster.js x 1,571,918 ops/sec ±1.16% (57 runs sampled) faster.js is 2189.1% faster (13.926μs) than native
完整的测试结果可以在
这里找到。 它们位于15英寸MacBook Pro 2018的Node v8.16.1中。
我的开发是否比标准实施快2000%? 毫无疑问,如此大幅提高生产率可以对任何计划产生最大的积极影响。 对不对
不,不是真的。
考虑一个简单的例子。
- 想象一下,平均GeoArena游戏需要5,000毫秒(ms)的计算。
- fast.js编译器将数组方法的执行速度平均提高了10倍(这是一个近似估计,而且,它被高估了;在大多数实际应用中,甚至没有两倍的加速)。
这是我们真正感兴趣的问题:“这5000毫秒中的哪一部分花在了数组方法的实现上?”。
假设一半。 也就是说,在数组方法上花费了2500毫秒,其余2500毫秒用于其他所有方法。 如果是这样,那么使用faster.js将大大提高性能。
条件示例:程序的执行时间大大减少结果表明,总计算时间减少了45%。
不幸的是,所有这些论点离现实都非常非常遥远。 当然,GeoArena使用许多数组方法。 但是,不同任务的代码执行时间的实际分配类似于以下内容。
严酷的现实可悲的是,我能说什么。
这正是Donald Knuth警告的错误。 我没有将精力投入应有的应用,并且在值得做的事情上也没有做。
在这里,简单的数学开始发挥作用。 如果某件事仅占用程序执行时间的1%,那么在最佳情况下对其进行优化只会使生产力提高1%。
这正是唐纳德·克努思(Donald Knuth)说的“不需要的地方”时所想到的。 而且,如果您考虑“需要什么”,那么事实证明这些是代表性能瓶颈的程序部分。 这些是对程序的整体性能做出重大贡献的代码段。 在这里,“生产率”的概念被广泛地使用。 它可能包括程序的运行时,其编译代码的大小以及其他内容。 在程序中对性能产生重大影响的部分,将其提高10%比在一些小事情上将其提高100%更好。
克努特还谈到“不必在必要时”施加努力。 这样做的重点是仅在必要时才需要优化某些内容。 当然,我有充分的理由考虑优化。 但是请记住,我开始开发fast.js,而在那之前我甚至没有尝试过在GeoArena中测试fast.js库? 在我的游戏中测试fast.js花费的时间可以节省我数周的工作。 希望您不要陷入我陷入的陷阱。
总结
如果您有兴趣尝试使用fast.js,可以看一下
这个演示。 您得到的结果取决于您的设备和浏览器。 例如,这里是15英寸MacBook Pro 2018的Chrome 76发生的情况。
Faster.js测试结果您可能有兴趣了解在GeoArena中使用fast.js的实际结果。 我在游戏还属于我的时候(如我所说,我卖了),进行了一些基础研究。 结果,结果如下:
- 使用faster.js可将典型游戏中主要游戏周期的执行速度提高约1%。
- 由于使用了fast.js,因此游戏包的大小增加了0.3%。 这稍微减慢了游戏页面的加载速度。 由于fast.js将标准短代码转换为更快但更长的代码,因此捆绑包的大小有所增加。
通常,faster.js有其优点和缺点,但是我的开发对GeoArena的性能没有太大影响。 如果我想先使用fast.js测试游戏,那我早就了解了。
愿我的故事对您发出警告。
亲爱的读者们! 您是否陷入过早优化的陷阱?
