在Nodejs中比较两个字符串时,为什么'==='比逐个字符比较慢

时间:2019-05-01 06:30:21

标签: javascript string performance

我发现在 Nodejs 中,通过比较两个字符串的每个字符来比较两个字符串比使用语句'str1 === str2'更快。 这是什么原因呢?在浏览器中,它正好相反。

这是我尝试过的代码,两个长字符串相等。节点版本为 v8.11.3

function createConstantStr(len) {
  let str = "";
  for (let i = 0; i < len; i++) {
    str += String.fromCharCode((i % 54) + 68);
  }

  return str;
}

let str = createConstantStr(1000000);
let str2 = createConstantStr(1000000);

console.time('equal')
console.log(str === str2);
console.timeEnd('equal')

console.time('equal by char')
let flag = true;
for (let i = 0; i < str.length; i++) {
  if (str[i] !== str2[i]) {
    flag = false;
    break;
  }
}

console.log(flag);
console.timeEnd('equal by char');

3 个答案:

答案 0 :(得分:28)

已经向您指出,如果您翻转两个测试,那么与===进行比较将比逐个字符进行比较要快。到目前为止,您对原因的解释还没有确切地说明原因。有一些问题会影响您的结果。

第一个console.log通话费用昂贵

如果我尝试这样做:

console.time("a");
console.log(1 + 2);
console.timeEnd("a");

console.time("b");
console.log("foo");
console.timeEnd("b");

我得到类似的东西

3
a: 3.864ms
foo
b: 0.050ms

如果我翻转代码以便拥有:

console.time("b");
console.log("foo");
console.timeEnd("b");

console.time("a");
console.log(1 + 2);
console.timeEnd("a");

然后我得到这样的东西:

foo
b: 3.538ms
3
a: 0.330ms

如果我在进行任何计时之前通过添加console.log来修改代码,例如:

console.log("start");

console.time("a");
console.log(1 + 2);
console.timeEnd("a");

console.time("b");
console.log("foo");
console.timeEnd("b");

然后我得到类似的东西:

start
3
a: 0.422ms
foo
b: 0.027ms

通过在开始计时之前放置console.log,我排除了从计时中调用console.log的初始费用。

按照您设置测试的方式,首先进行console.log或按字符进行测试的第一个===调用,而这是第一个{{ 1}}呼叫已添加到该测试。无论哪一个测试次之,都不承担该费用。最终,对于这样的测试,我宁愿将console.log移到正在计时的区域之外。例如,第一个定时区域可以这样写:

console.log

将结果存储在console.time('equal'); const result1 = str === str2; console.timeEnd('equal'); console.log(result1); 中,然后在定时区域之外使用result1可确保您看到结果,而同时不计算console.log(result1)产生的成本。

首先进行哪个测试,都要承担将v8内部创建的字符串树弄平的费用

Node使用v8 JavaScript引擎来运行JavaScript。 v8以多种方式实现字符串。 console.log在注释中显示v8支持的类层次结构。这是section relevant to strings

objects.h

对于我们的讨论,有两个重要的类:// - String // - SeqString // - SeqOneByteString // - SeqTwoByteString // - SlicedString // - ConsString // - ThinString // - ExternalString // - ExternalOneByteString // - ExternalTwoByteString // - InternalizedString // - SeqInternalizedString // - SeqOneByteInternalizedString // - SeqTwoByteInternalizedString // - ConsInternalizedString // - ExternalInternalizedString // - ExternalOneByteInternalizedString // - ExternalTwoByteInternalizedString SeqString。它们在将字符串存储在内存中的方式不同。 SeqString class是一个简单的实现:字符串只是一个字符数组。 (实际上ConsString本身是抽象的。实际的类是SeqStringSeqOneByteString,但这在这里并不重要。)ConsString但是将字符串存储为二进制树。 SeqTwoByteString有一个ConcString字段和一个first字段,它们是指向其他字符串的指针。

考虑以下代码:

second

如果v8使用let str = ""; for (let i = 0; i < 10; ++i) { str += i; } console.log(str); 来实现上述代码,则:

  • 在迭代0处,它将必须分配一个大小为1的新字符串,将旧值SeqStringstr)复制到该字符串上并附加到该""并将"0"设置为新字符串(str)。

  • 在迭代1中,它将必须分配一个大小为2的新字符串,将旧值"0"str)复制到该字符串并附加到该"0" ),并将"1"设置为新字符串(str)。

  • ...

  • 在迭代9处,它必须分配一个大小为10的新字符串,将旧值"01"str)复制到该字符串,然后追加到该"012345678"并将"9"设置为新字符串(str)。

为10个步骤复制的字符总数为1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55个字符。将55个字符移动到最后一个包含10个字符的字符串中。

实际上,v8像这样使用"0123456789"

  • 在迭代0处,分配一个新的ConsString,并将ConcString设置为旧值first,并将str设置为secondi,并将0设置为刚刚分配的新str

  • 在迭代1中,分配一个新的ConcString,并将ConcString设置为旧值first,并将str设置为second,并将"1"设置为刚刚分配的新str

  • ...

  • 在迭代9中,分配新的ConcString,并将ConcString设置为旧值first,并将str设置为second

如果我们将每个"9"表示为ConcString,其中(<first>, <second>)是其<first>字段的内容,而first是{{1} }字段,则最终结果是这样:

<second>

