我发现在 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');
答案 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)
产生的成本。
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
本身是抽象的。实际的类是SeqString
和SeqOneByteString
,但这在这里并不重要。)ConsString
但是将字符串存储为二进制树。 SeqTwoByteString
有一个ConcString
字段和一个first
字段,它们是指向其他字符串的指针。
考虑以下代码:
second
如果v8使用let str = "";
for (let i = 0; i < 10; ++i) {
str += i;
}
console.log(str);
来实现上述代码,则:
在迭代0处,它将必须分配一个大小为1的新字符串,将旧值SeqString
(str
)复制到该字符串上并附加到该""
并将"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
设置为second
( i
,并将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
的调用。您的两个测试最终都通过不同的方法使字符串变平。
对于您的代码,结果是这样的:
您的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而言,createConstantStr
和ConsString
是str
个对象。
您运行的第一个测试使str2
和ConsString
变平,因此: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