不要陷入过早优化的陷阱

唐纳德·克努斯(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(). 

在某些外部库中实现的方法如何比其标准版本更快? 事实是,这里有一个窍门(这些窍门随处可见)。 该库仅适用于处理稀疏数组。

这是此类数组的几个简单示例:

 //  -  :   1  . const sparse1 = [0, , 1]; console.log(sparse1.length); // 3 //  -   const sparse2 = []; // ...   - .   0 - 4    . sparse2[5] = 0; console.log(sparse2.length); // 6 

为了理解为什么该库不能与稀疏数组一起正常工作,我研究了其源代码。 事实证明,fast.js中的forEach()实现基于for循环。 forEach()方法的快速实现如下所示:

 //     . function fastForEach(array, f) {  for (let i = 0; i < array.length; i++) {    f(array[i], i, array);  } } const sparseArray = [1, , 2]; const print = x => console.log(x); fastForEach(sparseArray, print); //  print() 3 . sparseArray.forEach(print); //  print()  2 . 

调用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。 例如,这是源代码片段:

 //   const arr = [1, 2, 3]; const results = arr.map(e => 2 * e); 

fast.js编译器会将这段代码转换为以下代码-更快,但看起来更糟:

 //      faster.js const arr = [1, 2, 3]; const results = new Array(arr.length); const _f = (e => 2 * e); for (let _i = 0; _i < arr.length; _i++) {  results[_i] = _f(arr[_i], _i, arr); } 

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测试游戏,那我早就了解了。

愿我的故事对您发出警告。

亲爱的读者们! 您是否陷入过早优化的陷阱?

Source: https://habr.com/ru/post/zh-CN465809/


All Articles