为什么字符串拼接比数组连接更快?

128
今天我阅读了这篇关于字符串连接速度的帖子。令人惊讶的是,字符串连接速度更快:

http://jsben.ch/#/OJ3vo

结果与我想象的相反。此外,有很多文章对此进行了解释,比如这篇文章。 我可以猜测浏览器优化了最新版本的字符串concat,但他们是如何做到的?我们可以说在连接字符串时使用+更好吗?
更新 在现代浏览器中,字符串连接已经被优化,因此在连接字符串时使用+比使用join更快。 但是@Arthur指出,如果您想要使用分隔符连接字符串,则join更快。
更新-2020
Chrome:当您想要连接数组并生成字符串时,join几乎比字符串连接+2倍。 请参见:https://dev59.com/Pmw05IYBdhLWcg3wVwc4#54970240 作为说明:
  • 如果您有大字符串,则使用Array join更好
  • 如果我们需要在最终输出中生成多个小字符串,那么最好使用字符串连接+,否则,最后一步将需要多次数组转换为字符串,这会导致性能负担。


1
这段代码本应该产生 500 TB 的垃圾数据,但运行时间只有 200 毫秒。我认为他们只是为字符串分配了略微更多的空间,当你将一个短字符串添加到它时,它通常适合额外的空间中。 - Ivan Kuckir
10个回答

155
浏览器字符串优化已经改变了字符串拼接的情况。Firefox是第一个优化字符串拼接的浏览器。从1.0版本开始,使用加号运算符在所有情况下比使用数组技术更慢。其他浏览器也优化了字符串拼接,因此Safari、Opera、Chrome和Internet Explorer 8使用加号运算符的性能也更好。版本8之前的Internet Explorer没有这样的优化,所以数组技术总是比加号运算符快。--编写高效JavaScript:第7章-更快的网站 V8 JavaScript引擎(用于Google Chrome)使用此代码进行字符串拼接。
// ECMA-262, section 15.5.4.6
function StringConcat() {
  if (IS_NULL_OR_UNDEFINED(this) && !IS_UNDETECTABLE(this)) {
    throw MakeTypeError("called_on_null_or_undefined", ["String.prototype.concat"]);
  }
  var len = %_ArgumentsLength();
  var this_as_string = TO_STRING_INLINE(this);
  if (len === 1) {
    return this_as_string + %_Arguments(0);
  }
  var parts = new InternalArray(len + 1);
  parts[0] = this_as_string;
  for (var i = 0; i < len; i++) {
    var part = %_Arguments(i);
    parts[i + 1] = TO_STRING_INLINE(part);
  }
  return %StringBuilderConcat(parts, len + 1, "");
}

因此,他们通过创建一个InternalArray(变量parts)来进行内部优化,然后填充它。使用这些部分调用StringBuilderConcat函数。由于StringBuilderConcat函数是一些经过重度优化的C++代码,因此速度非常快。这里引用太长,但在runtime.cc文件中搜索RUNTIME_FUNCTION(MaybeObject*,Runtime_StringBuilderConcat)即可查看代码。


4
你忽略了真正有趣的事情,这个数组仅用于使用不同的参数数量调用Runtime_StringBuilderConcat。但真正的工作是在那里完成的。 - evilpie
44
优化101:你应该追求最少的缓慢!例如,arr.join vs str+,在Chrome上,你的操作每秒为25k/s vs 52k/s。在Firefox新版中,你的操作每秒为76k/s vs 212k/s。因此,str+更快。但是让我们看看其他浏览器。Opera的操作每秒为43k/s vs 26k/s。IE的操作每秒为1300/s vs 1002/s。看到发生了什么吗?唯一需要优化的浏览器将使用在所有其他浏览器上较慢但在其余浏览器上并不重要的操作。因此,所有这些文章都没有理解性能方面的任何内容。 - gcb
49
@gcb,仅加入速度更快的浏览器不应该被使用。我的用户中有95%使用Firefox和Chrome。我将对95%的情况进行优化。 - Paul Draper
7
如果90%的用户使用快速浏览器,而且无论你选择哪种选项都只能为他们节省0.001秒,但是如果你选择惩罚其他用户来争取那0.001秒的话,那么有10%的用户将会获得2秒的优势……这个决定很明显。如果你看不到这一点,我为你编写代码的对象感到遗憾。 - gcb
8
老旧的浏览器最终会被淘汰,但很少有人会回来转换所有那些数组连接。只要不给当前用户带来很大的不便,编写面向未来的代码是更好的选择。当处理旧浏览器时,除了拼接性能以外,还有更重要的事情需要关注。 - Thomas Higginbotham
显示剩余5条评论

26

Firefox之所以快速是因为它使用了一种叫做Ropes的东西 (Ropes: an Alternative to Strings),一个Rope基本上就是一个有向无环图,其中每个节点都是一个字符串。

