为什么使用JavaScript对32位数字进行排序比对33位数字进行排序要快得多?

问题描述 投票:2回答:1

以下代码只是创建一个数组并对其进行排序。非常奇怪的是,在我的2013 Macbook Pro上,花了5.8秒的时间对30位数字进行了排序:

n = 10000000;
numMax = 1000000000;

console.log(`numMax is how many bits: ${Math.ceil(Math.log(numMax) / Math.log(2))}`)

console.log("\n## Now creating array");

let start = Date.now();
let arr = new Array(n);

for (let i = 0; i < arr.length; ++i) 
    arr[i] = Math.floor(Math.random() * numMax);

console.log(`took ${(Date.now() - start)/ 1000} seconds to create the array`);


console.log("\n## Now sorting it");
start = Date.now();

arr.sort((a, b) => a - b);

console.log(`took ${(Date.now() - start)/ 1000} seconds to sort`);

但是让我们说它是34位数字。现在需要12.7秒才能运行:

n = 10000000;
numMax = 10000000000;

console.log(`numMax is how many bits: ${Math.ceil(Math.log(numMax) / Math.log(2))}`)

console.log("\n## Now creating array");

let start = Date.now();
let arr = new Array(n);

for (let i = 0; i < arr.length; ++i) 
    arr[i] = Math.floor(Math.random() * numMax);

console.log(`took ${(Date.now() - start)/ 1000} seconds to create the array`);


console.log("\n## Now sorting it");
start = Date.now();

arr.sort((a, b) => a - b);

console.log(`took ${(Date.now() - start)/ 1000} seconds to sort`);

在NodeJS上(更新:我正在使用v12.14.0),两者之间的差异更大:5.05秒vs 28.9秒。为什么差异如此之大?如果是由于Chrome或NodeJS能够通过使用32位整数(而不是64位整数或IEEE 754数字)对其进行优化,那么在排序(以及移动数据)过程中进行比较是否需要一个时钟周期在Quicksort的“分区阶段”)?为什么要花2倍甚至5倍以上的时间呢?它是否也适合将所有数据放入处理器的内部缓存中,以及内部缓存是否可以支持32位而不支持IEEE 754数字?

javascript node.js performance v8
1个回答
4
投票

V8开发人员在这里。简而言之:这就是为什么V8尽可能在内部使用“ Smis”(小整数)的原因。

在JavaScript中,任何值通常可以是任何值,因此引擎通常以某种格式表示值,该格式将类型信息与值本身一起存储。这包括数字;因此堆上的数字是一个具有两个字段的对象:类型描述符和实际数字值,这是每个JavaScript规范的IEEE-754 64位双精度值。由于小整数值特别常见,因此V8使用一种特殊技巧来对其进行更有效的编码:它们根本不作为对象存储在堆中,而是直接将值编码为“指针”,指针的位之一用于将其标记为所谓的Smi。在所有当前版本的Chrome中,V8使用32位堆指针,为Smi的有效负载保留31位。由于数字数组也很常见,因此为每个元素存储类型描述符非常浪费。相反,V8具有双精度数组,该数组本身记住所有元素都是双精度的事实(只有一次!)。然后可以将这些元素直接存储在数组中。

因此,在代码的30位版本中,数组的后备存储区是一个充满Smis的数组,调用比较器函数可以直接传递其中的两个。该功能又可以快速Smi检查并取消标记值以执行减法。

在34位版本中,阵列的后备存储存储了两倍的数据。每次需要调用比较器时,都会从数组中读取两个原始双精度值,并装箱为“堆号”,以便用作函数调用的参数,并且比较器函数必须先从堆中读取值能够减去它们。我实际上感到惊讶,这只是速度的两倍:-)

为了稍微了解一下此测试用例的性能细节,您可以强制数组存储堆号,而不是未装箱的双打。尽管这会预先消耗更多的内存并在许多用例中具有性能成本,但在这种特殊情况下,由于在执行期间分配的短期垃圾较少,因此实际上节省了大约10%的时间。如果您另外强制将比较器的结果作为Smi返回:

arr.sort((a, b) => a > b ? 1 : a < b ? -1 : 0);

节省了另外10%。

在NodeJS上,差异更大:5.05秒vs 28.9秒。

使用Node 13.11,我无法重现;我得到的数字几乎与Chrome 81完全相同。

Chrome或NodeJS能够通过使用32位整数而不是64位整数或IEEE 754数字来对其进行优化

能够使用32位整数CPU指令是使用Smi表示形式的副作用,但这不是造成性能差异的(主要原因)。在内部使用64位整数会违反JavaScript规范(除非引擎非常小心地检测并避免too precision的结果。)

比较是否需要一个时钟周期

估计现代CPU上的时钟周期非常困难,几乎没有什么比“恰好一个时钟周期”那么简单。一方面,CPU每个周期可以执行多条指令(的一部分),另一方面,它们具有流水线,这意味着仅完成一条指令的执行就需要很多周期。特别是,频繁的分支(即,CPU内部的“决策制定”),通常是排序算法所要做的,往往会遭受与流水线相关的延迟的影响。

Quicksort的“分区阶段”

V8不再使用Quicksort。也就是说,所有排序算法当然都必须移动数据。

它是否与将所有数据放入处理器的内部缓存中以及内部缓存是否可以支持32位但不支持IEEE 754数字有关?

CPU的缓存不关心数据类型。数据的大小(64位双精度为32位整数的两倍)可能会导致与缓存相关的性能差异。

如果优化程序可以推断出数组中的所有值都适合该大小,那么V8可以优化数字存储类型

几乎;不涉及任何推论:阵列从Smi后备存储乐观地开始,并根据需要将其概括化,例如当存储第一个double时,存储将切换到double数组。

您可能会看到“及时编译”的效果。

不是。当然,所有现代引擎均会通过JIT编译您的代码,但这对所有代码都是正确的,这里不再解释区别。

© www.soinside.com 2019 - 2024. All rights reserved.