通过这种方式,v8避免了必须一遍又一遍地复制字符串。每一步只是一个分配,并调整了两个指针。虽然将字符串存储为树有助于加快连接速度,但它的缺点是其他操作会变慢。 v8通过flattening second树减轻了这种情况。平整上面的示例后,它变为:

(((((((((("", "0"), "1"), "2"), "3"), "4"), "5"), "6"), "7"), "8"), "9")

请注意,将ConsString展平后,这个非常("0123456789", "") 的对象就会发生变异。 (从JS代码的角度来看,字符串保持不变。只是其内部v8表示形式已更改。) 比较平坦的ConsString树比较容易,而实际上这正是v8所做的(ref):

ConsString

我们正在讨论的字符串未内部化,因此ConsString被称为(ref):

bool String::Equals(Isolate* isolate, Handle<String> one, Handle<String> two) {
  if (one.is_identical_to(two)) return true;
  if (one->IsInternalizedString() && two->IsInternalizedString()) {
    return false;
  }
  return SlowEquals(isolate, one, two);
}

我在这里显示了比较字符串是否相等以在内部进行扁平化,但是在许多其他地方也可以找到对SlowEquals的调用。您的两个测试最终都通过不同的方法使字符串变平。

对于您的代码,结果是这样的:

  1. 您的bool String::SlowEquals(Isolate* isolate, Handle<String> one, Handle<String> two) { [... some shortcuts are attempted ...] one = String::Flatten(isolate, one); two = String::Flatten(isolate, two); 创建的字符串在内部存储为String::Flatten。因此,就v8而言,createConstantStrConsStringstr个对象。

  2. 您运行的第一个测试使str2ConsString变平,因此:a)此测试必须承担使字符串变平的费用,b)第二个测试从工作中受益str个对象已被拉平。 (请记住,当str2对象被展平时,该对象被突变了。因此,如果以后再次访问它,则它已经被展平。)

答案 1 :(得分:4)

我颠倒了比较操作,看起来像0 ms(firefox)上的===(有时是1毫秒)。因此,可能与尝试优化的编译器内部有关。有点像,strings在第二次比较操作中是相同的,我已经比较了它们。所以我将重用结果。

此youtube video讲得最好。

function createConstantStr(len) {
  let str = "";
  for (let i = 0; i < len; i++) {
    str += String.fromCharCode((i % 54) + 68);
  }

  return str;
}

let str = createConstantStr(1000000);
let str2 = createConstantStr(1000000);

console.time('equal by char')
let flag = true;
for (let i = 0; i < str.length; i++) {
  if (str[i] !== str2[i]) {
    flag = false;
    break;
  }
}

console.log(flag);
console.timeEnd('equal by char');

console.time('equal')
console.log(str === str2);
console.timeEnd('equal')

答案 2 :(得分:0)

(在irc://irc.freenode.net/##Javascript上获得TheWild的版权)

至少在Firefox和Chrome和Node中,str和str2是lazy-initialized,实际的createConstantStr()调用是在实际需要结果时运行的,而不是在告诉js创建结果时运行的。如果将其更改为

let str = createConstantStr(1000000);
let str2 = createConstantStr(1000000);
console.log(str[10],str2[20]);

然后将在console.log()调用中创建字符串,并且在基准测试中您将获得更多理智的结果,并且===的确更快。 (我的笔记本电脑上的 速度更快,从5毫秒的字符转换为<1毫秒,===)


原始消息:

我没有答案,但我只是想补充一点,我可以在firefox 60.6.3esr(64位)中重现它,===大约是28-31毫秒,而char大约是3-6毫秒:

function test(){
let createConstantStr=function(len) {
  let str = "";
  for (let i = 0; i < len; i++) {
    str += String.fromCharCode((i % 54) + 68);
  }

  return str;
};

let str = createConstantStr(1000000);
let str2 = createConstantStr(1000000);

console.time('equal')
console.log(str === str2);
console.timeEnd('equal')

console.time('equal by char')
let flag = true;
for (let i = 0; i < str.length; i++) {
  if (str[i] !== str2[i]) {
    flag = false;
    break;
  }
}

console.log(flag);
console.timeEnd('equal by char');
};

firefox结果:

test();
equal: 28ms
equal by char: 6ms
undefined
test();
equal: 29ms
equal by char: 4ms
undefined
test();
equal: 29ms
equal by char: 3ms
undefined
test();
equal: 31ms
equal by char: 5ms
undefined
test();
equal: 28ms
equal by char: 4ms
undefined

我可以在

中复制它
Google Chrome   74.0.3729.131 (Official Build) (64-bit) (cohort: Stable)
Revision    518a41c1fa7ce1c8bb5e22346e82e42b4d76a96f-refs/branch-heads/3729@{#954}
JavaScript  V8 7.4.288.26

chrome结果:

test();
true
equal: 23.493896484375ms
true
equal by char: 11.197021484375ms
undefined
test();
true
equal: 22.749755859375ms
true
equal by char: 11.500244140625ms
undefined
test();
true
equal: 24.43505859375ms
true
equal by char: 11.48291015625ms
undefined
test();
true
equal: 23.84521484375ms
true
equal by char: 11.38720703125ms
undefined
test();
true
equal: 21.8798828125ms
true
equal by char: 11.0390625ms
undefined
test();
true
equal: 23.989013671875ms
true
equal by char: 10.934814453125ms
undefined
  • 笔记本电脑滚动运行Intel Core i7 6700和1200MHz内存,不在电池上运行。