举个例子,如果你执行 a = 'abc'.concat('def'), 那么新创建的对象会像这样。 当然,在内存中它看起来并不完全是这样,因为你仍然需要为字符串类型、长度和可能的其他字段保留空间。

a = {
 nodeA: 'abc',
 nodeB: 'def'
}

同时b = a.concat('123')

b = {
  nodeA: a, /* {
             nodeA: 'abc',
             nodeB: 'def'
          } */
  nodeB: '123'
}           

所以在最简单的情况下,虚拟机几乎不需要做任何工作。唯一的问题是这会稍微减慢对产生的字符串的其他操作。当然这也会减少内存开销。

另一方面,['abc', 'def'].join('')通常只会分配内存来将新字符串平铺在内存中。(也许应该对此进行优化)


12

对于大量数据的连接,连接操作更快,因此问题陈述不正确。

let result = "";
let startTime = new Date().getTime();
for (let i = 0; i < 2000000; i++) {
    result += "x";
}
console.log("concatenation time: " + (new Date().getTime() - startTime));

startTime = new Date().getTime();
let array = new Array(2000000);
for (let i = 0; i < 2000000; i++) {
    array[i] = "x";
}
result = array.join("");
console.log("join time: " + (new Date().getTime() - startTime));

已测试在Chrome 72.0.3626.119,Firefox 65.0.1,Edge 42.17134.1.0上。请注意,即使包括数组创建,它也更快!


2020年8月。正确。在Chrome中:数组连接时间:462。字符串连接(+)时间:827。连接几乎快了2倍。 - Manohar Reddy Poreddy
点击“运行代码片段”几次,看看会发生什么。 - Julian

8

我知道这是一个旧的线程,但您的测试结果是不正确的。您的代码 output += myarray[i]; 应该更改为 output += "" + myarray[i];,因为您忘记了需要使用某种连接符将数组项粘在一起。正确的拼接代码应该如下:

var output = myarray[0];
for (var i = 1, len = myarray.length; i<len; i++){
    output += "" + myarray[i];
}

这样做会因为将元素粘合在一起而导致需要执行两个操作,而不是一个。

Array.join() 更快。


1
我不明白你的回答。在代码中,使用"" +和原始方法有什么区别? - Sanghyun Lee
2
为什么我们需要加上它?我们已经在不使用它的情况下将项目粘贴到“输出”中了。 - Sanghyun Lee
因为这就是join的工作原理。例如,您也可以执行Array.join(","),这将无法与您的for循环一起使用。 - Arthur
是的,如果你使用 "",它只会快3%,但如果你至少使用 " ",差异就更加明显了。此外,连接在第一次运行时胜出,但在此之后的每个连续运行中都会失败(我认为这是由于在第一次运行期间进行了代码优化)。 - Arthur
这是一个重要的观点。让我更新我的问题,包括这个发现。谢谢你让我知道这个。 - Sanghyun Lee
显示剩余3条评论

2
我认为,对于字符串而言,更容易预分配一个较大的缓冲区。每个元素只有2个字节(如果是UNICODE),因此即使您保守一些,也可以为字符串预分配一个相当大的缓冲区。对于数组,每个元素都比较“复杂”,因为每个元素都是一个对象,因此保守实现将为较少的元素预分配空间。
如果在每个for之前添加for(j=0;j<1000;j++),则会发现(在Chrome下)速度差异变小。最终,字符串连接仍然是1.5倍,但比之前的2.6要小。
并且,需要复制元素,Unicode字符可能比JS对象的引用更小。
请注意,许多JS引擎的实现可能针对单类型数组进行了优化,这将使我所写的所有内容都无用。

2

这些基准测试非常简单。重复连接相同的三个项将被内联,结果将被证明是确定性的和记忆化的,垃圾处理程序将只丢弃数组对象(它们的大小几乎可以忽略不计),并且可能仅由于没有外部引用以及字符串从未更改而被推入和弹出堆栈。如果测试涉及大量随机生成的字符串,我会更加印象深刻。比如一两千兆字节的字符串。

使用Array.join方法胜利吧!


1

这个测试展示了使用赋值拼接的字符串与使用数组.join方法生成的字符串的性能差异。在Chrome v31中,尽管赋值的整体速度仍然是数组.join方法的两倍,但当使用生成的字符串时,这种差距不再那么巨大。


1

截至2021年在Chrome上,对于10^4或10^5个字符串,数组push+join的速度约慢10倍,但对于10^6个字符串仅慢1.2倍。

可以在https://jsben.ch/dhIy上尝试。


1
there is no test on the link - Ayman Morsy

0
这显然取决于JavaScript引擎的实现。即使是同一个引擎的不同版本,你也可能得到显著不同的结果。你应该自己进行基准测试来验证这一点。
我会说在最近的V8版本中,String.concat的性能更好。但对于Firefox和Opera来说,Array.join是赢家。

-1

我的猜测是,虽然每个版本都承担了许多连接的成本,但连接版本还会额外构建数组。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接