YDS*_*DSS 13 javascript string performance
我发现在Node.js中,通过比较两个字符串的每个字符来比较两个字符串比使用语句'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');Run Code Online (Sandbox Code Playgroud)
Lou*_*uis 26
已经向您指出,如果您翻转两个测试,则与进行比较===会比逐个进行比较要快。到目前为止,您对原因的解释还没有确切地说明原因。有一些问题会影响您的结果。
console.log打电话很贵如果我尝试这样做:
console.time("a");
console.log(1 + 2);
console.timeEnd("a");
console.time("b");
console.log("foo");
console.timeEnd("b");
Run Code Online (Sandbox Code Playgroud)
我得到类似的东西:
3
a: 3.864ms
foo
b: 0.050ms
Run Code Online (Sandbox Code Playgroud)
如果我翻转代码,以便我有这个:
console.time("b");
console.log("foo");
console.timeEnd("b");
console.time("a");
console.log(1 + 2);
console.timeEnd("a");
Run Code Online (Sandbox Code Playgroud)
然后我得到这样的东西:
foo
b: 3.538ms
3
a: 0.330ms
Run Code Online (Sandbox Code Playgroud)
如果我console.log在进行任何计时之前通过添加a来修改代码,如下所示:
console.log("start");
console.time("a");
console.log(1 + 2);
console.timeEnd("a");
console.time("b");
console.log("foo");
console.timeEnd("b");
Run Code Online (Sandbox Code Playgroud)
然后我得到类似:
start
3
a: 0.422ms
foo
b: 0.027ms
Run Code Online (Sandbox Code Playgroud)
通过console.log在开始计时之前放置“ a ”,我从计时中排除了通话的初始费用console.log。
设置测试的方式是,第一个console.log调用是通过第一个调用===或按字符进行测试中的第一个调用完成的,并且将第一个console.log调用的成本添加到该测试中。无论哪一个测试次之,都不承担该费用。最终,对于这样的测试,我宁愿移出console.log正在计时的区域之外。例如,第一个定时区域可以这样写:
console.time('equal');
const result1 = str === str2;
console.timeEnd('equal');
console.log(result1);
Run Code Online (Sandbox Code Playgroud)
将结果存储在定时区域之外result1,然后console.log(result1)在定时区域外使用可确保您看到结果,同时不计算产生的成本console.log。
Node使用v8 JavaScript引擎来运行JavaScript。v8以多种方式实现字符串。objects.h在注释中显示v8支持的类层次结构。这是与字符串有关的部分:
// - String
// - SeqString
// - SeqOneByteString
// - SeqTwoByteString
// - SlicedString
// - ConsString
// - ThinString
// - ExternalString
// - ExternalOneByteString
// - ExternalTwoByteString
// - InternalizedString
// - SeqInternalizedString
// - SeqOneByteInternalizedString
// - SeqTwoByteInternalizedString
// - ConsInternalizedString
// - ExternalInternalizedString
// - ExternalOneByteInternalizedString
// - ExternalTwoByteInternalizedString
Run Code Online (Sandbox Code Playgroud)
对于我们的讨论,有两个重要的类:SeqString和ConsString。它们在将字符串存储在内存中的方式不同。的SeqString类是一个简单的实现:该字符串是简单的字符阵列。(实际上,SeqString它本身是抽象的。实际的类是SeqOneByteString,SeqTwoByteString但在这里并不重要。)ConsString但是,将字符串存储为二进制树。A ConcString有一个first字段和一个second字段,它们是指向其他字符串的指针。
考虑以下代码:
let str = "";
for (let i = 0; i < 10; ++i) {
str += i;
}
console.log(str);
Run Code Online (Sandbox Code Playgroud)
如果v8用于SeqString实现上面的代码,则:
在第0次迭代中,它将必须分配一个大小为1的新字符串,将其旧值复制到str(""),然后追加到该值,"0"然后设置str为新字符串("0")。
在迭代1中,它必须分配一个大小为2的新字符串,将其旧值复制到str("0")并附加到该值上,"1"然后设置str为新字符串("01")。
...
在迭代9时,它将必须分配一个大小为10的新字符串,将旧值str("012345678")复制到该值,然后追加到该值并将其"9"设置str为新字符串("0123456789")。
分十步复制的字符总数为1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 = 55个字符。将55个字符移动到最后包含10个字符的字符串中。
相反,v8实际上是这样使用的ConsString:
在迭代0处,分配一个new ConcString,其first设置为的旧值str,并second设置为i(作为字符串)0,并设置str为该ConcString刚分配的新值。
在迭代1中,分配一个新值,ConcString其first设置为的旧值str,并second设置为"1",并设置str为该ConcString刚分配的新值。
...
在迭代9,分配一个新的ConcString与first集到的旧值str,并second设置为"9"。
如果我们将每个字段表示ConcString为(<first>, <second>),<first>则表示其first字段的内容,并且<second>是该字段的内容second,那么最终结果是:
(((((((((("", "0"), "1"), "2"), "3"), "4"), "5"), "6"), "7"), "8"), "9")
Run Code Online (Sandbox Code Playgroud)
通过这种方式,v8避免了必须一遍又一遍地复制字符串。每一步只是一个分配,并调整了两个指针。虽然将字符串存储为树有助于加快连接速度,但它的缺点是其他操作会变慢。v8通过展平 ConsString树木来缓解这种情况。平展上面的示例后,它变为:
("0123456789", "")
Run Code Online (Sandbox Code Playgroud)
请注意,将a展ConsString平时,此ConsString对象被突变。(从JS代码的角度来看,该字符串保持不变。只是其内部v8表示形式有所更改。)比较扁平化的ConsString树更容易,实际上这正是v8所做的(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);
}
Run Code Online (Sandbox Code Playgroud)
我们正在讨论的字符串未内部化,因此SlowEquals称为(ref):
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);
Run Code Online (Sandbox Code Playgroud)
我在这里显示过,比较字符串是否相等以在内部进行扁平化,但是String::Flatten在许多其他地方都可以找到to 的调用。您的两个测试最终都通过不同的方法使字符串变平。
对于您的代码,结果是这样的:
您createConstantStr创建的字符串在内部存储为ConsString。所以,str和str2是ConsString对象,至于V8而言。
您运行的第一个测试会导致str并且str2将其扁平化:a)此测试必须承担扁平化字符串的费用,b)第二个测试得益于使用ConcString已经扁平化的对象。(请记住,当某个ConcString对象被展平时,该对象会发生突变。因此,如果稍后再次访问它,则它已经被展平。)