JavaScript中的痣洞

哈Ha! 我向您介绍了Mathius Buus撰写的文章“ JavaScript漏洞的翻译。



计算机是有趣的机器。 从理论上讲,在我们看来,他们是理想的机械数学家,使用数字并能很好地执行加法,乘法和减法运算。


但是,这样的抽象颇具误导性。 它使我们摆脱了计算机以不同的速度处理不同的数学运算的理解。 如果您使用JavaScript(或任何其他语言)编写并且关心所编写算法的性能,那么了解计算机如何在后台工作非常重要。


如果我们知道计算机的功能,则可以使用最短路径或虫洞使我们的程序比预期的快得多。


求取除法运算中的余数


这到底是什么意思? 让我们看一个例子:假设我们要实现一个ring list铃声列表是一个固定大小的列表,其中比列表大的插入片段将移动到列表顶部并成一个圆圈。 环形列表在许多方面都很方便-例如收集特定时间间隔的统计信息,缓冲数据等等,但请看以下实现:


const list = new Array(15000) function set (i, item) { //  % -   ,     //        //    ,     i    list[i % list.length] = item } 

此代码执行速度有多快? 让我们运行一个简单的速度测试


 console.time() for (var i = 0; i < 1e9; i++) { set(i, i) } console.timeEnd() 

在我的计算机上,插入10亿个文件花费了大约4秒钟的时间。 还不错


但是,让我们应用一个计算虫洞并将数组的大小更改为幻数:


 //     15000  16384 const list = new Array(16384) function set (i, item) { //  % -   ,     //        //    ,     i    list[i % list.length] = item } 

让我们尝试再次运行性能测试。 在我的计算机上,测试在约1.5秒内完成。 由于简单地调整大小,增加了两倍以上。 为了了解为什么会发生这种情况,我们必须了解以下内容,即计算机在底数为2的情况下工作。重要的是要知道是否得到除法的余数(%运算)。 如果该数字是2的倍数(2 ^ n)b 16384它是2 ^ 14,则这种计算要简单得多。实际上,计算机以二进制形式查看该数字,并仅获取最后的n位。


例如:执行此类操作时将发生353 500%16 384? 353 500的二进制表示形式将类似于1010110010011011100。由于16384 == 2 ^ 14-我们需要采用最后的14位-10101(10010011011100)或9346。


我们可以将这些知识用于另一个虫洞。 对于计算机,获取最后n位非常简单快捷。 实际上,只需要生成二进制数(2&n)-1和(操作&)


 const list = new Array(16384) function set (i, item) { //    &( )  %    2 ^ n list[i & (list.length - 1)] = i } 

在我的计算机上再次运行性能测试,我们将看到执行时间将减少到〜1 s,或者与第一次运行相比,性能将提高四倍。 所有这一切都归因于对计算机工作原理的理解。


智能编译器或VM可以通过将获取幕后剩余部分的操作转换为按位操作,反之亦然来进行这种优化。 实际上,最新的V8 Javascript VM(未在NodeJS中实现)可以做到这一点。


数值虫孔


另一个有用的技巧是理解数字的读写方式。 还记得我们如何使用32位计算机以及如何获得64位吗? 最多32位,我们只有16位。 这到底是什么意思? 通常,我们是这样想的-计算机上有多少RAM。 2 ^ 32 = 4294967296或4 GB,这意味着我们只能在32位计算机上访问4 GB的内存。 编写JS程序时,通常不需要考虑它,因为我们通常不使用那么多的内存。


但是,了解32位和64位计算机之间的区别非常重要。 由于处理器在64位计算机上接收了64位寄存器,因此操作速度比32位计算机(只有32位寄存器)快2倍。


我们如何使用有关该虫洞的信息?
让我们编写一个简单的程序,将一个Uint8Array复制到另一个。 如果您不熟悉Unit8Arrays,它们与NodeJS中的Buffers非常相似,或者仅仅是“干净”的内存。


 function copy (input, output) { //    input.length <= output.length for (var i = 0; i < input.length; i++) { //   8-  (byte) output[i] = input[i] } } 

同样,让我们​​通过运行性能测试来衡量速度。


 //    1MB Uint8Arrays     const input = new Uint8Array(1024 * 1024) const output = new Uint8Array(1024 * 1024) console.time() for (var i = 0; i < 1e4; i++) { copy(input, output) } console.timeEnd() 

在我的计算机上,该程序约7.5秒完成。 我们如何使用虫洞来加速? 使用Uint8Array,我们一次只能复制8位,但是有一台64位计算机,我们可以同时复制64位信息。 我们可以在JavaScript中通过在复制之前将Uint8Array转换为Float64Array来做到这一点,而这不会花费任何费用。


 function copy (input, output) { //    input.length <= output.length //       64-  //        , //      64-  //  BigInts   JavaScript,    BigInt64Array. const input64 = new Float64Array(input.buffer, input.byteOffset, input.length / 8) const output64 = new Float64Array(output.buffer, output.byteOffset, output.length / 8) for (var i = 0; i < input64.length; i++) { //   64-  output64[i] = input64[i] } } 

再次运行性能测试,我们得到了1秒的运行时间,这使速度提高了8倍。


对于复制,可以接受的解决方案是对Uint8Array使用array.set(otherArray)方法,该方法使我们可以使用本机代码进行复制-比任何其他虫洞都快得多。 作为参考,这将使我们在我的计算机上进行测试时得到的执行时间约为0.2秒,比以前的解决方案快5倍。


JavaScript星系到处都是虫洞


使用上面的虫洞将帮助您更快地完成大量实际算法。 还有更多这样的虫洞。 我最喜欢的是Math.clz32 ,该方法以数字的32位二进制表示形式返回前导零位的数量。 我们可以将这种方法用于许多有趣的算法。 我使用它将位域的实现加速了4倍,从而使内存消耗减少了4倍,并且在某些情况下可以更快地对数字进行排序


了解计算机的基本原理可以使您编写所需的最快程序。 即使您使用高级语言(例如JavaScript)编写,该知识也很重要。


PS:


特别感谢您帮助翻译和调整Olga Pereverzeva的翻译

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


All Articles