在 Java 和 C# 等语言中,通常使用 StringBuilder 连接大量字符串以获得线性渐近而不是二次渐近。
然而,JavaScript 以某种方式只能处理一种 String 类型。级联的渐近线在那里是线性的,至少对于高达 131072 次迭代的循环。但是,奇怪的是,时间并不取决于添加的字符串的长度。至少在 Chrome 中是这样的。
这是怎么发生的?
还有一个给 JS 鉴赏家的额外问题:262144 到底发生了什么?
using System;
using System.Collections.Generic;
using System.Diagnostics;
public class Program
{
private static string Test(int n, string s)
{
var res = "";
for (var q=0; q<n; ++q)
res += s;
return res;
}
public static void Main()
{
var res = new Dictionary<string, double>[10];
const int N = 1024;
var sw = new Stopwatch();
for (var n=0; n<res.Length; ++n)
{
res[n] = new Dictionary<string, double>();
foreach (var s in new string[] {"!", "!2", "!234", "!2345678"})
{
res[n][s] = 0;
for (var q=0; q<N; ++q)
{
sw.Restart();
Test(1 << n, s);
sw.Stop();
res[n][s] += sw.ElapsedTicks;
}
res[n][s] /= N;
}
}
for (var n=0; n<res.Length; ++n)
{
Console.Write("{0,2} {1,7} ", n, 1<<n);
foreach (var kvp in res[n])
Console.Write("{0,10:0.000} ", kvp.Value / 1000);
Console.WriteLine();
}
}
}
0 1 0.001 0.000 0.000 0.000
1 2 0.002 0.001 0.001 0.001
2 4 0.003 0.003 0.005 0.003
3 8 0.006 0.006 0.007 0.007
4 16 0.013 0.015 0.013 0.014
5 32 0.025 0.025 0.032 0.034
6 64 0.050 0.059 0.070 0.097
7 128 0.120 0.142 0.220 0.303
8 256 0.337 0.417 0.630 0.972
9 512 0.897 1.256 1.964 4.087
不幸的是,随着迭代次数的增加,该程序不适合在 ideone 上分配的 5 秒执行时间。以下是我家用电脑的结果:
D:\Temp\Supertemp>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe StringConcat.cs && StringConcat.exe
Microsoft (R) Visual C# Compiler version 4.7.2046.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.
This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240
0 1 0.000 0.000 0.000 0.000
1 2 0.000 0.000 0.000 0.000
2 4 0.000 0.000 0.000 0.000
3 8 0.001 0.001 0.001 0.001
4 16 0.002 0.001 0.001 0.002
5 32 0.002 0.004 0.004 0.004
6 64 0.005 0.006 0.009 0.014
7 128 0.013 0.019 0.028 0.043
8 256 0.042 0.057 0.087 0.148
9 512 0.118 0.174 0.302 0.559
10 1024 0.354 0.606 1.124 2.279
11 2048 1.220 2.242 4.545 10.041
12 4096 4.517 8.982 19.706 41.568
13 8192 17.864 39.063 82.814 169.274
14 16384 78.454 165.893 337.830 718.843
function test(n, s) {
var res = '';
for (var q=0; q<n; ++q) {
res += s;
}
return res;
}
var res = [], N = 1024;
for (var n=0; n<=18; ++n) {
res.push({len: 1<<n});
for (var s of ['!', '!2', '!234', '!2345678']) {
res[n][s] = 0;
for (var q=0; q<N; ++q) {
var t = performance.now();
test(1 << n, s);
t = performance.now() - t;
res[n][s] += t;
}
res[n][s] /= N;
res[n][s] = res[n][s].toFixed(3);
}
}
console.table(res)
为什么会这样?
从V8 sorts来看,字符串针对连接进行了优化。连接时,不是创建新字符串,而是创建一个引用被连接片段的实例。每个块也可以引用子块。这样一棵二叉树就建立起来了。
还有子字符串的字符串子类,单字节和双字节字符串显式分开等等。
结果,像连接这样简单的操作变成了这样:
任何这样的智慧总是一种妥协,上面覆盖着启发式方法。JavaScript 引擎会尝试猜测何时使用哪种方法更有利可图:何时创建完整的字符串,何时构建字符串的二叉树;当连接速度更重要时,当迭代速度更重要时;等等。
一个明显的优势:可能非常慢的操作变得更快。这对于程序员经常使用某些类使用模式的情况尤其重要,例如字符串连接。但缺点也很明显:其他操作变慢,此外,引擎并不总是猜测程序员的意图并选择最优化的方式。
为什么这是在 JavaScript 中完成的?
因为有意使语言易于理解和使用。魔法越多,你就越不用考虑内部表示,写程序就越容易。
此外,在创建该语言时,执行速度根本不重要,因为没有人用该语言编写复杂的程序。复杂的程序出现得晚得多。如果您不是在最新版本的 Chrome 中运行此程序,而是在任何 20 年前的浏览器中运行,您会发现性能有很大不同。
为什么不用其他语言来完成?
这取决于语言及其目的。例如,C++中的字符串简单而笨拙,要求程序员指定每个动作,在循环体中或外部声明字符串,通过引用和通过值将字符串传递给函数之间在性能上存在差异,程序员需要提前指明字符串的长度,等等。进一步。
另一方面,在 PHP 中,数组就像 JS 中的字符串,在复杂性方面会让你领先一步。数组可以是向量、字典、集合等。它们具有针对每种情况进行优化的复杂结构。
通常,标准类的方法因语言而异,甚至在同一语言编译器的不同版本之间也不同。今天,类型化数组出现在 JavaScript 和 PHP 中,明天可能会出现 StringBuilder。