Javascript 对象与 Map/Set 键查找性能

V S*_*S X 16 javascript performance dictionary object set

我试图探索 JavaScript与普通按键访问Object相比如何执行。我在JSBEN.CH上运行了以下 3 段代码。MapSet

对象

const object = {};

for (let i = 0; i < 10000; ++i) {
    object[`key_${i}`] = 1;
}

let result = 0;
  
for (let i = 0; i < 10000; ++i) {
    result += object[`key_${i}`];
}
Run Code Online (Sandbox Code Playgroud)

地图

const map = new Map();

for (let i = 0; i < 10000; ++i) {
    map.set(`key_${i}`, 1);
}

let result = 0;

for (let i = 0; i < 10000; ++i) {
    result += map.get(`key_${i}`);
}
Run Code Online (Sandbox Code Playgroud)

const set = new Set();

for (let i = 0; i < 10000; ++i) {
    set.add(`key_${i}`);
}

let result = 0;

for (let i = 0; i < 10000; ++i) {
    result += set.has(`key_${i}`);
}
Run Code Online (Sandbox Code Playgroud)

正如您可以检查测试链接一样,Map似乎Set执行几乎相似,但Objects每次都慢得多。有人可以解释一下性能比基本密钥访问操作Objects差的原因是什么吗?MapSet

编辑 1:仅将键设置为 on也Object比/MapSet

Jon*_*lms 32

仅查看相对数字总是危险的,这里有一些绝对数字,在 Intel 8350U 上的 NodeJS v14.14.0 上运行:

迭代 对象写入 对象读取 地图写入 地图读取
100 0毫秒 0毫秒 0毫秒 0毫秒
1.000 3毫秒 1毫秒 0毫秒 0毫秒
10,000 7毫秒 4毫秒 8毫秒 1毫秒
1.000.000 1222毫秒 527毫秒 632毫秒 542毫秒

正如我们所看到的,对于 10,000 次迭代,在上面的运行中,对象和地图之间的差异为 1 毫秒,并且由于这是时间测量的准确性,因此我们无法真正从该测试中得出任何结论。结果绝对是随机的。

对于 100 万次迭代,我们可以看到 Map 写入相对于 Object 写入的明显优势,读取性能非常相似。现在,如果我们看一下绝对数字,这仍然是100 万次写入/秒。因此,尽管对象写入速度慢很多,但这不太可能成为应用程序的瓶颈。

为了得到准确的解释,人们必须分析引擎执行的所有步骤。为此,您可以运行node --print-code并分析运行的字节码。我没有时间这样做,但这里有一些观察结果:

  1. 如果对象是用Object.create(null)(没有原型)构造的,则性能大致相同,因此原型查找根本不会影响性能。

  2. 在第 20 次迭代之后,V8 选择 的内部表示,dictionary_map因此object这基本上是一个哈希图与另一个哈希图竞争(可以运行node --allow-natives-syntax然后使用%DebugPrint(object)来获取内部表示)。

作为参考,以下是用于运行基准测试的代码:

function benchmark(TIMES) {  
  console.log("BENCHMARK ", TIMES);

  const object = Object.create(null);

    let start = Date.now();
    for (let i = 0; i < TIMES; ++i) {
        object[`key_${i}`] = 1;
    }

    console.log("Object write took", Date.now() - start);
    start = Date.now();

    let result = 0;
  
    for (let i = 0; i < TIMES; ++i) {
      result += object[`key_${i}`];
    }

    console.log("Object read took", Date.now() - start);
    start = Date.now();

  


  const map = new Map();
  
  for (let i = 0; i < TIMES; ++i) {
    map.set(`key_${i}`, 1);
  }
  
  console.log("Map write took", Date.now() - start);
  start = Date.now();

  result = 0;
  
  for (let i = 0; i < TIMES; ++i) {
    result += map.get(`key_${i}`);
  }

  console.log("Map read took", Date.now() - start);

}

benchmark(100);
benchmark(1_000);
benchmark(10_000);
benchmark(1_000_000);
Run Code Online (Sandbox Code Playgroud)

总结:

  • 将映射用于具有许多不同的、不断变化的键的字典,因为它们比内部表示为哈希表的对象稍好一些
  • 将对象用于——好吧——对象。如果您的键数量较少并且经常访问这些键,则对象会更快(因为引擎可以使用内联缓存、具有固定内存布局的隐藏类等)